editor2 rendering (#3244)

Conrad Irwin created

- Make tab bar visible
- Fix tab text colors
- Ensure panes cover the available space
- Make the close button close
- Fix bug when unsubscribe called after remove
- WIP: start on editor element
- Add hover behaviour to tabs
- Add PointingHand on tabs
- gpui2: Add on_hover events
- Tooltip on tabs
- MOAR TOOLTIPS
- Use an `IconButton` for the tab close button
- Remove unneeded type qualification
- Tooltips in mouse event handler & fix executor timer
- Move more tooltip logic into gpui2 & fix tooltip moving on paint
- Update tooltip code a bit
- Allow multiple subscriptions from one entity handle

Release Notes:

- N/A

Change summary

crates/editor2/src/editor.rs                |  92 ++++++-----
crates/editor2/src/element.rs               | 181 ++++++++++++++++++----
crates/editor2/src/items.rs                 |  15 +
crates/editor2/src/scroll.rs                |  28 +-
crates/editor2/src/scroll/autoscroll.rs     |   6 
crates/gpui2/build.rs                       |   1 
crates/gpui2/src/app.rs                     |   8 +
crates/gpui2/src/elements/div.rs            |  15 +
crates/gpui2/src/geometry.rs                |   8 
crates/gpui2/src/interactive.rs             | 131 ++++++++++++++++
crates/gpui2/src/platform/mac/dispatcher.rs |  14 -
crates/gpui2/src/style.rs                   |  21 ++
crates/gpui2/src/styled.rs                  |  53 ++++++
crates/gpui2/src/subscription.rs            |  63 ++++---
crates/gpui2/src/window.rs                  |  39 +++-
crates/ui2/src/components.rs                |   2 
crates/ui2/src/components/icon.rs           |   3 
crates/ui2/src/components/tooltip.rs        |  31 +++
crates/workspace2/src/pane.rs               |  36 +++
crates/workspace2/src/pane_group.rs         |   2 
crates/workspace2/src/status_bar.rs         |   1 
crates/zed2/src/main.rs                     |   1 
22 files changed, 582 insertions(+), 169 deletions(-)

Detailed changes

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>,
-    //     ) {
-    //         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>,
+    ) {
+        self.soft_wrap_mode_override = Some(mode);
+        cx.notify();
+    }
 
-    //     pub fn set_wrap_width(&self, width: Option<f32>, 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<Pixels>, 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<Self>) {
     //         if self.soft_wrap_mode_override.is_some() {
@@ -9321,11 +9322,14 @@ impl EventEmitter for Editor {
 }
 
 impl Render for Editor {
-    type Element = Div<Self>;
+    type Element = EditorElement;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        // todo!()
-        div()
+        EditorElement::new(EditorStyle {
+            text: cx.text_style(),
+            line_height_scalar: 1.,
+            theme_id: 0,
+        })
     }
 }
 

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<Editor>) -> 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<Editor>) -> 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<Editor>) -> 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<Editor>) -> 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<Editor> for EditorElement {
         element_state: &mut Self::ElementState,
         cx: &mut gpui::ViewContext<Editor>,
     ) -> 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<Editor> for EditorElement {
     fn paint(
         &mut self,
         bounds: Bounds<gpui::Pixels>,
-        view_state: &mut Editor,
+        editor: &mut Editor,
         element_state: &mut Self::ElementState,
         cx: &mut gpui::ViewContext<Editor>,
     ) {
-        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::<SmallVec<[_; 2]>>();
+
+        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();
     }
 }
 

crates/editor2/src/items.rs 🔗

@@ -578,18 +578,24 @@ impl Item for Editor {
 
     fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
         let theme = cx.theme();
+
         AnyElement::new(
             div()
                 .flex()
                 .flex_row()
                 .items_center()
-                .bg(gpui::white())
-                .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)),
+                    )
                 })),
         )
     }
@@ -625,8 +631,7 @@ impl Item for Editor {
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         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<Self>) {

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<Self>) {
-    //         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<Self>) {
+        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,

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<Editor>,
     ) -> 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 { .. }) {

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")

crates/gpui2/src/app.rs 🔗

@@ -161,6 +161,7 @@ pub struct AppContext {
     flushing_effects: bool,
     pending_updates: usize,
     pub(crate) active_drag: Option<AnyDrag>,
+    pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
     pub(crate) frame_consumers: HashMap<DisplayId, Task<()>>,
     pub(crate) background_executor: BackgroundExecutor,
@@ -219,6 +220,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,
@@ -900,3 +902,9 @@ pub(crate) struct AnyDrag {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }
+
+#[derive(Clone)]
+pub(crate) struct AnyTooltip {
+    pub view: AnyView,
+    pub cursor_offset: Point<Pixels>,
+}

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,22 @@ where
         cx: &mut ViewContext<V>,
     ) {
         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(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);
             }
 
-            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);

crates/gpui2/src/geometry.rs 🔗

@@ -21,7 +21,7 @@ pub fn point<T: Clone + Debug + Default>(x: T, y: T) -> Point<T> {
 }
 
 impl<T: Clone + Debug + Default> Point<T> {
-    pub fn new(x: T, y: T) -> Self {
+    pub const fn new(x: T, y: T) -> Self {
         Self { x, y }
     }
 
@@ -825,6 +825,12 @@ impl From<Pixels> for u32 {
     }
 }
 
+impl From<u32> for Pixels {
+    fn from(pixels: u32) -> Self {
+        Pixels(pixels as f32)
+    }
+}
+
 impl From<Pixels> for usize {
     fn from(pixels: Pixels) -> Self {
         pixels.0 as usize

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, Task, 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<Pixels> = Point::new(px(10.0), px(8.0));
 
 pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V>;
@@ -333,6 +336,37 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
             }));
         self
     }
+
+    fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> 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<W>(
+        mut self,
+        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + '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(Arc::new(move |view_state, cx| {
+            build_tooltip(view_state, cx).into()
+        }));
+
+        self
+    }
 }
 
 pub trait ElementInteraction<V: 'static>: 'static {
@@ -568,6 +602,77 @@ pub trait ElementInteraction<V: 'static>: '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.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.lock().is_none();
+                    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);
+                    }
+                });
+            }
+
+            if let Some(tooltip_builder) = stateful.tooltip_builder.take() {
+                let active_tooltip = element_state.active_tooltip.clone();
+                let pending_mouse_down = element_state.pending_mouse_down.clone();
+
+                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();
+                    if !is_hovered {
+                        active_tooltip.lock().take();
+                        return;
+                    }
+
+                    if active_tooltip.lock().is_none() {
+                        let task = cx.spawn({
+                            let active_tooltip = active_tooltip.clone();
+                            let tooltip_builder = tooltip_builder.clone();
+
+                            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();
+                            }
+                        });
+                        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.tooltip.is_some() {
+                        cx.active_tooltip = active_tooltip.tooltip.clone()
+                    }
+                }
+            }
+
             let active_state = element_state.active_state.clone();
             if active_state.lock().is_none() {
                 let active_group_bounds = stateful
@@ -639,6 +744,8 @@ pub struct StatefulInteraction<V> {
     active_style: StyleRefinement,
     group_active_style: Option<GroupStyle>,
     drag_listener: Option<DragListener<V>>,
+    hover_listener: Option<HoverListener<V>>,
+    tooltip_builder: Option<TooltipBuilder<V>>,
 }
 
 impl<V: 'static> ElementInteraction<V> for StatefulInteraction<V> {
@@ -666,6 +773,8 @@ impl<V> From<ElementId> for StatefulInteraction<V> {
             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 +804,8 @@ impl<V> StatelessInteraction<V> {
             stateless: self,
             click_listeners: SmallVec::new(),
             drag_listener: None,
+            hover_listener: None,
+            tooltip_builder: None,
             active_style: StyleRefinement::default(),
             group_active_style: None,
         }
@@ -746,8 +857,16 @@ impl ActiveState {
 #[derive(Default)]
 pub struct InteractiveElementState {
     active_state: Arc<Mutex<ActiveState>>,
+    hover_state: Arc<Mutex<bool>>,
     pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
     scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
+    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+}
+
+struct ActiveTooltip {
+    #[allow(unused)] // used to drop the task
+    waiting: Option<Task<()>>,
+    tooltip: Option<AnyTooltip>,
 }
 
 impl InteractiveElementState {
@@ -1097,6 +1216,10 @@ pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>)
 pub(crate) type DragListener<V> =
     Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
 
+pub(crate) type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
+
+pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+
 pub type KeyListener<V> = Box<
     dyn Fn(
             &mut V,

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,

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;
@@ -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]
@@ -98,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<CursorStyle>,
+
     pub z_index: Option<u32>,
 }
 
@@ -107,6 +113,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 +310,7 @@ impl Default for Style {
     fn default() -> Self {
         Style {
             display: Display::Block,
+            visibility: Visibility::Visible,
             overflow: Point {
                 x: Overflow::Visible,
                 y: Overflow::Visible,
@@ -328,6 +342,7 @@ impl Default for Style {
             corner_radii: Corners::default(),
             box_shadow: Default::default(),
             text: TextStyleRefinement::default(),
+            mouse_cursor: None,
             z_index: None,
         }
     }

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,
+    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;
@@ -60,6 +61,54 @@ 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
+    }
+
+    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

crates/gpui2/src/subscription.rs 🔗

@@ -14,7 +14,7 @@ impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
 }
 
 struct SubscriberSetState<EmitterKey, Callback> {
-    subscribers: BTreeMap<EmitterKey, BTreeMap<usize, Callback>>,
+    subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Callback>>>,
     dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
     next_subscriber_id: usize,
 }
@@ -38,12 +38,18 @@ where
         lock.subscribers
             .entry(emitter_key.clone())
             .or_default()
+            .get_or_insert_with(|| 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<Item = Callback> {
         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<F>(&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));
         }
     }
 }

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<RwLock<SlotMap<FocusId, AtomicUsize>>>,
     default_prevented: bool,
     mouse_position: Point<Pixels>,
+    requested_cursor_style: Option<CursorStyle>,
     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<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
@@ -981,12 +987,27 @@ 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(active_tooltip.cursor_offset), |cx| {
+                    let available_space =
+                        size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                    active_tooltip.view.draw(available_space, cx);
+                });
+            });
         }
 
         self.window.root_view = Some(root_view);
         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;
     }
 

crates/ui2/src/components.rs 🔗

@@ -23,6 +23,7 @@ mod tab;
 mod toast;
 mod toggle;
 mod tool_divider;
+mod tooltip;
 
 pub use avatar::*;
 pub use button::*;
@@ -49,3 +50,4 @@ pub use tab::*;
 pub use toast::*;
 pub use toggle::*;
 pub use tool_divider::*;
+pub use tooltip::*;

crates/ui2/src/components/icon.rs 🔗

@@ -186,7 +186,6 @@ impl IconElement {
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let fill = self.color.color(cx);
         let svg_size = match self.size {
             IconSize::Small => rems(0.75),
             IconSize::Medium => rems(0.9375),
@@ -196,7 +195,7 @@ impl IconElement {
             .size(svg_size)
             .flex_none()
             .path(self.icon.path())
-            .text_color(fill)
+            .text_color(self.color.color(cx))
     }
 }
 

crates/ui2/src/components/tooltip.rs 🔗

@@ -0,0 +1,31 @@
+use gpui2::{div, px, Div, ParentElement, Render, SharedString, Styled, ViewContext};
+use theme2::ActiveTheme;
+
+#[derive(Clone, Debug)]
+pub struct TextTooltip {
+    title: SharedString,
+}
+
+impl TextTooltip {
+    pub fn new(str: SharedString) -> Self {
+        Self { title: str }
+    }
+}
+
+impl Render for TextTooltip {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let theme = cx.theme();
+        div()
+            .bg(theme.colors().background)
+            .rounded(px(8.))
+            .border()
+            .font("Zed Sans")
+            .border_color(theme.colors().border)
+            .text_color(theme.colors().text)
+            .pl_2()
+            .pr_2()
+            .child(self.title.clone())
+    }
+}

crates/workspace2/src/pane.rs 🔗

@@ -26,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)]
@@ -1359,15 +1359,30 @@ impl Pane {
         cx: &mut ViewContext<'_, Pane>,
     ) -> impl Component<Self> {
         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())
+                .invisible()
+                .group_hover("", |style| style.visible())
+                .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 (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_background,
                 cx.theme().colors().ghost_element_hover,
                 cx.theme().colors().ghost_element_active,
             ),
             true => (
+                cx.theme().colors().text,
                 cx.theme().colors().tab_active_background,
                 cx.theme().colors().element_hover,
                 cx.theme().colors().element_active,
@@ -1377,7 +1392,12 @@ impl Pane {
         let close_right = ItemSettings::get_global(cx).close_position.right();
 
         div()
+            .group("")
             .id(item.id())
+            .cursor_pointer()
+            .when_some(item.tab_tooltip_text(cx), |div, text| {
+                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::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
             // .on_drop(|_view, state: View<DraggedTab>, cx| {
@@ -1397,6 +1417,7 @@ impl Pane {
                     .flex()
                     .items_center()
                     .gap_1p5()
+                    .text_color(text_color)
                     .children(if item.has_conflict(cx) {
                         Some(
                             IconElement::new(Icon::ExclamationTriangle)
@@ -1457,7 +1478,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()
@@ -1888,13 +1909,14 @@ impl Render for Pane {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> 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 {}

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))

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_background)
             .child(self.render_left_tools(cx))
             .child(self.render_right_tools(cx))

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)
             }