Merge pull request #28 from zed-industries/close-tabs
Add tab close buttons
This commit is contained in:
+134
-55
@@ -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),
|
||||
|
||||
+41
-8
@@ -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
@@ -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>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -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(),
|
||||
)
|
||||
|
||||
+142
-95
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user