Merge pull request #28 from zed-industries/close-tabs

Add tab close buttons
This commit is contained in:
Nathan Sobo
2021-04-28 13:19:42 -06:00
committed by GitHub
18 changed files with 501 additions and 184 deletions
+134 -55
View File
@@ -9,7 +9,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
use keymap::MatchResult;
use parking_lot::Mutex;
use parking_lot::{Mutex, RwLock};
use pathfinder_geometry::{rect::RectF, vector::vec2f};
use platform::Event;
use postage::{sink::Sink as _, stream::Stream as _};
@@ -216,7 +216,7 @@ impl App {
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.0.borrow().font_cache.clone()
self.0.borrow().ctx.font_cache.clone()
}
fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
@@ -326,7 +326,7 @@ impl TestAppContext {
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.0.borrow().font_cache.clone()
self.0.borrow().ctx.font_cache.clone()
}
pub fn platform(&self) -> Rc<dyn platform::Platform> {
@@ -370,7 +370,6 @@ type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext);
pub struct MutableAppContext {
weak_self: Option<rc::Weak<RefCell<Self>>>,
platform: Rc<dyn platform::Platform>,
font_cache: Arc<FontCache>,
assets: Arc<AssetCache>,
ctx: AppContext,
actions: HashMap<TypeId, HashMap<String, Vec<Box<ActionCallback>>>>,
@@ -404,14 +403,15 @@ impl MutableAppContext {
Self {
weak_self: None,
platform,
font_cache: Arc::new(FontCache::new(fonts)),
assets: Arc::new(AssetCache::new(asset_source)),
ctx: AppContext {
models: HashMap::new(),
windows: HashMap::new(),
models: Default::default(),
windows: Default::default(),
values: Default::default(),
ref_counts: Arc::new(Mutex::new(RefCounts::default())),
background: Arc::new(executor::Background::new()),
thread_pool: scoped_pool::Pool::new(num_cpus::get(), "app"),
font_cache: Arc::new(FontCache::new(fonts)),
},
actions: HashMap::new(),
global_actions: HashMap::new(),
@@ -443,7 +443,7 @@ impl MutableAppContext {
}
pub fn font_cache(&self) -> &Arc<FontCache> {
&self.font_cache
&self.ctx.font_cache
}
pub fn foreground_executor(&self) -> &Rc<executor::Foreground> {
@@ -584,6 +584,11 @@ impl MutableAppContext {
);
}
pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) {
self.pending_effects
.push_back(Effect::ViewNotification { window_id, view_id });
}
pub fn dispatch_action<T: 'static + Any>(
&mut self,
window_id: usize,
@@ -594,7 +599,7 @@ impl MutableAppContext {
self.dispatch_action_any(window_id, &responder_chain, name, Box::new(arg).as_ref());
}
fn dispatch_action_any(
pub(crate) fn dispatch_action_any(
&mut self,
window_id: usize,
path: &[usize],
@@ -763,7 +768,7 @@ impl MutableAppContext {
let text_layout_cache = TextLayoutCache::new(self.platform.fonts());
let presenter = Rc::new(RefCell::new(Presenter::new(
window_id,
self.font_cache.clone(),
self.ctx.font_cache.clone(),
text_layout_cache,
self.assets.clone(),
self,
@@ -787,15 +792,7 @@ impl MutableAppContext {
}
}
let actions = presenter.borrow_mut().dispatch_event(event, ctx.as_ref());
for action in actions {
ctx.dispatch_action_any(
window_id,
&action.path,
action.name,
action.arg.as_ref(),
);
}
presenter.borrow_mut().dispatch_event(event, ctx);
})
}));
}
@@ -865,8 +862,9 @@ impl MutableAppContext {
fn remove_dropped_entities(&mut self) {
loop {
let (dropped_models, dropped_views) = self.ctx.ref_counts.lock().take_dropped();
if dropped_models.is_empty() && dropped_views.is_empty() {
let (dropped_models, dropped_views, dropped_values) =
self.ctx.ref_counts.lock().take_dropped();
if dropped_models.is_empty() && dropped_views.is_empty() && dropped_values.is_empty() {
break;
}
@@ -890,6 +888,11 @@ impl MutableAppContext {
window.views.remove(&view_id);
}
}
let mut values = self.ctx.values.write();
for key in dropped_values {
values.remove(&key);
}
}
}
@@ -910,11 +913,14 @@ impl MutableAppContext {
self.focus(window_id, view_id);
}
}
if self.pending_effects.is_empty() {
self.remove_dropped_entities();
self.update_windows();
}
}
self.flushing_effects = false;
self.remove_dropped_entities();
self.update_windows();
}
}
@@ -1308,9 +1314,11 @@ impl AsRef<AppContext> for MutableAppContext {
pub struct AppContext {
models: HashMap<usize, Box<dyn AnyModel>>,
windows: HashMap<usize, Window>,
values: RwLock<HashMap<(TypeId, usize), Box<dyn Any>>>,
background: Arc<executor::Background>,
ref_counts: Arc<Mutex<RefCounts>>,
thread_pool: scoped_pool::Pool,
font_cache: Arc<FontCache>,
}
impl AppContext {
@@ -1350,9 +1358,20 @@ impl AppContext {
&self.background
}
pub fn font_cache(&self) -> &FontCache {
&self.font_cache
}
pub fn thread_pool(&self) -> &scoped_pool::Pool {
&self.thread_pool
}
pub fn value<Tag: 'static, T: 'static + Default>(&self, id: usize) -> ValueHandle<T> {
let key = (TypeId::of::<Tag>(), id);
let mut values = self.values.write();
values.entry(key).or_insert_with(|| Box::new(T::default()));
ValueHandle::new(TypeId::of::<Tag>(), id, &self.ref_counts)
}
}
impl ReadModel for AppContext {
@@ -1823,12 +1842,7 @@ impl<'a, T: View> ViewContext<'a, T> {
}
pub fn notify(&mut self) {
self.app
.pending_effects
.push_back(Effect::ViewNotification {
window_id: self.window_id,
view_id: self.view_id,
});
self.app.notify_view(self.window_id, self.view_id);
}
pub fn propagate_action(&mut self) {
@@ -1958,7 +1972,7 @@ pub struct ModelHandle<T> {
impl<T: Entity> ModelHandle<T> {
fn new(model_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
ref_counts.lock().inc(model_id);
ref_counts.lock().inc_entity(model_id);
Self {
model_id,
model_type: PhantomData,
@@ -2031,7 +2045,7 @@ impl<T: Entity> ModelHandle<T> {
impl<T> Clone for ModelHandle<T> {
fn clone(&self) -> Self {
if let Some(ref_counts) = self.ref_counts.upgrade() {
ref_counts.lock().inc(self.model_id);
ref_counts.lock().inc_entity(self.model_id);
}
Self {
@@ -2122,7 +2136,7 @@ pub struct ViewHandle<T> {
impl<T: View> ViewHandle<T> {
fn new(window_id: usize, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
ref_counts.lock().inc(view_id);
ref_counts.lock().inc_entity(view_id);
Self {
window_id,
view_id,
@@ -2205,7 +2219,7 @@ impl<T: View> ViewHandle<T> {
impl<T> Clone for ViewHandle<T> {
fn clone(&self) -> Self {
if let Some(ref_counts) = self.ref_counts.upgrade() {
ref_counts.lock().inc(self.view_id);
ref_counts.lock().inc_entity(self.view_id);
}
Self {
@@ -2282,7 +2296,7 @@ impl AnyViewHandle {
impl<T: View> From<&ViewHandle<T>> for AnyViewHandle {
fn from(handle: &ViewHandle<T>) -> Self {
if let Some(ref_counts) = handle.ref_counts.upgrade() {
ref_counts.lock().inc(handle.view_id);
ref_counts.lock().inc_entity(handle.view_id);
}
AnyViewHandle {
window_id: handle.window_id,
@@ -2342,48 +2356,113 @@ impl<T> Clone for WeakViewHandle<T> {
}
}
pub struct ValueHandle<T> {
value_type: PhantomData<T>,
tag_type_id: TypeId,
id: usize,
ref_counts: Weak<Mutex<RefCounts>>,
}
impl<T: 'static> ValueHandle<T> {
fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
ref_counts.lock().inc_value(tag_type_id, id);
Self {
value_type: PhantomData,
tag_type_id,
id,
ref_counts: Arc::downgrade(ref_counts),
}
}
pub fn read<R>(&self, ctx: &AppContext, f: impl FnOnce(&T) -> R) -> R {
f(ctx
.values
.read()
.get(&(self.tag_type_id, self.id))
.unwrap()
.downcast_ref()
.unwrap())
}
pub fn update<R>(&self, ctx: &AppContext, f: impl FnOnce(&mut T) -> R) -> R {
f(ctx
.values
.write()
.get_mut(&(self.tag_type_id, self.id))
.unwrap()
.downcast_mut()
.unwrap())
}
}
impl<T> Drop for ValueHandle<T> {
fn drop(&mut self) {
if let Some(ref_counts) = self.ref_counts.upgrade() {
ref_counts.lock().dec_value(self.tag_type_id, self.id);
}
}
}
#[derive(Default)]
struct RefCounts {
counts: HashMap<usize, usize>,
entity_counts: HashMap<usize, usize>,
value_counts: HashMap<(TypeId, usize), usize>,
dropped_models: HashSet<usize>,
dropped_views: HashSet<(usize, usize)>,
dropped_values: HashSet<(TypeId, usize)>,
}
impl RefCounts {
fn inc(&mut self, model_id: usize) {
*self.counts.entry(model_id).or_insert(0) += 1;
fn inc_entity(&mut self, model_id: usize) {
*self.entity_counts.entry(model_id).or_insert(0) += 1;
}
fn inc_value(&mut self, tag_type_id: TypeId, id: usize) {
*self.value_counts.entry((tag_type_id, id)).or_insert(0) += 1;
}
fn dec_model(&mut self, model_id: usize) {
if let Some(count) = self.counts.get_mut(&model_id) {
*count -= 1;
if *count == 0 {
self.counts.remove(&model_id);
self.dropped_models.insert(model_id);
}
} else {
panic!("Expected ref count to be positive")
let count = self.entity_counts.get_mut(&model_id).unwrap();
*count -= 1;
if *count == 0 {
self.entity_counts.remove(&model_id);
self.dropped_models.insert(model_id);
}
}
fn dec_view(&mut self, window_id: usize, view_id: usize) {
if let Some(count) = self.counts.get_mut(&view_id) {
*count -= 1;
if *count == 0 {
self.counts.remove(&view_id);
self.dropped_views.insert((window_id, view_id));
}
} else {
panic!("Expected ref count to be positive")
let count = self.entity_counts.get_mut(&view_id).unwrap();
*count -= 1;
if *count == 0 {
self.entity_counts.remove(&view_id);
self.dropped_views.insert((window_id, view_id));
}
}
fn take_dropped(&mut self) -> (HashSet<usize>, HashSet<(usize, usize)>) {
fn dec_value(&mut self, tag_type_id: TypeId, id: usize) {
let key = (tag_type_id, id);
let count = self.value_counts.get_mut(&key).unwrap();
*count -= 1;
if *count == 0 {
self.value_counts.remove(&key);
self.dropped_values.insert(key);
}
}
fn take_dropped(
&mut self,
) -> (
HashSet<usize>,
HashSet<(usize, usize)>,
HashSet<(TypeId, usize)>,
) {
let mut dropped_models = HashSet::new();
let mut dropped_views = HashSet::new();
let mut dropped_values = HashSet::new();
std::mem::swap(&mut self.dropped_models, &mut dropped_models);
std::mem::swap(&mut self.dropped_views, &mut dropped_views);
(dropped_models, dropped_views)
std::mem::swap(&mut self.dropped_values, &mut dropped_values);
(dropped_models, dropped_views, dropped_values)
}
}
+2
View File
@@ -7,6 +7,7 @@ mod event_handler;
mod flex;
mod label;
mod line_box;
mod mouse_event_handler;
mod new;
mod stack;
mod svg;
@@ -22,6 +23,7 @@ pub use event_handler::*;
pub use flex::*;
pub use label::*;
pub use line_box::*;
pub use mouse_event_handler::*;
pub use new::*;
pub use stack::*;
pub use svg::*;
+134
View File
@@ -0,0 +1,134 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
AfterLayoutContext, AppContext, DebugContext, Element, ElementBox, Event, EventContext,
LayoutContext, PaintContext, SizeConstraint, ValueHandle,
};
use serde_json::json;
pub struct MouseEventHandler {
state: ValueHandle<MouseState>,
child: ElementBox,
click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MouseState {
pub hovered: bool,
pub clicked: bool,
}
impl MouseEventHandler {
pub fn new<Tag, F>(id: usize, ctx: &AppContext, render_child: F) -> Self
where
Tag: 'static,
F: FnOnce(MouseState) -> ElementBox,
{
let state_handle = ctx.value::<Tag, _>(id);
let state = state_handle.read(ctx, |state| *state);
let child = render_child(state);
Self {
state: state_handle,
child,
click_handler: None,
}
}
pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
self.click_handler = Some(Box::new(handler));
self
}
}
impl Element for MouseEventHandler {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
ctx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, ctx), ())
}
fn after_layout(
&mut self,
_: Vector2F,
_: &mut Self::LayoutState,
ctx: &mut AfterLayoutContext,
) {
self.child.after_layout(ctx);
}
fn paint(
&mut self,
bounds: RectF,
_: &mut Self::LayoutState,
ctx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), ctx);
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
ctx: &mut EventContext,
) -> bool {
let click_handler = self.click_handler.as_mut();
let handled_in_child = self.child.dispatch_event(event, ctx);
self.state.update(ctx.app, |state| match event {
Event::MouseMoved { position } => {
let mouse_in = bounds.contains_point(*position);
if state.hovered != mouse_in {
state.hovered = mouse_in;
ctx.notify();
true
} else {
handled_in_child
}
}
Event::LeftMouseDown { position, .. } => {
if !handled_in_child && bounds.contains_point(*position) {
state.clicked = true;
ctx.notify();
true
} else {
handled_in_child
}
}
Event::LeftMouseUp { position, .. } => {
if !handled_in_child && state.clicked {
state.clicked = false;
ctx.notify();
if let Some(handler) = click_handler {
if bounds.contains_point(*position) {
handler(ctx);
}
}
true
} else {
handled_in_child
}
}
_ => handled_in_child,
})
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
ctx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "MouseEventHandler",
"child": self.child.debug(ctx),
})
}
}
+5 -3
View File
@@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde_json::json;
use crate::{
@@ -11,14 +13,14 @@ use crate::{
};
pub struct Svg {
path: String,
path: Cow<'static, str>,
color: ColorU,
}
impl Svg {
pub fn new(path: String) -> Self {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
path,
path: path.into(),
color: ColorU::black(),
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum Event {
KeyDown {
keystroke: Keystroke,
@@ -21,4 +21,7 @@ pub enum Event {
LeftMouseDragged {
position: Vector2F,
},
MouseMoved {
position: Vector2F,
},
}
+6
View File
@@ -108,6 +108,12 @@ impl Event {
),
precise: native_event.hasPreciseScrollingDeltas() == YES,
}),
NSEventType::NSMouseMoved => window_height.map(|window_height| Self::MouseMoved {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
}),
_ => None,
}
}
+13 -11
View File
@@ -153,7 +153,8 @@ impl Renderer {
atlas_id,
shader_data: shaders::GPUISprite {
origin: origin.floor().to_float2(),
size: size.to_float2(),
target_size: size.to_float2(),
source_size: size.to_float2(),
atlas_origin: atlas_origin.to_float2(),
color: path.color.to_uchar4(),
compute_winding: 1,
@@ -493,7 +494,8 @@ impl Renderer {
.or_insert_with(Vec::new)
.push(shaders::GPUISprite {
origin: origin.to_float2(),
size: sprite.size.to_float2(),
target_size: sprite.size.to_float2(),
source_size: sprite.size.to_float2(),
atlas_origin: sprite.atlas_origin.to_float2(),
color: glyph.color.to_uchar4(),
compute_winding: 0,
@@ -502,21 +504,21 @@ impl Renderer {
}
for icon in layer.icons() {
let sprite = self.sprite_cache.render_icon(
icon.bounds.size(),
icon.path.clone(),
icon.svg.clone(),
scene.scale_factor(),
);
let origin = icon.bounds.origin() * scene.scale_factor();
let target_size = icon.bounds.size() * scene.scale_factor();
let source_size = (target_size * 2.).ceil().to_i32();
let sprite =
self.sprite_cache
.render_icon(source_size, icon.path.clone(), icon.svg.clone());
// Snap sprite to pixel grid.
let origin = (icon.bounds.origin() * scene.scale_factor()).floor();
sprites_by_atlas
.entry(sprite.atlas_id)
.or_insert_with(Vec::new)
.push(shaders::GPUISprite {
origin: origin.to_float2(),
size: sprite.size.to_float2(),
target_size: target_size.to_float2(),
source_size: sprite.size.to_float2(),
atlas_origin: sprite.atlas_origin.to_float2(),
color: icon.color.to_uchar4(),
compute_winding: 0,
+2 -1
View File
@@ -49,7 +49,8 @@ typedef enum {
typedef struct {
vector_float2 origin;
vector_float2 size;
vector_float2 target_size;
vector_float2 source_size;
vector_float2 atlas_origin;
vector_uchar4 color;
uint8_t compute_winding;
+2 -2
View File
@@ -186,9 +186,9 @@ vertex SpriteFragmentInput sprite_vertex(
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUISprite sprite = sprites[sprite_id];
float2 position = unit_vertex * sprite.size + sprite.origin;
float2 position = unit_vertex * sprite.target_size + sprite.origin;
float4 device_position = to_device_position(position, *viewport_size);
float2 atlas_position = (unit_vertex * sprite.size + sprite.atlas_origin) / *atlas_size;
float2 atlas_position = (unit_vertex * sprite.source_size + sprite.atlas_origin) / *atlas_size;
return SpriteFragmentInput {
device_position,
+4 -6
View File
@@ -9,7 +9,7 @@ use crate::{
use etagere::BucketedAtlasAllocator;
use metal::{MTLPixelFormat, TextureDescriptor};
use ordered_float::OrderedFloat;
use std::{collections::HashMap, sync::Arc};
use std::{borrow::Cow, collections::HashMap, sync::Arc};
#[derive(Hash, Eq, PartialEq)]
struct GlyphDescriptor {
@@ -29,7 +29,7 @@ pub struct GlyphSprite {
#[derive(Hash, Eq, PartialEq)]
struct IconDescriptor {
path: String,
path: Cow<'static, str>,
width: i32,
height: i32,
}
@@ -137,15 +137,13 @@ impl SpriteCache {
pub fn render_icon(
&mut self,
size: Vector2F,
path: String,
size: Vector2I,
path: Cow<'static, str>,
svg: usvg::Tree,
scale_factor: f32,
) -> IconSprite {
let atlases = &mut self.atlases;
let atlas_size = self.atlas_size;
let device = &self.device;
let size = (size * scale_factor).round().to_i32();
assert!(size.x() < atlas_size.x());
assert!(size.y() < atlas_size.y());
self.icons
+4
View File
@@ -82,6 +82,10 @@ unsafe fn build_classes() {
sel!(mouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseMoved:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
+41 -8
View File
@@ -9,7 +9,11 @@ use crate::{
};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use std::{any::Any, collections::HashMap, sync::Arc};
use std::{
any::Any,
collections::{HashMap, HashSet},
sync::Arc,
};
pub struct Presenter {
window_id: usize,
@@ -18,6 +22,7 @@ pub struct Presenter {
font_cache: Arc<FontCache>,
text_layout_cache: TextLayoutCache,
asset_cache: Arc<AssetCache>,
last_mouse_moved_event: Option<Event>,
}
impl Presenter {
@@ -35,6 +40,7 @@ impl Presenter {
font_cache,
text_layout_cache,
asset_cache,
last_mouse_moved_event: None,
}
}
@@ -80,6 +86,10 @@ impl Presenter {
};
ctx.paint(root_view_id, Vector2F::zero());
self.text_layout_cache.finish_frame();
if let Some(event) = self.last_mouse_moved_event.clone() {
self.dispatch_event(event, app)
}
} else {
log::error!("could not find root_view_id for window {}", self.window_id);
}
@@ -114,20 +124,37 @@ impl Presenter {
}
}
pub fn dispatch_event(&mut self, event: Event, app: &AppContext) -> Vec<ActionToDispatch> {
pub fn dispatch_event(&mut self, event: Event, app: &mut MutableAppContext) {
if let Some(root_view_id) = app.root_view_id(self.window_id) {
if matches!(event, Event::MouseMoved { .. }) {
self.last_mouse_moved_event = Some(event.clone());
}
let mut ctx = EventContext {
rendered_views: &mut self.rendered_views,
actions: Vec::new(),
actions: Default::default(),
font_cache: &self.font_cache,
text_layout_cache: &self.text_layout_cache,
view_stack: Vec::new(),
app,
view_stack: Default::default(),
invalidated_views: Default::default(),
app: app.as_ref(),
};
ctx.dispatch_event(root_view_id, &event);
ctx.actions
} else {
Vec::new()
let invalidated_views = ctx.invalidated_views;
let actions = ctx.actions;
for view_id in invalidated_views {
app.notify_view(self.window_id, view_id);
}
for action in actions {
app.dispatch_action_any(
self.window_id,
&action.path,
action.name,
action.arg.as_ref(),
);
}
}
}
@@ -214,6 +241,7 @@ pub struct EventContext<'a> {
pub text_layout_cache: &'a TextLayoutCache,
pub app: &'a AppContext,
view_stack: Vec<usize>,
invalidated_views: HashSet<usize>,
}
impl<'a> EventContext<'a> {
@@ -236,6 +264,11 @@ impl<'a> EventContext<'a> {
arg: Box::new(arg),
});
}
pub fn notify(&mut self) {
self.invalidated_views
.insert(*self.view_stack.last().unwrap());
}
}
pub struct DebugContext<'a> {
+3 -1
View File
@@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde_json::json;
use crate::{
@@ -51,7 +53,7 @@ pub struct Glyph {
pub struct Icon {
pub bounds: RectF,
pub svg: usvg::Tree,
pub path: String,
pub path: Cow<'static, str>,
pub color: ColorU,
}
View File
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.6568 6.34314L12 12M12 12L6.34314 17.6568M12 12L17.6568 17.6568M12 12L6.34314 6.34314" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 236 B

+1
View File
@@ -474,6 +474,7 @@ impl Element for BufferElement {
precise,
} => self.scroll(*position, *delta, *precise, layout, paint, ctx),
Event::KeyDown { chars, .. } => self.key_down(chars, ctx),
_ => false,
}
} else {
false
+1 -1
View File
@@ -189,7 +189,7 @@ impl FileFinder {
LineBox::new(
settings.ui_font_family,
settings.ui_font_size,
Svg::new("icons/file-16.svg".into()).boxed(),
Svg::new("icons/file-16.svg").boxed(),
)
.boxed(),
)
+142 -95
View File
@@ -1,7 +1,7 @@
use super::{ItemViewHandle, SplitDirection};
use crate::{settings::Settings, watch};
use gpui::{
color::{ColorF, ColorU},
color::ColorU,
elements::*,
geometry::{rect::RectF, vector::vec2f},
keymap::Binding,
@@ -25,6 +25,12 @@ pub fn init(app: &mut MutableAppContext) {
app.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), ctx| {
pane.close_active_item(ctx);
});
app.add_action(
"pane:close_item",
|pane: &mut Pane, item_id: &usize, ctx| {
pane.close_item(*item_id, ctx);
},
);
app.add_action("pane:split_up", |pane: &mut Pane, _: &(), ctx| {
pane.split(SplitDirection::Up, ctx);
});
@@ -155,15 +161,17 @@ impl Pane {
pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
if !self.items.is_empty() {
self.items.remove(self.active_item);
if self.active_item >= self.items.len() {
self.active_item = self.items.len().saturating_sub(1);
}
ctx.notify();
self.close_item(self.items[self.active_item].id(), ctx)
}
}
pub fn close_item(&mut self, item_id: usize, ctx: &mut ViewContext<Self>) {
self.items.retain(|item| item.id() != item_id);
self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
if self.items.is_empty() {
ctx.emit(Event::Remove);
}
ctx.notify();
}
fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
@@ -176,67 +184,80 @@ impl Pane {
ctx.emit(Event::Split(direction));
}
fn render_tabs(&self, app: &AppContext) -> ElementBox {
fn render_tabs(&self, ctx: &AppContext) -> ElementBox {
let settings = smol::block_on(self.settings.read());
let border_color = ColorU::from_u32(0xdbdbdcff);
let line_height = ctx.font_cache().line_height(
ctx.font_cache().default_font(settings.ui_font_family),
settings.ui_font_size,
);
let mut row = Flex::row();
let last_item_ix = self.items.len() - 1;
for (ix, item) in self.items.iter().enumerate() {
let title = item.title(app);
let mut border = Border::new(1.0, border_color);
border.left = ix > 0;
border.right = ix == last_item_ix;
border.bottom = ix != self.active_item;
let padding = 6.;
let mut container = Container::new(
Stack::new()
.with_child(
Align::new(
Label::new(title, settings.ui_font_family, settings.ui_font_size)
.boxed(),
)
.boxed(),
)
.with_child(
LineBox::new(
settings.ui_font_family,
settings.ui_font_size,
Align::new(Self::render_modified_icon(item.is_dirty(app)))
.right()
.boxed(),
)
.boxed(),
)
.boxed(),
)
.with_vertical_padding(padding)
.with_horizontal_padding(10.)
.with_border(border);
if ix == self.active_item {
container = container
.with_background_color(ColorU::white())
.with_padding_bottom(padding + border.width);
} else {
container = container.with_background_color(ColorU::from_u32(0xeaeaebff));
}
enum Tab {}
row.add_child(
Expanded::new(
1.0,
ConstrainedBox::new(
EventHandler::new(container.boxed())
.on_mouse_down(move |ctx| {
ctx.dispatch_action("pane:activate_item", ix);
true
})
.boxed(),
)
.with_min_width(80.0)
.with_max_width(264.0)
MouseEventHandler::new::<Tab, _>(item.id(), ctx, |mouse_state| {
let title = item.title(ctx);
let mut border = Border::new(1.0, border_color);
border.left = ix > 0;
border.right = ix == last_item_ix;
border.bottom = ix != self.active_item;
let mut container = Container::new(
Stack::new()
.with_child(
Align::new(
Label::new(
title,
settings.ui_font_family,
settings.ui_font_size,
)
.boxed(),
)
.boxed(),
)
.with_child(
Align::new(Self::render_tab_icon(
item.id(),
line_height - 2.,
mouse_state.hovered,
item.is_dirty(ctx),
ctx,
))
.right()
.boxed(),
)
.boxed(),
)
.with_horizontal_padding(10.)
.with_border(border);
if ix == self.active_item {
container = container
.with_background_color(ColorU::white())
.with_padding_bottom(border.width);
} else {
container =
container.with_background_color(ColorU::from_u32(0xeaeaebff));
}
ConstrainedBox::new(
EventHandler::new(container.boxed())
.on_mouse_down(move |ctx| {
ctx.dispatch_action("pane:activate_item", ix);
true
})
.boxed(),
)
.with_min_width(80.0)
.with_max_width(264.0)
.boxed()
})
.boxed(),
)
.named("tab"),
@@ -247,17 +268,9 @@ impl Pane {
// so that the tab's border doesn't abut the window's border.
row.add_child(
ConstrainedBox::new(
Container::new(
LineBox::new(
settings.ui_font_family,
settings.ui_font_size,
Empty::new().boxed(),
)
Container::new(Empty::new().boxed())
.with_border(Border::bottom(1.0, border_color))
.boxed(),
)
.with_uniform_padding(6.0)
.with_border(Border::bottom(1.0, border_color))
.boxed(),
)
.with_min_width(20.)
.named("fixed-filler"),
@@ -266,43 +279,77 @@ impl Pane {
row.add_child(
Expanded::new(
0.0,
Container::new(
LineBox::new(
settings.ui_font_family,
settings.ui_font_size,
Empty::new().boxed(),
)
Container::new(Empty::new().boxed())
.with_border(Border::bottom(1.0, border_color))
.boxed(),
)
.with_uniform_padding(6.0)
.with_border(Border::bottom(1.0, border_color))
.boxed(),
)
.named("filler"),
);
row.named("tabs")
ConstrainedBox::new(row.boxed())
.with_height(line_height + 16.)
.named("tabs")
}
fn render_modified_icon(is_modified: bool) -> ElementBox {
let diameter = 8.;
ConstrainedBox::new(
Canvas::new(move |bounds, ctx| {
if is_modified {
let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
ctx.scene.push_quad(Quad {
bounds: square,
background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
border: Default::default(),
corner_radius: diameter / 2.,
});
fn render_tab_icon(
item_id: usize,
close_icon_size: f32,
tab_hovered: bool,
is_modified: bool,
ctx: &AppContext,
) -> ElementBox {
enum TabCloseButton {}
let modified_color = ColorU::from_u32(0x556de8ff);
let mut clicked_color = modified_color;
clicked_color.a = 180;
let icon = if tab_hovered {
let mut icon = Svg::new("icons/x.svg");
MouseEventHandler::new::<TabCloseButton, _>(item_id, ctx, |mouse_state| {
if mouse_state.hovered {
Container::new(icon.with_color(ColorU::white()).boxed())
.with_background_color(if mouse_state.clicked {
clicked_color
} else {
modified_color
})
.with_corner_radius(close_icon_size / 2.)
.boxed()
} else {
if is_modified {
icon = icon.with_color(modified_color);
}
icon.boxed()
}
})
.boxed(),
)
.with_width(diameter)
.with_height(diameter)
.named("tab-right-icon")
.on_click(move |ctx| ctx.dispatch_action("pane:close_item", item_id))
.named("close-tab-icon")
} else {
let diameter = 8.;
ConstrainedBox::new(
Canvas::new(move |bounds, ctx| {
if is_modified {
let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
ctx.scene.push_quad(Quad {
bounds: square,
background: Some(modified_color),
border: Default::default(),
corner_radius: diameter / 2.,
});
}
})
.boxed(),
)
.with_width(diameter)
.with_height(diameter)
.named("unsaved-tab-icon")
};
ConstrainedBox::new(Align::new(icon).boxed())
.with_width(close_icon_size)
.named("tab-icon")
}
}