From b21c25826dc84dc68eea21033a9775d93412b48c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 15:51:33 +0100 Subject: [PATCH 01/17] Make tab bar visible --- crates/editor2/src/items.rs | 5 ++--- crates/workspace2/src/pane.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index c439adcc440f75a559240810db512bd9f9a07874..84263d4ed75042ed398189d16062483162ef71e0 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -578,12 +578,12 @@ impl Item for Editor { fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement { let theme = cx.theme(); + AnyElement::new( div() .flex() .flex_row() .items_center() - .bg(gpui::white()) .text_color(gpui::white()) .child(self.title(cx).to_string()) .children(detail.and_then(|detail| { @@ -625,8 +625,7 @@ impl Item for Editor { fn deactivated(&mut self, cx: &mut ViewContext) { let selection = self.selections.newest_anchor(); - todo!() - // self.push_to_nav_history(selection.head(), None, cx); + self.push_to_nav_history(selection.head(), None, cx); } fn workspace_deactivated(&mut self, cx: &mut ViewContext) { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index acc41deba880ed986e1391d6fb61d4818ffeef5a..c56defed2667c66d56fe7fdabcd8df6f4a84b2a9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1457,7 +1457,7 @@ impl Pane { ), ) .child( - div().w_0().flex_1().h_full().child( + div().flex_1().h_full().child( div().id("tabs").flex().overflow_x_scroll().children( self.items .iter() From 0edcec7c4eee800ebb93d24884d51678cd5ed301 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 09:12:12 -0600 Subject: [PATCH 02/17] Fix tab text colors --- crates/editor2/src/items.rs | 10 ++++++++-- crates/workspace2/src/pane.rs | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 84263d4ed75042ed398189d16062483162ef71e0..a4d34ad36f218af6440ac8183d47c4afca1547df 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -584,12 +584,18 @@ impl Item for Editor { .flex() .flex_row() .items_center() - .text_color(gpui::white()) + .gap_2() .child(self.title(cx).to_string()) .children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); - Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) + + Some( + div() + .text_color(theme.colors().text_muted) + .text_xs() + .child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)), + ) })), ) } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index c56defed2667c66d56fe7fdabcd8df6f4a84b2a9..eef829afa71596bc0b3c0903ec372af47ab9644f 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1361,13 +1361,15 @@ impl Pane { let label = item.tab_content(Some(detail), cx); let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); - let (tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { + let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { false => ( + cx.theme().colors().text_muted, cx.theme().colors().tab_inactive, cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_active, ), true => ( + cx.theme().colors().text, cx.theme().colors().tab_active, cx.theme().colors().element_hover, cx.theme().colors().element_active, @@ -1397,6 +1399,7 @@ impl Pane { .flex() .items_center() .gap_1p5() + .text_color(text_color) .children(if item.has_conflict(cx) { Some( IconElement::new(Icon::ExclamationTriangle) From b5c2cf371a5c405d122b9a85da8bace4bb7e7dab Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 16:13:23 +0100 Subject: [PATCH 03/17] Ensure panes cover the available space --- crates/workspace2/src/pane.rs | 7 ++++--- crates/workspace2/src/pane_group.rs | 2 +- crates/workspace2/src/status_bar.rs | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index eef829afa71596bc0b3c0903ec372af47ab9644f..1bccaaea6410f46ae1f77a24a4c951ac1abfdc30 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1891,13 +1891,14 @@ impl Render for Pane { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() + .size_full() .child(self.render_tab_bar(cx)) - .child(div() /* toolbar */) + .child(div() /* todo!(toolbar) */) .child(if let Some(item) = self.active_item() { - item.to_any().render() + div().flex_1().child(item.to_any()) } else { // todo!() - div().child("Empty Pane").render() + div().child("Empty Pane") }) // enum MouseNavigationHandler {} diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index 441aef21f53308924da76d58bea363a202b2ecfa..a9e95b8c29acf604cb7c44e366d6e357257b715e 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -201,7 +201,7 @@ impl Member { // Some(pane) // }; - div().child(pane.clone()).render() + div().size_full().child(pane.clone()).render() // Stack::new() // .with_child(pane_element.contained().with_border(leader_border)) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index ca4ebcdb1305bf72b6d04d1409d4c80358ecbab0..4f7ba02c2ae4ae2fbe370941917e4525d7dc0ebb 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -44,6 +44,7 @@ impl Render for StatusBar { .items_center() .justify_between() .w_full() + .h_8() .bg(cx.theme().colors().status_bar) .child(self.render_left_tools(cx)) .child(self.render_right_tools(cx)) From 920ea1bf0a64a679dfebe4cdc611a1b8cb3c008a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 09:20:13 -0600 Subject: [PATCH 04/17] Make the close button close --- crates/workspace2/src/pane.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 1bccaaea6410f46ae1f77a24a4c951ac1abfdc30..4c4f26fe0eeb4a8eb53af571cbf2c146edefd2c5 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1359,7 +1359,16 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl Component { let label = item.tab_content(Some(detail), cx); - let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); + let close_icon = || { + let id = item.id(); + div() + .id(item.id()) + .child(IconElement::new(Icon::Close).color(IconColor::Muted)) + .on_click(move |pane: &mut Self, _, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + }) + }; let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { false => ( From 580694dbda97cb851e9084290a011cd860ca362f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 09:56:21 -0600 Subject: [PATCH 05/17] Fix bug when unsubscribe called after remove Co-Authored-By: Julia --- crates/gpui2/src/subscription.rs | 63 ++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/crates/gpui2/src/subscription.rs b/crates/gpui2/src/subscription.rs index 64fcd74dd2a1ab47d4d997d8de52f98fe719c850..744e83bbbddda1a6e6264fa62f81fb23c30f6830 100644 --- a/crates/gpui2/src/subscription.rs +++ b/crates/gpui2/src/subscription.rs @@ -14,7 +14,7 @@ impl Clone for SubscriberSet { } struct SubscriberSetState { - subscribers: BTreeMap>, + subscribers: BTreeMap>>, dropped_subscribers: BTreeSet<(EmitterKey, usize)>, next_subscriber_id: usize, } @@ -38,12 +38,18 @@ where lock.subscribers .entry(emitter_key.clone()) .or_default() + .insert(Default::default()) .insert(subscriber_id, callback); let this = self.0.clone(); Subscription { unsubscribe: Some(Box::new(move || { let mut lock = this.lock(); - if let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) { + let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else { + // remove was called with this emitter_key + return; + }; + + if let Some(subscribers) = subscribers { subscribers.remove(&subscriber_id); if subscribers.is_empty() { lock.subscribers.remove(&emitter_key); @@ -62,34 +68,43 @@ where pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator { let subscribers = self.0.lock().subscribers.remove(&emitter); - subscribers.unwrap_or_default().into_values() + subscribers + .unwrap_or_default() + .map(|s| s.into_values()) + .into_iter() + .flatten() } pub fn retain(&self, emitter: &EmitterKey, mut f: F) where F: FnMut(&mut Callback) -> bool, { - let entry = self.0.lock().subscribers.remove_entry(emitter); - if let Some((emitter, mut subscribers)) = entry { - subscribers.retain(|_, callback| f(callback)); - let mut lock = self.0.lock(); - - // Add any new subscribers that were added while invoking the callback. - if let Some(new_subscribers) = lock.subscribers.remove(&emitter) { - subscribers.extend(new_subscribers); - } - - // Remove any dropped subscriptions that were dropped while invoking the callback. - for (dropped_emitter, dropped_subscription_id) in - mem::take(&mut lock.dropped_subscribers) - { - debug_assert_eq!(emitter, dropped_emitter); - subscribers.remove(&dropped_subscription_id); - } - - if !subscribers.is_empty() { - lock.subscribers.insert(emitter, subscribers); - } + let Some(mut subscribers) = self + .0 + .lock() + .subscribers + .get_mut(emitter) + .and_then(|s| s.take()) + else { + return; + }; + + subscribers.retain(|_, callback| f(callback)); + let mut lock = self.0.lock(); + + // Add any new subscribers that were added while invoking the callback. + if let Some(Some(new_subscribers)) = lock.subscribers.remove(&emitter) { + subscribers.extend(new_subscribers); + } + + // Remove any dropped subscriptions that were dropped while invoking the callback. + for (dropped_emitter, dropped_subscription_id) in mem::take(&mut lock.dropped_subscribers) { + debug_assert_eq!(*emitter, dropped_emitter); + subscribers.remove(&dropped_subscription_id); + } + + if !subscribers.is_empty() { + lock.subscribers.insert(emitter.clone(), Some(subscribers)); } } } From d3b02c4de47c80bd3a7bb544e75c7e347c0f3839 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Nov 2023 17:14:11 +0100 Subject: [PATCH 06/17] WIP: start on editor element --- crates/editor2/src/editor.rs | 92 ++++++------ crates/editor2/src/element.rs | 181 +++++++++++++++++++----- crates/editor2/src/scroll.rs | 28 ++-- crates/editor2/src/scroll/autoscroll.rs | 6 +- crates/gpui2/src/geometry.rs | 6 + 5 files changed, 217 insertions(+), 96 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ea747de5de689e6050581aecd5a618acd79ef9a5..52628f61b58c9ee4411321266c6789b02b98a465 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -37,8 +37,8 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ div, AnyElement, AppContext, BackgroundExecutor, Context, Div, Element, EventEmitter, - FocusHandle, Hsla, Model, Pixels, Render, Subscription, Task, TextStyle, View, ViewContext, - VisualContext, WeakView, WindowContext, + FocusHandle, Hsla, Model, Pixels, Render, Styled, Subscription, Task, TextStyle, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -68,6 +68,7 @@ use scroll::{ use selections_collection::{MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; +use smallvec::SmallVec; use std::{ any::TypeId, borrow::Cow, @@ -8347,51 +8348,51 @@ impl Editor { // .text() // } - // pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { - // let mut wrap_guides = smallvec::smallvec![]; + pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { + let mut wrap_guides = smallvec::smallvec![]; - // if self.show_wrap_guides == Some(false) { - // return wrap_guides; - // } + if self.show_wrap_guides == Some(false) { + return wrap_guides; + } - // let settings = self.buffer.read(cx).settings_at(0, cx); - // if settings.show_wrap_guides { - // if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { - // wrap_guides.push((soft_wrap as usize, true)); - // } - // wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) - // } + let settings = self.buffer.read(cx).settings_at(0, cx); + if settings.show_wrap_guides { + if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { + wrap_guides.push((soft_wrap as usize, true)); + } + wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) + } - // wrap_guides - // } + wrap_guides + } - // pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - // let settings = self.buffer.read(cx).settings_at(0, cx); - // let mode = self - // .soft_wrap_mode_override - // .unwrap_or_else(|| settings.soft_wrap); - // match mode { - // language_settings::SoftWrap::None => SoftWrap::None, - // language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - // language_settings::SoftWrap::PreferredLineLength => { - // SoftWrap::Column(settings.preferred_line_length) - // } - // } - // } + pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { + let settings = self.buffer.read(cx).settings_at(0, cx); + let mode = self + .soft_wrap_mode_override + .unwrap_or_else(|| settings.soft_wrap); + match mode { + language_settings::SoftWrap::None => SoftWrap::None, + language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + language_settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length) + } + } + } - // pub fn set_soft_wrap_mode( - // &mut self, - // mode: language_settings::SoftWrap, - // cx: &mut ViewContext, - // ) { - // self.soft_wrap_mode_override = Some(mode); - // cx.notify(); - // } + pub fn set_soft_wrap_mode( + &mut self, + mode: language_settings::SoftWrap, + cx: &mut ViewContext, + ) { + self.soft_wrap_mode_override = Some(mode); + cx.notify(); + } - // pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { - // self.display_map - // .update(cx, |map, cx| map.set_wrap_width(width, cx)) - // } + pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { + self.display_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } // pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext) { // if self.soft_wrap_mode_override.is_some() { @@ -9321,11 +9322,14 @@ impl EventEmitter for Editor { } impl Render for Editor { - type Element = Div; + type Element = EditorElement; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - // todo!() - div() + EditorElement::new(EditorStyle { + text: cx.text_style(), + line_height_scalar: 1., + theme_id: 0, + }) } } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 645cdc76469e1db52c898036a49f960aa0bfee67..6420d1e6cd6482ac99e79f3de0dfbd3c2963359c 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3,17 +3,18 @@ use super::{ }; use crate::{ display_map::{BlockStyle, DisplaySnapshot}, - EditorStyle, + EditorMode, EditorStyle, SoftWrap, }; use anyhow::Result; use gpui::{ - black, px, relative, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, TextRun, - TextSystem, + black, point, px, relative, size, AnyElement, Bounds, Element, Hsla, Line, Pixels, Size, Style, + TextRun, TextSystem, ViewContext, }; use language::{CursorShape, Selection}; use smallvec::SmallVec; -use std::{ops::Range, sync::Arc}; +use std::{cmp, ops::Range, sync::Arc}; use sum_tree::Bias; +use theme::ActiveTheme; enum FoldMarkers {} @@ -1321,29 +1322,31 @@ impl EditorElement { // } // } - // fn column_pixels(&self, column: usize, cx: &ViewContext) -> f32 { - // let style = &self.style; - - // cx.text_layout_cache() - // .layout_str( - // " ".repeat(column).as_str(), - // style.text.font_size, - // &[( - // column, - // RunStyle { - // font_id: style.text.font_id, - // color: Color::black(), - // underline: Default::default(), - // }, - // )], - // ) - // .width() - // } + fn column_pixels(&self, column: usize, cx: &ViewContext) -> Pixels { + let style = &self.style; + let font_size = style.text.font_size * cx.rem_size(); + let layout = cx + .text_system() + .layout_text( + " ".repeat(column).as_str(), + font_size, + &[TextRun { + len: column, + font: style.text.font(), + color: Hsla::default(), + underline: None, + }], + None, + ) + .unwrap(); + + layout[0].width + } - // fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> f32 { - // let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; - // self.column_pixels(digit_count, cx) - // } + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> Pixels { + let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; + self.column_pixels(digit_count, cx) + } //Folds contained in a hunk are ignored apart from shrinking visual size //If a fold contains any hunks then that fold line is marked as modified @@ -2002,6 +2005,7 @@ impl Element for EditorElement { element_state: &mut Self::ElementState, cx: &mut gpui::ViewContext, ) -> gpui::LayoutId { + let rem_size = cx.rem_size(); let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); @@ -2011,18 +2015,125 @@ impl Element for EditorElement { fn paint( &mut self, bounds: Bounds, - view_state: &mut Editor, + editor: &mut Editor, element_state: &mut Self::ElementState, cx: &mut gpui::ViewContext, ) { - let text_style = cx.text_style(); - - let layout_text = cx.text_system().layout_text( - "hello world", - text_style.font_size * cx.rem_size(), - &[text_style.to_run("hello world".len())], - None, - ); + // let mut size = constraint.max; + // if size.x().is_infinite() { + // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); + // } + + let snapshot = editor.snapshot(cx); + let style = self.style.clone(); + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size * cx.rem_size(); + let line_height = (font_size * style.line_height_scalar).round(); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + let em_advance = cx + .text_system() + .advance(font_id, font_size, 'm') + .unwrap() + .width; + + let gutter_padding; + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let descent = cx.text_system().descent(font_id, font_size).unwrap(); + + let gutter_padding_factor = 3.5; + gutter_padding = (em_width * gutter_padding_factor).round(); + gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; + gutter_margin = -descent; + } else { + gutter_padding = px(0.0); + gutter_width = px(0.0); + gutter_margin = px(0.0); + }; + + let text_width = bounds.size.width - gutter_width; + let overscroll = point(em_width, px(0.)); + let snapshot = { + editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); + + let editor_width = text_width - gutter_margin - overscroll.x - em_width; + let wrap_width = match editor.soft_wrap_mode(cx) { + SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, + SoftWrap::EditorWidth => editor_width, + SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), + }; + + if editor.set_wrap_width(Some(wrap_width), cx) { + editor.snapshot(cx) + } else { + snapshot + } + }; + + let wrap_guides = editor + .wrap_guides(cx) + .iter() + .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) + .collect::>(); + + let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; + // todo!("this should happen during layout") + if let EditorMode::AutoHeight { max_lines } = snapshot.mode { + todo!() + // size.set_y( + // scroll_height + // .min(constraint.max_along(Axis::Vertical)) + // .max(constraint.min_along(Axis::Vertical)) + // .max(line_height) + // .min(line_height * max_lines as f32), + // ) + } else if let EditorMode::SingleLine = snapshot.mode { + todo!() + // size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) + } + // todo!() + // else if size.y().is_infinite() { + // // size.set_y(scroll_height); + // } + // + let gutter_size = size(gutter_width, bounds.size.height); + let text_size = size(text_width, bounds.size.height); + + let autoscroll_horizontally = + editor.autoscroll_vertically(bounds.size.height, line_height, cx); + let mut snapshot = editor.snapshot(cx); + + let scroll_position = snapshot.scroll_position(); + // The scroll position is a fractional point, the whole number of which represents + // the top of the window in terms of display rows. + let start_row = scroll_position.y as u32; + let height_in_lines = f32::from(bounds.size.height / line_height); + let max_row = snapshot.max_point().row(); + + // Add 1 to ensure selections bleed off screen + let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); + + dbg!(start_row..end_row); + // let text_style = cx.text_style(); + // let layout_text = cx.text_system().layout_text( + // "hello world", + // text_style.font_size * cx.rem_size(), + // &[text_style.to_run("hello world".len())], + // None, + // ); + // let line_height = text_style + // .line_height + // .to_pixels(text_style.font_size.into(), cx.rem_size()); + + // layout_text.unwrap()[0] + // .paint(bounds.origin, line_height, cx) + // .unwrap(); } } diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 5e4b32265a221cb2561134776fac7bb3fc266931..1876952ae2fa773dcd03277cfdff85d3ffd0d9e9 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -303,20 +303,20 @@ impl Editor { self.scroll_manager.visible_line_count } - // pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext) { - // let opened_first_time = self.scroll_manager.visible_line_count.is_none(); - // self.scroll_manager.visible_line_count = Some(lines); - // if opened_first_time { - // cx.spawn(|editor, mut cx| async move { - // editor - // .update(&mut cx, |editor, cx| { - // editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx) - // }) - // .ok() - // }) - // .detach() - // } - // } + pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext) { + let opened_first_time = self.scroll_manager.visible_line_count.is_none(); + self.scroll_manager.visible_line_count = Some(lines); + if opened_first_time { + cx.spawn(|editor, mut cx| async move { + editor + .update(&mut cx, |editor, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx) + }) + .ok() + }) + .detach() + } + } pub fn set_scroll_position( &mut self, diff --git a/crates/editor2/src/scroll/autoscroll.rs b/crates/editor2/src/scroll/autoscroll.rs index a4c37a258e982b6099611ac548f4802b3437b897..9315d5c0997fd649e8c0f16cc5425cca1803a8f4 100644 --- a/crates/editor2/src/scroll/autoscroll.rs +++ b/crates/editor2/src/scroll/autoscroll.rs @@ -48,11 +48,11 @@ impl AutoscrollStrategy { impl Editor { pub fn autoscroll_vertically( &mut self, - viewport_height: f32, - line_height: f32, + viewport_height: Pixels, + line_height: Pixels, cx: &mut ViewContext, ) -> bool { - let visible_lines = viewport_height / line_height; + let visible_lines = f32::from(viewport_height / line_height); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index b2fad4efda9e127ce74c319ac2471cf24798a247..081b11aae0e250237e091bf8ab7ecc565e57c960 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -825,6 +825,12 @@ impl From for u32 { } } +impl From for Pixels { + fn from(pixels: u32) -> Self { + Pixels(pixels as f32) + } +} + impl From for usize { fn from(pixels: Pixels) -> Self { pixels.0 as usize From c604a2e34e8b26dd3780de0a250b0e5bf8bbba7b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 10:54:26 -0600 Subject: [PATCH 07/17] Add hover behaviour to tabs Co-Authored-By: Marshall Co-Authored-By: Nathan --- crates/gpui2/src/element.rs | 13 +++++++++++++ crates/gpui2/src/elements/div.rs | 8 ++++++-- crates/gpui2/src/style.rs | 11 +++++++++++ crates/gpui2/src/styled.rs | 21 +++++++++++++++++++++ crates/ui2/src/elements/icon.rs | 13 +++++++++++-- crates/workspace2/src/pane.rs | 10 +++++++++- 6 files changed, 71 insertions(+), 5 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index a92dbd6ff94a98165f5b200bc4eadedb2471bf5a..2a0f557272f4899eb21f2bbb3123a08045a56158 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -212,6 +212,19 @@ pub trait Component { { self.map(|this| if condition { then(this) } else { this }) } + + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } } impl Component for AnyElement { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 56940efce4a0982c4255b613f682137ed3ce1582..bf28ec3bf5ffffe1b6bd3db9d0fe306c6ce7b7fb 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -3,7 +3,7 @@ use crate::{ ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement, Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, - StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, + StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, Visibility, }; use refineable::Refineable; use smallvec::SmallVec; @@ -249,11 +249,15 @@ where cx: &mut ViewContext, ) { self.with_element_id(cx, |this, _global_id, cx| { + let style = this.compute_style(bounds, element_state, cx); + if style.visibility == Visibility::Hidden { + return; + } + if let Some(group) = this.group.clone() { GroupBounds::push(group, bounds, cx); } - let style = this.compute_style(bounds, element_state, cx); let z_index = style.z_index.unwrap_or(0); let mut child_min = point(Pixels::MAX, Pixels::MAX); diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index b30d4aa00250f981f48301f6e3c8007b55b1bb36..b3cd5c478b3ba111816ec8c44afd5e63b8f9b1a0 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -19,6 +19,9 @@ pub struct Style { /// What layout strategy should be used? pub display: Display, + /// Should the element be painted on screen? + pub visibility: Visibility, + // Overflow properties /// How children overflowing their container should affect layout #[refineable] @@ -107,6 +110,13 @@ impl Styled for StyleRefinement { } } +#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] +pub enum Visibility { + #[default] + Visible, + Hidden, +} + #[derive(Clone, Debug)] pub struct BoxShadow { pub color: Hsla, @@ -297,6 +307,7 @@ impl Default for Style { fn default() -> Self { Style { display: Display::Block, + visibility: Visibility::Visible, overflow: Point { x: Overflow::Visible, y: Overflow::Visible, diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index 1eed74f0969a437ae3827815345b10962091f6fb..a88a8b4d6f85c63cb49e9a762196f4205c24a5c8 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -1,6 +1,7 @@ use crate::{ self as gpui2, hsla, point, px, relative, rems, AlignItems, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, StyleRefinement, + Visibility, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::smallvec; @@ -60,6 +61,26 @@ pub trait Styled { self } + /// Sets the visibility of the element to `visible`. + /// [Docs](https://tailwindcss.com/docs/visibility) + fn visible(mut self) -> Self + where + Self: Sized, + { + self.style().visibility = Some(Visibility::Visible); + self + } + + /// Sets the visibility of the element to `hidden`. + /// [Docs](https://tailwindcss.com/docs/visibility) + fn invisible(mut self) -> Self + where + Self: Sized, + { + self.style().visibility = Some(Visibility::Hidden); + self + } + /// Sets the flex direction of the element to `column`. /// [Docs](https://tailwindcss.com/docs/flex-direction#column) fn flex_col(mut self) -> Self diff --git a/crates/ui2/src/elements/icon.rs b/crates/ui2/src/elements/icon.rs index 5885d76101f18ce332e418423afb0f2d1f3d0da8..94e15ca0f1187dcc8851619fa7d94f0b295300c8 100644 --- a/crates/ui2/src/elements/icon.rs +++ b/crates/ui2/src/elements/icon.rs @@ -159,6 +159,7 @@ impl Icon { pub struct IconElement { icon: Icon, color: IconColor, + hover_color: Option, size: IconSize, } @@ -167,6 +168,7 @@ impl IconElement { Self { icon, color: IconColor::default(), + hover_color: None, size: IconSize::default(), } } @@ -176,13 +178,17 @@ impl IconElement { self } + pub fn hover_color(mut self, hover_color: impl Into>) -> Self { + self.hover_color = hover_color.into(); + self + } + pub fn size(mut self, size: IconSize) -> Self { self.size = size; self } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let fill = self.color.color(cx); let svg_size = match self.size { IconSize::Small => rems(0.75), IconSize::Medium => rems(0.9375), @@ -192,7 +198,10 @@ impl IconElement { .size(svg_size) .flex_none() .path(self.icon.path()) - .text_color(fill) + .text_color(self.color.color(cx)) + .when_some(self.hover_color, |this, hover_color| { + this.hover(|style| style.text_color(hover_color.color(cx))) + }) } } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 4c4f26fe0eeb4a8eb53af571cbf2c146edefd2c5..f8e3c02178963204ca004cc2ef79405e11cc84f6 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1361,9 +1361,16 @@ impl Pane { let label = item.tab_content(Some(detail), cx); let close_icon = || { let id = item.id(); + div() .id(item.id()) - .child(IconElement::new(Icon::Close).color(IconColor::Muted)) + .invisible() + .group_hover("", |style| style.visible()) + .child( + IconElement::new(Icon::Close) + .color(IconColor::Muted) + .hover_color(IconColor::Accent), + ) .on_click(move |pane: &mut Self, _, cx| { pane.close_item_by_id(id, SaveIntent::Close, cx) .detach_and_log_err(cx); @@ -1388,6 +1395,7 @@ impl Pane { let close_right = ItemSettings::get_global(cx).close_position.right(); div() + .group("") .id(item.id()) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) From d73c54f6049aeef161e6673eeabeedea2918b830 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 11:30:15 -0600 Subject: [PATCH 08/17] Add PointingHand on tabs --- crates/gpui2/src/elements/div.rs | 7 +++++++ crates/gpui2/src/style.rs | 10 +++++++--- crates/gpui2/src/styled.rs | 34 +++++++++++++++++++++++++++++--- crates/gpui2/src/window.rs | 31 ++++++++++++++++++++--------- crates/workspace2/src/pane.rs | 6 ++++-- crates/zed2/src/main.rs | 1 - 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index bf28ec3bf5ffffe1b6bd3db9d0fe306c6ce7b7fb..e011041bae2259f8a290ef098b061740721f4307 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -254,6 +254,13 @@ where return; } + if let Some(mouse_cursor) = style.mouse_cursor { + let hovered = bounds.contains_point(&cx.mouse_position()); + if hovered { + cx.set_cursor_style(mouse_cursor); + } + } + if let Some(group) = this.group.clone() { GroupBounds::push(group, bounds, cx); } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index b3cd5c478b3ba111816ec8c44afd5e63b8f9b1a0..d2571a3253522cbfed12ffce1d130d762ac59a5a 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,8 +1,8 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, - Corners, CornersRefinement, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, - FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rems, Result, Rgba, - SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, + Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, + FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rems, + Result, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; @@ -101,6 +101,9 @@ pub struct Style { /// TEXT pub text: TextStyleRefinement, + /// The mouse cursor style shown when the mouse pointer is over an element. + pub mouse_cursor: Option, + pub z_index: Option, } @@ -339,6 +342,7 @@ impl Default for Style { corner_radii: Corners::default(), box_shadow: Default::default(), text: TextStyleRefinement::default(), + mouse_cursor: None, z_index: None, } } diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index a88a8b4d6f85c63cb49e9a762196f4205c24a5c8..4a9a5d7ecf0c1fc18791dd5be596f4bfc488c7b4 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -1,7 +1,7 @@ use crate::{ - self as gpui2, hsla, point, px, relative, rems, AlignItems, DefiniteLength, Display, Fill, - FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, StyleRefinement, - Visibility, + self as gpui2, hsla, point, px, relative, rems, AlignItems, CursorStyle, DefiniteLength, + Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, + StyleRefinement, Visibility, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::smallvec; @@ -81,6 +81,34 @@ pub trait Styled { self } + fn cursor(mut self, cursor: CursorStyle) -> Self + where + Self: Sized, + { + self.style().mouse_cursor = Some(cursor); + self + } + + /// Sets the cursor style when hovering an element to `default`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_default(mut self) -> Self + where + Self: Sized, + { + self.style().mouse_cursor = Some(CursorStyle::Arrow); + self + } + + /// Sets the cursor style when hovering an element to `pointer`. + /// [Docs](https://tailwindcss.com/docs/cursor) + fn cursor_pointer(mut self) -> Self + where + Self: Sized, + { + self.style().mouse_cursor = Some(CursorStyle::PointingHand); + self + } + /// Sets the flex direction of the element to `column`. /// [Docs](https://tailwindcss.com/docs/flex-direction#column) fn flex_col(mut self) -> Self diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 2284f3ccc2269c21fe55a32b3ad3d1f4b1b3ca4a..e998766bbb8126ac62b99ac2611caec72a941dc6 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,14 +1,14 @@ use crate::{ px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, - Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, - Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, - Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, - PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, - Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, - WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId, + Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, + GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, + KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, + PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, + RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, + Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, + VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -190,6 +190,7 @@ pub struct Window { pub(crate) focus_handles: Arc>>, default_prevented: bool, mouse_position: Point, + requested_cursor_style: Option, scale_factor: f32, bounds: WindowBounds, bounds_observers: SubscriberSet<(), AnyObserver>, @@ -283,6 +284,7 @@ impl Window { focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), default_prevented: true, mouse_position, + requested_cursor_style: None, scale_factor, bounds, bounds_observers: SubscriberSet::new(), @@ -669,6 +671,10 @@ impl<'a> WindowContext<'a> { self.window.mouse_position } + pub fn set_cursor_style(&mut self, style: CursorStyle) { + self.window.requested_cursor_style = Some(style) + } + /// Called during painting to invoke the given closure in a new stacking context. The given /// z-index is interpreted relative to the previous call to `stack`. pub fn stack(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { @@ -987,6 +993,13 @@ impl<'a> WindowContext<'a> { let scene = self.window.scene_builder.build(); self.window.platform_window.draw(scene); + let cursor_style = self + .window + .requested_cursor_style + .take() + .unwrap_or(CursorStyle::Arrow); + self.platform.set_cursor_style(cursor_style); + self.window.dirty = false; } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index f8e3c02178963204ca004cc2ef79405e11cc84f6..4d5b96ea6d201764f3fd1ee6f09bccf0bc3f1019 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -9,8 +9,9 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model, - PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + AppContext, AsyncWindowContext, Component, CursorStyle, Div, EntityId, EventEmitter, + FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -1397,6 +1398,7 @@ impl Pane { div() .group("") .id(item.id()) + .cursor_pointer() // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) // .on_drop(|_view, state: View, cx| { diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 79ba132e4ffedaa275d1c3cbdb64cf3692970b13..52eb512eb4e4c47850031689f13f336f89488b6b 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -208,7 +208,6 @@ fn main() { if stdout_is_a_pty() { cx.activate(true); let urls = collect_url_args(); - dbg!(&urls); if !urls.is_empty() { listener.open_urls(urls) } From 26e64fb84385bc1d6f3f531a60fc1472b51989d4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 12:51:38 -0600 Subject: [PATCH 09/17] gpui2: Add on_hover events --- crates/gpui2/src/interactive.rs | 60 +++++++++++++++++++++++++++++++++ crates/workspace2/src/pane.rs | 4 +++ 2 files changed, 64 insertions(+) diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 020cb82cd2a26bf7a011b44db7dd78711c386024..7966b68e121c7882e9f32bb219ac07cefeed0f22 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -333,6 +333,37 @@ pub trait StatefulInteractive: StatelessInteractive { })); self } + + fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self + where + Self: Sized, + { + debug_assert!( + self.stateful_interaction().hover_listener.is_none(), + "calling on_hover more than once on the same element is not supported" + ); + self.stateful_interaction().hover_listener = Some(Box::new(listener)); + self + } + + fn tooltip( + mut self, + build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self + where + Self: Sized, + W: 'static + Render, + { + debug_assert!( + self.stateful_interaction().tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.stateful_interaction().tooltip_builder = Some(Box::new(move |view_state, cx| { + build_tooltip(view_state, cx).into() + })); + + self + } } pub trait ElementInteraction: 'static { @@ -568,6 +599,24 @@ pub trait ElementInteraction: 'static { } } + if let Some(hover_listener) = stateful.hover_listener.take() { + let was_hovered = element_state.hover_state.clone(); + let has_mouse_down = element_state.pending_mouse_down.lock().is_some(); + cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = bounds.contains_point(&event.position) && !has_mouse_down; + let mut was_hovered = was_hovered.lock(); + + if is_hovered != was_hovered.clone() { + *was_hovered = is_hovered; + drop(was_hovered); + hover_listener(view_state, is_hovered, cx); + } + }); + } + let active_state = element_state.active_state.clone(); if active_state.lock().is_none() { let active_group_bounds = stateful @@ -639,6 +688,8 @@ pub struct StatefulInteraction { active_style: StyleRefinement, group_active_style: Option, drag_listener: Option>, + hover_listener: Option>, + tooltip_builder: Option>, } impl ElementInteraction for StatefulInteraction { @@ -666,6 +717,8 @@ impl From for StatefulInteraction { stateless: StatelessInteraction::default(), click_listeners: SmallVec::new(), drag_listener: None, + hover_listener: None, + tooltip_builder: None, active_style: StyleRefinement::default(), group_active_style: None, } @@ -695,6 +748,8 @@ impl StatelessInteraction { stateless: self, click_listeners: SmallVec::new(), drag_listener: None, + hover_listener: None, + tooltip_builder: None, active_style: StyleRefinement::default(), group_active_style: None, } @@ -746,6 +801,7 @@ impl ActiveState { #[derive(Default)] pub struct InteractiveElementState { active_state: Arc>, + hover_state: Arc>, pending_mouse_down: Arc>>, scroll_offset: Option>>>, } @@ -1097,6 +1153,10 @@ pub type ClickListener = Box) pub(crate) type DragListener = Box, &mut ViewContext) -> AnyDrag + 'static>; +pub(crate) type HoverListener = Box) + 'static>; + +pub(crate) type TooltipBuilder = Box) -> AnyView + 'static>; + pub type KeyListener = Box< dyn Fn( &mut V, diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 4d5b96ea6d201764f3fd1ee6f09bccf0bc3f1019..acda04c5a47cbf147ef1fac3d54c900be7eb5614 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1399,6 +1399,10 @@ impl Pane { .group("") .id(item.id()) .cursor_pointer() + .on_hover(|_, hovered, _| { + dbg!(hovered); + }) + // .tooltip(|pane, cx| cx.create_view( tooltip.child("Hovering the tab")) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) // .on_drop(|_view, state: View, cx| { From 33245d119e24063ed2732d138e58c296cd5518ca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 14:02:46 -0600 Subject: [PATCH 10/17] Tooltip on tabs Co-Authored-By: Julia --- crates/gpui2/src/app.rs | 2 ++ crates/gpui2/src/interactive.rs | 24 +++++++++++++++++-- crates/gpui2/src/window.rs | 8 +++++++ crates/ui2/src/components.rs | 2 ++ crates/ui2/src/components/tooltip.rs | 36 ++++++++++++++++++++++++++++ crates/workspace2/src/pane.rs | 12 ++++++---- 6 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 crates/ui2/src/components/tooltip.rs diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 9ec76a86d4e7bd0726cc1a38f98a942728c7e43d..974395e8971f92a3524a5a8177f792a567b88904 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -157,6 +157,7 @@ pub struct AppContext { flushing_effects: bool, pending_updates: usize, pub(crate) active_drag: Option, + pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: HashMap>, pub(crate) frame_consumers: HashMap>, pub(crate) background_executor: BackgroundExecutor, @@ -215,6 +216,7 @@ impl AppContext { flushing_effects: false, pending_updates: 0, active_drag: None, + active_tooltip: None, next_frame_callbacks: HashMap::default(), frame_consumers: HashMap::default(), background_executor: executor, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 7966b68e121c7882e9f32bb219ac07cefeed0f22..def7315a7cca6fe8cd3c7b9376438218cb226852 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -358,7 +358,7 @@ pub trait StatefulInteractive: StatelessInteractive { self.stateful_interaction().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); - self.stateful_interaction().tooltip_builder = Some(Box::new(move |view_state, cx| { + self.stateful_interaction().tooltip_builder = Some(Arc::new(move |view_state, cx| { build_tooltip(view_state, cx).into() })); @@ -602,6 +602,10 @@ pub trait ElementInteraction: 'static { if let Some(hover_listener) = stateful.hover_listener.take() { let was_hovered = element_state.hover_state.clone(); let has_mouse_down = element_state.pending_mouse_down.lock().is_some(); + + let active_tooltip = element_state.active_tooltip.clone(); + let tooltip_builder = stateful.tooltip_builder.clone(); + cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; @@ -612,11 +616,26 @@ pub trait ElementInteraction: 'static { if is_hovered != was_hovered.clone() { *was_hovered = is_hovered; drop(was_hovered); + if let Some(tooltip_builder) = &tooltip_builder { + let mut active_tooltip = active_tooltip.lock(); + if is_hovered && active_tooltip.is_none() { + *active_tooltip = Some(tooltip_builder(view_state, cx)); + } else if !is_hovered { + active_tooltip.take(); + } + } + hover_listener(view_state, is_hovered, cx); } }); } + if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { + if *element_state.hover_state.lock() { + cx.active_tooltip = Some(active_tooltip.clone()); + } + } + let active_state = element_state.active_state.clone(); if active_state.lock().is_none() { let active_group_bounds = stateful @@ -804,6 +823,7 @@ pub struct InteractiveElementState { hover_state: Arc>, pending_mouse_down: Arc>>, scroll_offset: Option>>>, + active_tooltip: Arc>>, } impl InteractiveElementState { @@ -1155,7 +1175,7 @@ pub(crate) type DragListener = pub(crate) type HoverListener = Box) + 'static>; -pub(crate) type TooltipBuilder = Box) -> AnyView + 'static>; +pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; pub type KeyListener = Box< dyn Fn( diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index e998766bbb8126ac62b99ac2611caec72a941dc6..46ac30592bb8e6e60c134c702acb675e7fea4a39 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -987,6 +987,14 @@ impl<'a> WindowContext<'a> { cx.active_drag = Some(active_drag); }); }); + } else if let Some(active_tooltip) = self.app.active_tooltip.take() { + self.stack(1, |cx| { + cx.with_element_offset(Some(cx.mouse_position()), |cx| { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_tooltip.draw(available_space, cx); + }); + }); } self.window.root_view = Some(root_view); diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 7a9cac01ca7c54999210b3220986062906196e1b..f10f8cee8d74cba0bdc0bfc6e307f147f8e424bc 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -31,6 +31,7 @@ mod theme_selector; mod title_bar; mod toast; mod toolbar; +mod tooltip; mod traffic_lights; mod workspace; @@ -67,5 +68,6 @@ pub use theme_selector::*; pub use title_bar::*; pub use toast::*; pub use toolbar::*; +pub use tooltip::*; pub use traffic_lights::*; pub use workspace::*; diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs new file mode 100644 index 0000000000000000000000000000000000000000..eb53b506ebd8467eb9f6d31dab2abe36dd62496a --- /dev/null +++ b/crates/ui2/src/components/tooltip.rs @@ -0,0 +1,36 @@ +use gpui2::{ + div, px, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, +}; +use theme2::ActiveTheme; + +#[derive(Clone, Debug)] +pub struct TextTooltip { + title: SharedString, +} + +impl TextTooltip { + pub fn new(str: SharedString) -> Self { + Self { title: str } + } + + pub fn build_view(str: SharedString, cx: &mut C) -> C::Result> { + cx.build_view(|cx| TextTooltip::new(str)) + } +} + +impl Render for TextTooltip { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let theme = cx.theme(); + div() + .bg(theme.colors().background) + .rounded(px(8.)) + .border() + .border_color(theme.colors().border) + .text_color(theme.colors().text) + .pl_2() + .pr_2() + .child(self.title.clone()) + } +} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index acda04c5a47cbf147ef1fac3d54c900be7eb5614..a1ec16848840355748666fb7cd0ad99ee789d4f9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -9,9 +9,8 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - AppContext, AsyncWindowContext, Component, CursorStyle, Div, EntityId, EventEmitter, - FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + AnyView, AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, + Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -27,7 +26,7 @@ use std::{ }, }; use ui::v_stack; -use ui::{prelude::*, Icon, IconButton, IconColor, IconElement}; +use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip}; use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] @@ -1402,7 +1401,10 @@ impl Pane { .on_hover(|_, hovered, _| { dbg!(hovered); }) - // .tooltip(|pane, cx| cx.create_view( tooltip.child("Hovering the tab")) + .when_some(item.tab_tooltip_text(cx), |div, text| { + div.tooltip(move |_, cx| TextTooltip::build_view(text.clone(), cx)) + }) + // .tooltip(|pane, cx| cx.build_view(|cx| div().child(title))) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) // .on_drop(|_view, state: View, cx| { From f97046b86fae67d9cbf8ccf4e7b86781f9d2f7a9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 14:15:32 -0600 Subject: [PATCH 11/17] MOAR TOOLTIPS --- crates/gpui2/src/interactive.rs | 51 ++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index def7315a7cca6fe8cd3c7b9376438218cb226852..4a6185f7370239ef7c62d241df1d35e7aa7061b3 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -17,6 +17,7 @@ use std::{ ops::Deref, path::PathBuf, sync::Arc, + time::{Duration, Instant}, }; const DRAG_THRESHOLD: f64 = 2.; @@ -603,9 +604,6 @@ pub trait ElementInteraction: 'static { let was_hovered = element_state.hover_state.clone(); let has_mouse_down = element_state.pending_mouse_down.lock().is_some(); - let active_tooltip = element_state.active_tooltip.clone(); - let tooltip_builder = stateful.tooltip_builder.clone(); - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; @@ -616,23 +614,39 @@ pub trait ElementInteraction: 'static { if is_hovered != was_hovered.clone() { *was_hovered = is_hovered; drop(was_hovered); - if let Some(tooltip_builder) = &tooltip_builder { - let mut active_tooltip = active_tooltip.lock(); - if is_hovered && active_tooltip.is_none() { - *active_tooltip = Some(tooltip_builder(view_state, cx)); - } else if !is_hovered { - active_tooltip.take(); - } - } hover_listener(view_state, is_hovered, cx); } }); } - if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { - if *element_state.hover_state.lock() { - cx.active_tooltip = Some(active_tooltip.clone()); + // if we're hovered: + // if no timer, start timer + // if timer hits 1s, call tooltip_builder() + // + + if let Some(tooltip_builder) = &stateful.tooltip_builder { + let mut active_tooltip = element_state.active_tooltip.lock(); + let is_hovered = bounds.contains_point(&cx.mouse_position()) + && !element_state.pending_mouse_down.lock().is_some(); + + if is_hovered { + if let Some(active_tooltip) = active_tooltip { + active_tooltip.view = Some(tooltip_builder(cx)) + } else { + *active_tooltip = Some(ActiveTooltip { + hover_start: Instant::now(), + view: None, + }); + } + } else { + active_tooltip.take(); + } + + if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { + if *element_state.hover_state.lock() { + cx.active_tooltip = Some(active_tooltip.clone()); + } } } @@ -823,7 +837,12 @@ pub struct InteractiveElementState { hover_state: Arc>, pending_mouse_down: Arc>>, scroll_offset: Option>>>, - active_tooltip: Arc>>, + active_tooltip: Arc>>, +} + +pub struct ActiveTooltip { + hover_start: Instant, + view: Option, } impl InteractiveElementState { @@ -1175,7 +1194,7 @@ pub(crate) type DragListener = pub(crate) type HoverListener = Box) + 'static>; -pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; +pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; pub type KeyListener = Box< dyn Fn( From 1361b617082bf730dfbd4aaa57bf8bbe4226e08f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 3 Nov 2023 16:36:02 -0400 Subject: [PATCH 12/17] Use an `IconButton` for the tab close button --- crates/ui2/src/elements/icon.rs | 10 ---------- crates/workspace2/src/pane.rs | 19 ++++++++----------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/crates/ui2/src/elements/icon.rs b/crates/ui2/src/elements/icon.rs index 94e15ca0f1187dcc8851619fa7d94f0b295300c8..9c056d68d1ce4beab415c9ca7f8465ea9302f045 100644 --- a/crates/ui2/src/elements/icon.rs +++ b/crates/ui2/src/elements/icon.rs @@ -159,7 +159,6 @@ impl Icon { pub struct IconElement { icon: Icon, color: IconColor, - hover_color: Option, size: IconSize, } @@ -168,7 +167,6 @@ impl IconElement { Self { icon, color: IconColor::default(), - hover_color: None, size: IconSize::default(), } } @@ -178,11 +176,6 @@ impl IconElement { self } - pub fn hover_color(mut self, hover_color: impl Into>) -> Self { - self.hover_color = hover_color.into(); - self - } - pub fn size(mut self, size: IconSize) -> Self { self.size = size; self @@ -199,9 +192,6 @@ impl IconElement { .flex_none() .path(self.icon.path()) .text_color(self.color.color(cx)) - .when_some(self.hover_color, |this, hover_color| { - this.hover(|style| style.text_color(hover_color.color(cx))) - }) } } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index a1ec16848840355748666fb7cd0ad99ee789d4f9..0c2ee5f46e9ef58667cd6c7cbb916638df9789ef 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -9,8 +9,8 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - AnyView, AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, - Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model, + PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -1366,15 +1366,12 @@ impl Pane { .id(item.id()) .invisible() .group_hover("", |style| style.visible()) - .child( - IconElement::new(Icon::Close) - .color(IconColor::Muted) - .hover_color(IconColor::Accent), - ) - .on_click(move |pane: &mut Self, _, cx| { - pane.close_item_by_id(id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - }) + .child(IconButton::::new("close_tab", Icon::Close).on_click( + move |pane: &mut Self, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + }, + )) }; let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { From b5224bdafd5417619f5652458bcc0b38d896c0d5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 3 Nov 2023 16:40:46 -0400 Subject: [PATCH 13/17] Remove unneeded type qualification --- crates/workspace2/src/pane.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 0c2ee5f46e9ef58667cd6c7cbb916638df9789ef..3cc9a8b5f2ee0c41d1b051fe4af83658128028e5 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1366,7 +1366,7 @@ impl Pane { .id(item.id()) .invisible() .group_hover("", |style| style.visible()) - .child(IconButton::::new("close_tab", Icon::Close).on_click( + .child(IconButton::new("close_tab", Icon::Close).on_click( move |pane: &mut Self, cx| { pane.close_item_by_id(id, SaveIntent::Close, cx) .detach_and_log_err(cx); From 3834e26f7150215502754eae999f7fc35ca11008 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 3 Nov 2023 18:02:58 -0400 Subject: [PATCH 14/17] Tooltips in mouse event handler & fix executor timer Co-Authored-By: Conrad Irwin --- crates/gpui2/build.rs | 1 + crates/gpui2/src/interactive.rs | 56 +++++++++------------ crates/gpui2/src/platform/mac/dispatcher.rs | 14 +----- crates/gpui2/src/window.rs | 13 +++-- crates/ui2/src/components/tooltip.rs | 34 +++++++++++-- crates/workspace2/src/pane.rs | 8 +-- 6 files changed, 67 insertions(+), 59 deletions(-) diff --git a/crates/gpui2/build.rs b/crates/gpui2/build.rs index c9abfaa6bb5e79ad07d77b1855c9414c2b0b5b05..6e8a0868b969c7a85627fd974f0f1bde793eb587 100644 --- a/crates/gpui2/build.rs +++ b/crates/gpui2/build.rs @@ -20,6 +20,7 @@ fn generate_dispatch_bindings() { .header("src/platform/mac/dispatch.h") .allowlist_var("_dispatch_main_q") .allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT") + .allowlist_var("DISPATCH_TIME_NOW") .allowlist_function("dispatch_get_global_queue") .allowlist_function("dispatch_async_f") .allowlist_function("dispatch_after_f") diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 4a6185f7370239ef7c62d241df1d35e7aa7061b3..f7a8f033c4be2c8683587481385b07ecd60800ad 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -17,7 +17,6 @@ use std::{ ops::Deref, path::PathBuf, sync::Arc, - time::{Duration, Instant}, }; const DRAG_THRESHOLD: f64 = 2.; @@ -602,13 +601,14 @@ pub trait ElementInteraction: 'static { if let Some(hover_listener) = stateful.hover_listener.take() { let was_hovered = element_state.hover_state.clone(); - let has_mouse_down = element_state.pending_mouse_down.lock().is_some(); + let has_mouse_down = element_state.pending_mouse_down.clone(); cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - let is_hovered = bounds.contains_point(&event.position) && !has_mouse_down; + let is_hovered = + bounds.contains_point(&event.position) && has_mouse_down.lock().is_none(); let mut was_hovered = was_hovered.lock(); if is_hovered != was_hovered.clone() { @@ -620,33 +620,30 @@ pub trait ElementInteraction: 'static { }); } - // if we're hovered: - // if no timer, start timer - // if timer hits 1s, call tooltip_builder() - // + if let Some(tooltip_builder) = stateful.tooltip_builder.take() { + let tooltip_view = element_state.tooltip_view.clone(); + let pending_mouse_down = element_state.pending_mouse_down.clone(); - if let Some(tooltip_builder) = &stateful.tooltip_builder { - let mut active_tooltip = element_state.active_tooltip.lock(); - let is_hovered = bounds.contains_point(&cx.mouse_position()) - && !element_state.pending_mouse_down.lock().is_some(); + cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + let is_hovered = bounds.contains_point(&event.position) + && pending_mouse_down.lock().is_none(); + let mut tooltip_view = tooltip_view.lock(); - if is_hovered { - if let Some(active_tooltip) = active_tooltip { - active_tooltip.view = Some(tooltip_builder(cx)) + if is_hovered { + if tooltip_view.is_none() { + *tooltip_view = Some(tooltip_builder(view_state, cx)); + } } else { - *active_tooltip = Some(ActiveTooltip { - hover_start: Instant::now(), - view: None, - }); + tooltip_view.take(); } - } else { - active_tooltip.take(); - } + }); - if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { - if *element_state.hover_state.lock() { - cx.active_tooltip = Some(active_tooltip.clone()); - } + if let Some(active_tooltip) = element_state.tooltip_view.lock().as_ref() { + cx.active_tooltip = Some(active_tooltip.clone()); } } @@ -837,12 +834,7 @@ pub struct InteractiveElementState { hover_state: Arc>, pending_mouse_down: Arc>>, scroll_offset: Option>>>, - active_tooltip: Arc>>, -} - -pub struct ActiveTooltip { - hover_start: Instant, - view: Option, + tooltip_view: Arc>>, } impl InteractiveElementState { @@ -1194,7 +1186,7 @@ pub(crate) type DragListener = pub(crate) type HoverListener = Box) + 'static>; -pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; +pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; pub type KeyListener = Box< dyn Fn( diff --git a/crates/gpui2/src/platform/mac/dispatcher.rs b/crates/gpui2/src/platform/mac/dispatcher.rs index f5334912c6b7aec93fed2af3c33832ff241313c9..68c0e3b4f53c4040899ac0d344dc1dec095a9bc1 100644 --- a/crates/gpui2/src/platform/mac/dispatcher.rs +++ b/crates/gpui2/src/platform/mac/dispatcher.rs @@ -11,11 +11,7 @@ use objc::{ }; use parking::{Parker, Unparker}; use parking_lot::Mutex; -use std::{ - ffi::c_void, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{ffi::c_void, sync::Arc, time::Duration}; include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); @@ -62,16 +58,10 @@ impl PlatformDispatcher for MacDispatcher { } fn dispatch_after(&self, duration: Duration, runnable: Runnable) { - let now = SystemTime::now(); - let after_duration = now - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_nanos() as u64 - + duration.as_nanos() as u64; unsafe { let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0); - let when = dispatch_time(0, after_duration as i64); + let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64); dispatch_after_f( when, queue, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 46ac30592bb8e6e60c134c702acb675e7fea4a39..fe6f516e434b4bc186467adf0d19e3e33a8a3d2d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -989,11 +989,14 @@ impl<'a> WindowContext<'a> { }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.stack(1, |cx| { - cx.with_element_offset(Some(cx.mouse_position()), |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_tooltip.draw(available_space, cx); - }); + cx.with_element_offset( + Some(cx.mouse_position() + Point::new(px(8.0), px(8.0))), + |cx| { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_tooltip.draw(available_space, cx); + }, + ); }); } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index eb53b506ebd8467eb9f6d31dab2abe36dd62496a..f94518224dae49df85dfc69d8830be609c0b21c0 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,20 +1,44 @@ +use std::time::Duration; + use gpui2::{ - div, px, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, + div, px, Component, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, + VisualContext, WindowContext, }; use theme2::ActiveTheme; +const DELAY: Duration = Duration::from_millis(500); + #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, + visible: bool, } impl TextTooltip { pub fn new(str: SharedString) -> Self { - Self { title: str } + Self { + title: str, + visible: false, + } } - pub fn build_view(str: SharedString, cx: &mut C) -> C::Result> { - cx.build_view(|cx| TextTooltip::new(str)) + pub fn build_view(str: SharedString, cx: &mut WindowContext) -> View { + let view = cx.build_view(|cx| TextTooltip::new(str)); + + let handle = view.downgrade(); + cx.spawn(|mut cx| async move { + cx.background_executor().timer(DELAY).await; + + handle + .update(&mut cx, |this, cx| { + this.visible = true; + cx.notify(); + }) + .ok(); + }) + .detach(); + + view } } @@ -24,9 +48,11 @@ impl Render for TextTooltip { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let theme = cx.theme(); div() + .when(!self.visible, |this| this.invisible()) .bg(theme.colors().background) .rounded(px(8.)) .border() + .font("Zed Sans") .border_color(theme.colors().border) .text_color(theme.colors().text) .pl_2() diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index a1ec16848840355748666fb7cd0ad99ee789d4f9..64b995b538a8773a0cac848cc03b7c9e1f58cffb 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -9,8 +9,8 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - AnyView, AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, - Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model, + PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -1398,13 +1398,9 @@ impl Pane { .group("") .id(item.id()) .cursor_pointer() - .on_hover(|_, hovered, _| { - dbg!(hovered); - }) .when_some(item.tab_tooltip_text(cx), |div, text| { div.tooltip(move |_, cx| TextTooltip::build_view(text.clone(), cx)) }) - // .tooltip(|pane, cx| cx.build_view(|cx| div().child(title))) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) // .on_drop(|_view, state: View, cx| { From 4725cd2cd6b396c5adf19c0a27d87a1750598cf3 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 3 Nov 2023 18:37:15 -0400 Subject: [PATCH 15/17] Move more tooltip logic into gpui2 & fix tooltip moving on paint Co-Authored-By: Conrad Irwin --- crates/gpui2/src/app.rs | 7 +++- crates/gpui2/src/geometry.rs | 2 +- crates/gpui2/src/interactive.rs | 58 ++++++++++++++++++++++------ crates/gpui2/src/window.rs | 13 +++---- crates/ui2/src/components/tooltip.rs | 33 +--------------- crates/workspace2/src/pane.rs | 2 +- 6 files changed, 61 insertions(+), 54 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 974395e8971f92a3524a5a8177f792a567b88904..9e454dc6529fae93fc799d23f5ff43c52e3ed834 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -157,7 +157,7 @@ pub struct AppContext { flushing_effects: bool, pending_updates: usize, pub(crate) active_drag: Option, - pub(crate) active_tooltip: Option, + pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: HashMap>, pub(crate) frame_consumers: HashMap>, pub(crate) background_executor: BackgroundExecutor, @@ -898,3 +898,8 @@ pub(crate) struct AnyDrag { pub view: AnyView, pub cursor_offset: Point, } + +pub(crate) struct AnyTooltip { + pub view: AnyView, + pub cursor_offset: Point, +} diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 081b11aae0e250237e091bf8ab7ecc565e57c960..d6755a53973f00d5fee4fd11912f6e73da26cf71 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -21,7 +21,7 @@ pub fn point(x: T, y: T) -> Point { } impl Point { - pub fn new(x: T, y: T) -> Self { + pub const fn new(x: T, y: T) -> Self { Self { x, y } } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index f7a8f033c4be2c8683587481385b07ecd60800ad..dd09ad512bf25d2e3049efa63ea032256df35d7e 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,8 +1,8 @@ use crate::{ - div, point, px, Action, AnyDrag, AnyView, AppContext, BorrowWindow, Bounds, Component, - DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, Keystroke, - Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, View, - ViewContext, + div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds, + Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, + Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, + StyleRefinement, View, ViewContext, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -17,9 +17,12 @@ use std::{ ops::Deref, path::PathBuf, sync::Arc, + time::Duration, }; const DRAG_THRESHOLD: f64 = 2.; +const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); pub trait StatelessInteractive: Element { fn stateless_interaction(&mut self) -> &mut StatelessInteraction; @@ -621,7 +624,7 @@ pub trait ElementInteraction: 'static { } if let Some(tooltip_builder) = stateful.tooltip_builder.take() { - let tooltip_view = element_state.tooltip_view.clone(); + let active_tooltip = element_state.active_tooltip.clone(); let pending_mouse_down = element_state.pending_mouse_down.clone(); cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { @@ -631,19 +634,44 @@ pub trait ElementInteraction: 'static { let is_hovered = bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none(); - let mut tooltip_view = tooltip_view.lock(); + let mut tooltip_lock = active_tooltip.lock(); if is_hovered { - if tooltip_view.is_none() { - *tooltip_view = Some(tooltip_builder(view_state, cx)); + if tooltip_lock.is_none() { + *tooltip_lock = Some(ActiveTooltip { + view: tooltip_builder(view_state, cx), + visible: false, + coordinates: event.position, + }); + + let active_tooltip = active_tooltip.clone(); + cx.spawn(move |view, mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + + view.update(&mut cx, |_, cx| { + if let Some(active_tooltip) = active_tooltip.lock().as_mut() { + active_tooltip.visible = true; + active_tooltip.coordinates = + cx.mouse_position() + TOOLTIP_OFFSET; + } + cx.notify(); + }) + .ok() + }) + .detach(); } } else { - tooltip_view.take(); + tooltip_lock.take(); } }); - if let Some(active_tooltip) = element_state.tooltip_view.lock().as_ref() { - cx.active_tooltip = Some(active_tooltip.clone()); + if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { + if active_tooltip.visible { + cx.active_tooltip = Some(AnyTooltip { + view: active_tooltip.view.clone(), + cursor_offset: active_tooltip.coordinates, + }); + } } } @@ -834,7 +862,13 @@ pub struct InteractiveElementState { hover_state: Arc>, pending_mouse_down: Arc>>, scroll_offset: Option>>>, - tooltip_view: Arc>>, + active_tooltip: Arc>>, +} + +struct ActiveTooltip { + view: AnyView, + visible: bool, + coordinates: Point, } impl InteractiveElementState { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fe6f516e434b4bc186467adf0d19e3e33a8a3d2d..9cab40082b02912eced41363a2afa6506fb3d9c0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -989,14 +989,11 @@ impl<'a> WindowContext<'a> { }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.stack(1, |cx| { - cx.with_element_offset( - Some(cx.mouse_position() + Point::new(px(8.0), px(8.0))), - |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_tooltip.draw(available_space, cx); - }, - ); + cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_tooltip.view.draw(available_space, cx); + }); }); } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index f94518224dae49df85dfc69d8830be609c0b21c0..0bc00a9be1120e80c2b840bc064ddf3ee584a1c0 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,44 +1,16 @@ use std::time::Duration; -use gpui2::{ - div, px, Component, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, - VisualContext, WindowContext, -}; +use gpui2::{div, px, Div, ParentElement, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; -const DELAY: Duration = Duration::from_millis(500); - #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, - visible: bool, } impl TextTooltip { pub fn new(str: SharedString) -> Self { - Self { - title: str, - visible: false, - } - } - - pub fn build_view(str: SharedString, cx: &mut WindowContext) -> View { - let view = cx.build_view(|cx| TextTooltip::new(str)); - - let handle = view.downgrade(); - cx.spawn(|mut cx| async move { - cx.background_executor().timer(DELAY).await; - - handle - .update(&mut cx, |this, cx| { - this.visible = true; - cx.notify(); - }) - .ok(); - }) - .detach(); - - view + Self { title: str } } } @@ -48,7 +20,6 @@ impl Render for TextTooltip { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let theme = cx.theme(); div() - .when(!self.visible, |this| this.invisible()) .bg(theme.colors().background) .rounded(px(8.)) .border() diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 64b995b538a8773a0cac848cc03b7c9e1f58cffb..131a3e977b4cd8a2e461f225e26413e9fe4f50e4 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1399,7 +1399,7 @@ impl Pane { .id(item.id()) .cursor_pointer() .when_some(item.tab_tooltip_text(cx), |div, text| { - div.tooltip(move |_, cx| TextTooltip::build_view(text.clone(), cx)) + div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone()))) }) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) From de5458cfe0f419324a7258647b887c0eb8db5c71 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 3 Nov 2023 21:40:28 -0600 Subject: [PATCH 16/17] Update tooltip code a bit This fixes a tiny UX bug where the tooltip would appear to move if you hovered over an element, then moved your mouse out and back within 500ms. The fix is to retain the task, so we can drop it to cancel it when the mouse leaves. Also changes the time we construct the tooltip to the time it first shows. --- crates/gpui2/src/app.rs | 1 + crates/gpui2/src/interactive.rs | 64 ++++++++++++++-------------- crates/ui2/src/components/tooltip.rs | 2 - 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 9e454dc6529fae93fc799d23f5ff43c52e3ed834..bc9101fa0cc18b6a826e45b1230bfc31d3223578 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -899,6 +899,7 @@ pub(crate) struct AnyDrag { pub cursor_offset: Point, } +#[derive(Clone)] pub(crate) struct AnyTooltip { pub view: AnyView, pub cursor_offset: Point, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index dd09ad512bf25d2e3049efa63ea032256df35d7e..da208b38131f8ddd0e297c47bc504065af232d15 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -2,7 +2,7 @@ use crate::{ div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds, Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, - StyleRefinement, View, ViewContext, + StyleRefinement, Task, View, ViewContext, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -627,50 +627,48 @@ pub trait ElementInteraction: 'static { let active_tooltip = element_state.active_tooltip.clone(); let pending_mouse_down = element_state.pending_mouse_down.clone(); - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } let is_hovered = bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none(); - let mut tooltip_lock = active_tooltip.lock(); - - if is_hovered { - if tooltip_lock.is_none() { - *tooltip_lock = Some(ActiveTooltip { - view: tooltip_builder(view_state, cx), - visible: false, - coordinates: event.position, - }); + if !is_hovered { + active_tooltip.lock().take(); + return; + } + if active_tooltip.lock().is_none() { + let task = cx.spawn({ let active_tooltip = active_tooltip.clone(); - cx.spawn(move |view, mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; + let tooltip_builder = tooltip_builder.clone(); - view.update(&mut cx, |_, cx| { - if let Some(active_tooltip) = active_tooltip.lock().as_mut() { - active_tooltip.visible = true; - active_tooltip.coordinates = - cx.mouse_position() + TOOLTIP_OFFSET; - } + move |view, mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + view.update(&mut cx, move |view_state, cx| { + active_tooltip.lock().replace(ActiveTooltip { + waiting: None, + tooltip: Some(AnyTooltip { + view: tooltip_builder(view_state, cx), + cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, + }), + }); cx.notify(); }) - .ok() - }) - .detach(); - } - } else { - tooltip_lock.take(); + .ok(); + } + }); + active_tooltip.lock().replace(ActiveTooltip { + waiting: Some(task), + tooltip: None, + }); } }); if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { - if active_tooltip.visible { - cx.active_tooltip = Some(AnyTooltip { - view: active_tooltip.view.clone(), - cursor_offset: active_tooltip.coordinates, - }); + if active_tooltip.tooltip.is_some() { + cx.active_tooltip = active_tooltip.tooltip.clone() } } } @@ -866,9 +864,9 @@ pub struct InteractiveElementState { } struct ActiveTooltip { - view: AnyView, - visible: bool, - coordinates: Point, + #[allow(unused)] // used to drop the task + waiting: Option>, + tooltip: Option, } impl InteractiveElementState { diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 0bc00a9be1120e80c2b840bc064ddf3ee584a1c0..c05214eea4e8d977ab85d875b341e1165a785b3a 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use gpui2::{div, px, Div, ParentElement, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; From d88dccffbc035178e8437204f9e41d23c3c04485 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 6 Nov 2023 11:02:30 -0700 Subject: [PATCH 17/17] Allow multiple subscriptions from one entity handle --- crates/gpui2/src/subscription.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui2/src/subscription.rs b/crates/gpui2/src/subscription.rs index 744e83bbbddda1a6e6264fa62f81fb23c30f6830..2f4ec0d2f13212440fe7d23585781bac7b2be069 100644 --- a/crates/gpui2/src/subscription.rs +++ b/crates/gpui2/src/subscription.rs @@ -38,7 +38,7 @@ where lock.subscribers .entry(emitter_key.clone()) .or_default() - .insert(Default::default()) + .get_or_insert_with(|| Default::default()) .insert(subscriber_id, callback); let this = self.0.clone(); Subscription {