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 c439adcc44..84263d4ed7 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 acc41deba8..c56defed26 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 84263d4ed7..a4d34ad36f 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 c56defed26..eef829afa7 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 eef829afa7..1bccaaea64 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 441aef21f5..a9e95b8c29 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 ca4ebcdb13..4f7ba02c2a 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 1bccaaea64..4c4f26fe0e 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 | 57 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/crates/gpui2/src/subscription.rs b/crates/gpui2/src/subscription.rs index 64fcd74dd2..744e83bbbd 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(); + let Some(mut subscribers) = self + .0 + .lock() + .subscribers + .get_mut(emitter) + .and_then(|s| s.take()) + else { + return; + }; - // Add any new subscribers that were added while invoking the callback. - if let Some(new_subscribers) = lock.subscribers.remove(&emitter) { - subscribers.extend(new_subscribers); - } + subscribers.retain(|_, callback| f(callback)); + let mut lock = self.0.lock(); - // 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); - } + // 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); + } - if !subscribers.is_empty() { - lock.subscribers.insert(emitter, 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 | 177 +++++++++++++++++++----- crates/editor2/src/scroll.rs | 28 ++-- crates/editor2/src/scroll/autoscroll.rs | 6 +- crates/gpui2/src/geometry.rs | 6 + 5 files changed, 215 insertions(+), 94 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ea747de5de..52628f61b5 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 645cdc7646..6420d1e6cd 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; + 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(); - // 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() - // } + 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 mut size = constraint.max; + // if size.x().is_infinite() { + // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); + // } - 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 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 5e4b32265a..1876952ae2 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 a4c37a258e..9315d5c099 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 b2fad4efda..081b11aae0 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 a92dbd6ff9..2a0f557272 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 56940efce4..bf28ec3bf5 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 b30d4aa002..b3cd5c478b 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 1eed74f096..a88a8b4d6f 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 5885d76101..94e15ca0f1 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 4c4f26fe0e..f8e3c02178 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 bf28ec3bf5..e011041bae 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 b3cd5c478b..d2571a3253 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 a88a8b4d6f..4a9a5d7ecf 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 2284f3ccc2..e998766bbb 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 f8e3c02178..4d5b96ea6d 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 79ba132e4f..52eb512eb4 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 020cb82cd2..7966b68e12 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 4d5b96ea6d..acda04c5a4 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 9ec76a86d4..974395e897 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 7966b68e12..def7315a7c 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 e998766bbb..46ac30592b 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 7a9cac01ca..f10f8cee8d 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 0000000000..eb53b506eb --- /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 acda04c5a4..a1ec168488 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 def7315a7c..4a6185f737 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 94e15ca0f1..9c056d68d1 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 a1ec168488..0c2ee5f46e 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 0c2ee5f46e..3cc9a8b5f2 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 c9abfaa6bb..6e8a0868b9 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 4a6185f737..f7a8f033c4 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; + } - if is_hovered { - if let Some(active_tooltip) = active_tooltip { - active_tooltip.view = Some(tooltip_builder(cx)) + 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 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 f5334912c6..68c0e3b4f5 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 46ac30592b..fe6f516e43 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 eb53b506eb..f94518224d 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 a1ec168488..64b995b538 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 974395e897..9e454dc652 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 081b11aae0..d6755a5397 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 f7a8f033c4..dd09ad512b 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 fe6f516e43..9cab40082b 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 f94518224d..0bc00a9be1 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 64b995b538..131a3e977b 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 9e454dc652..bc9101fa0c 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 dd09ad512b..da208b3813 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 0bc00a9be1..c05214eea4 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 744e83bbbd..2f4ec0d2f1 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 {