Detailed changes
@@ -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)
}
}
@@ -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::*;
@@ -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),
+ })
+ }
+}
@@ -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(),
}
}
@@ -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,
+ },
}
@@ -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,
}
}
@@ -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,
@@ -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;
@@ -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,
@@ -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
@@ -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),
@@ -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> {
@@ -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,
}
@@ -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>
@@ -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
@@ -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(),
)
@@ -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")
}
}