Merge branch 'main' into saving-2

Mikayla created

Change summary

Cargo.lock                                     |  11 
Cargo.toml                                     |   1 
crates/command_palette2/src/command_palette.rs |  16 
crates/editor2/src/display_map.rs              |  29 
crates/editor2/src/editor.rs                   |  32 
crates/editor2/src/element.rs                  | 133 +++---
crates/editor2/src/movement.rs                 |  10 
crates/editor2/src/selections_collection.rs    |   8 
crates/file_finder2/src/file_finder.rs         |  21 
crates/go_to_line2/src/go_to_line.rs           |  19 
crates/gpui2/src/element.rs                    |   2 
crates/gpui2/src/elements/div.rs               |  12 
crates/gpui2/src/elements/overlay.rs           |  39 +
crates/gpui2/src/elements/text.rs              | 191 ++++++---
crates/gpui2/src/platform/mac/text_system.rs   |  10 
crates/gpui2/src/platform/mac/window.rs        |  36 
crates/gpui2/src/style.rs                      |   1 
crates/gpui2/src/text_system.rs                |  82 +++
crates/gpui2/src/text_system/line.rs           | 265 ++++++++-----
crates/gpui2/src/text_system/line_layout.rs    | 136 +++++-
crates/gpui2/src/window.rs                     |  26 +
crates/picker2/src/picker2.rs                  |  10 
crates/storybook2/src/storybook2.rs            |   2 
crates/storybook3/Cargo.toml                   |  17 
crates/storybook3/src/storybook3.rs            |  73 +++
crates/terminal_view2/src/terminal_view.rs     |  16 
crates/ui2/src/components/context_menu.rs      | 384 ++++++++++++++++---
crates/ui2/src/components/icon_button.rs       |  23 +
crates/ui2/src/components/list.rs              |  24 +
crates/ui2/src/story.rs                        |   1 
crates/ui2/src/styled_ext.rs                   |   1 
crates/workspace2/src/dock.rs                  |  58 ++
crates/workspace2/src/modal_layer.rs           |  22 
crates/workspace2/src/workspace2.rs            |  40 +
34 files changed, 1,231 insertions(+), 520 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8823,6 +8823,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "storybook3"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "settings2",
+ "theme2",
+ "ui2",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.4"

Cargo.toml 🔗

@@ -95,6 +95,7 @@ members = [
     "crates/sqlez_macros",
     "crates/rich_text",
     "crates/storybook2",
+    "crates/storybook3",
     "crates/sum_tree",
     "crates/terminal",
     "crates/terminal2",

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,9 +1,8 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
-    Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
+    ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::{
@@ -16,7 +15,7 @@ use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
 };
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 use zed_actions::OpenZedURL;
 
 actions!(Toggle);
@@ -69,10 +68,9 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<ModalEvent> for CommandPalette {}
-impl Modal for CommandPalette {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx));
+impl ManagedView for CommandPalette {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 
@@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 

crates/editor2/src/display_map.rs 🔗

@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
-    Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
+    Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
+    TextRun, UnderlineStyle, WrappedLine,
 };
 use inlay_map::InlayMap;
 use language::{
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn lay_out_line_for_row(
+    pub fn layout_row(
         &self,
         display_row: u32,
         TextLayoutDetails {
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
             editor_style,
             rem_size,
         }: &TextLayoutDetails,
-    ) -> Line {
+    ) -> Arc<LineLayout> {
         let mut runs = Vec::new();
         let mut line = String::new();
 
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
 
         let font_size = editor_style.text.font_size.to_pixels(*rem_size);
         text_system
-            .layout_text(&line, font_size, &runs, None)
-            .unwrap()
-            .pop()
-            .unwrap()
+            .layout_line(&line, font_size, &runs)
+            .expect("we expect the font to be loaded because it's rendered by the editor")
     }
 
-    pub fn x_for_point(
+    pub fn x_for_display_point(
         &self,
         display_point: DisplayPoint,
         text_layout_details: &TextLayoutDetails,
     ) -> Pixels {
-        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
-        layout_line.x_for_index(display_point.column() as usize)
+        let line = self.layout_row(display_point.row(), text_layout_details);
+        line.x_for_index(display_point.column() as usize)
     }
 
-    pub fn column_for_x(
+    pub fn display_column_for_x(
         &self,
         display_row: u32,
-        x_coordinate: Pixels,
-        text_layout_details: &TextLayoutDetails,
+        x: Pixels,
+        details: &TextLayoutDetails,
     ) -> u32 {
-        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
-        layout_line.closest_index_for_x(x_coordinate) as u32
+        let layout_line = self.layout_row(display_row, details);
+        layout_line.closest_index_for_x(x) as u32
     }
 
     pub fn chars_at(

crates/editor2/src/editor.rs 🔗

@@ -5445,7 +5445,9 @@ impl Editor {
                     *head.column_mut() += 1;
                     head = display_map.clip_point(head, Bias::Right);
                     let goal = SelectionGoal::HorizontalPosition(
-                        display_map.x_for_point(head, &text_layout_details).into(),
+                        display_map
+                            .x_for_display_point(head, &text_layout_details)
+                            .into(),
                     );
                     selection.collapse_to(head, goal);
 
@@ -6391,8 +6393,8 @@ impl Editor {
             let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
             let range = oldest_selection.display_range(&display_map).sorted();
 
-            let start_x = display_map.x_for_point(range.start, &text_layout_details);
-            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
             let positions = start_x.min(end_x)..start_x.max(end_x);
 
             selections.clear();
@@ -6431,15 +6433,16 @@ impl Editor {
                     let range = selection.display_range(&display_map).sorted();
                     debug_assert_eq!(range.start.row(), range.end.row());
                     let mut row = range.start.row();
-                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
-                        selection.goal
-                    {
-                        px(start)..px(end)
-                    } else {
-                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
-                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
-                        start_x.min(end_x)..start_x.max(end_x)
-                    };
+                    let positions =
+                        if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
+                            px(start)..px(end)
+                        } else {
+                            let start_x =
+                                display_map.x_for_display_point(range.start, &text_layout_details);
+                            let end_x =
+                                display_map.x_for_display_point(range.end, &text_layout_details);
+                            start_x.min(end_x)..start_x.max(end_x)
+                        };
 
                     while row != end_row {
                         if above {
@@ -6992,7 +6995,7 @@ impl Editor {
                         let display_point = point.to_display_point(display_snapshot);
                         let goal = SelectionGoal::HorizontalPosition(
                             display_snapshot
-                                .x_for_point(display_point, &text_layout_details)
+                                .x_for_display_point(display_point, &text_layout_details)
                                 .into(),
                         );
                         (display_point, goal)
@@ -9759,7 +9762,8 @@ impl InputHandler for Editor {
         let scroll_left = scroll_position.x * em_width;
 
         let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
-        let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
+        let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
+            + self.gutter_width;
         let y = line_height * (start.row() as f32 - scroll_position.y);
 
         Some(Bounds {

crates/editor2/src/element.rs 🔗

@@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
-    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line,
+    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
     MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
-    ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View,
-    ViewContext, WindowContext,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
+    TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -476,7 +476,7 @@ impl EditorElement {
             Self::paint_diff_hunks(bounds, layout, cx);
         }
 
-        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+        for (ix, line) in layout.line_numbers.iter().enumerate() {
             if let Some(line) = line {
                 let line_origin = bounds.origin
                     + point(
@@ -775,21 +775,21 @@ impl EditorElement {
                                         .chars_at(cursor_position)
                                         .next()
                                         .and_then(|(character, _)| {
-                                            let text = character.to_string();
+                                            let text = SharedString::from(character.to_string());
+                                            let len = text.len();
                                             cx.text_system()
-                                                .layout_text(
-                                                    &text,
+                                                .shape_line(
+                                                    text,
                                                     cursor_row_layout.font_size,
                                                     &[TextRun {
-                                                        len: text.len(),
+                                                        len,
                                                         font: self.style.text.font(),
                                                         color: self.style.background,
+                                                        background_color: None,
                                                         underline: None,
                                                     }],
-                                                    None,
                                                 )
-                                                .unwrap()
-                                                .pop()
+                                                .log_err()
                                         })
                                 } else {
                                     None
@@ -1244,20 +1244,20 @@ impl EditorElement {
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let layout = cx
             .text_system()
-            .layout_text(
-                " ".repeat(column).as_str(),
+            .shape_line(
+                SharedString::from(" ".repeat(column)),
                 font_size,
                 &[TextRun {
                     len: column,
                     font: style.text.font(),
                     color: Hsla::default(),
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
             .unwrap();
 
-        layout[0].width
+        layout.width
     }
 
     fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
@@ -1338,7 +1338,7 @@ impl EditorElement {
         relative_rows
     }
 
-    fn layout_line_numbers(
+    fn shape_line_numbers(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
@@ -1347,12 +1347,12 @@ impl EditorElement {
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> (
-        Vec<Option<gpui::Line>>,
+        Vec<Option<ShapedLine>>,
         Vec<Option<(FoldStatus, BufferRow, bool)>>,
     ) {
         let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
         let include_line_numbers = snapshot.mode == EditorMode::Full;
-        let mut line_number_layouts = Vec::with_capacity(rows.len());
+        let mut shaped_line_numbers = Vec::with_capacity(rows.len());
         let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
         let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
@@ -1387,15 +1387,14 @@ impl EditorElement {
                         len: line_number.len(),
                         font: self.style.text.font(),
                         color,
+                        background_color: None,
                         underline: None,
                     };
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line_number, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
+                        .shape_line(line_number.clone().into(), font_size, &[run])
                         .unwrap();
-                    line_number_layouts.push(Some(layout));
+                    shaped_line_numbers.push(Some(shaped_line));
                     fold_statuses.push(
                         is_singleton
                             .then(|| {
@@ -1408,17 +1407,17 @@ impl EditorElement {
                 }
             } else {
                 fold_statuses.push(None);
-                line_number_layouts.push(None);
+                shaped_line_numbers.push(None);
             }
         }
 
-        (line_number_layouts, fold_statuses)
+        (shaped_line_numbers, fold_statuses)
     }
 
     fn layout_lines(
         &mut self,
         rows: Range<u32>,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> Vec<LineWithInvisibles> {
@@ -1439,18 +1438,17 @@ impl EditorElement {
                 .chain(iter::repeat(""))
                 .take(rows.len());
             placeholder_lines
-                .map(|line| {
+                .filter_map(move |line| {
                     let run = TextRun {
                         len: line.len(),
                         font: self.style.text.font(),
                         color: placeholder_color,
+                        background_color: None,
                         underline: Default::default(),
                     };
                     cx.text_system()
-                        .layout_text(line, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
-                        .unwrap()
+                        .shape_line(line.to_string().into(), font_size, &[run])
+                        .log_err()
                 })
                 .map(|line| LineWithInvisibles {
                     line,
@@ -1726,7 +1724,7 @@ impl EditorElement {
             .head
         });
 
-        let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+        let (line_numbers, fold_statuses) = self.shape_line_numbers(
             start_row..end_row,
             &active_rows,
             head_for_relative,
@@ -1740,8 +1738,7 @@ impl EditorElement {
         let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
 
         let mut max_visible_line_width = Pixels::ZERO;
-        let line_layouts =
-            self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+        let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
         for line_with_invisibles in &line_layouts {
             if line_with_invisibles.line.width > max_visible_line_width {
                 max_visible_line_width = line_with_invisibles.line.width;
@@ -1879,35 +1876,31 @@ impl EditorElement {
         let invisible_symbol_font_size = font_size / 2.;
         let tab_invisible = cx
             .text_system()
-            .layout_text(
-                "→",
+            .shape_line(
+                "→".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "→".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
         let space_invisible = cx
             .text_system()
-            .layout_text(
-                "•",
+            .shape_line(
+                "•".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "•".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
 
         LayoutState {
@@ -1939,7 +1932,7 @@ impl EditorElement {
             active_rows,
             highlighted_rows,
             highlighted_ranges,
-            line_number_layouts,
+            line_numbers,
             display_hunks,
             blocks,
             selections,
@@ -2201,7 +2194,7 @@ impl EditorElement {
 
 #[derive(Debug)]
 pub struct LineWithInvisibles {
-    pub line: Line,
+    pub line: ShapedLine,
     invisibles: Vec<Invisible>,
 }
 
@@ -2211,7 +2204,7 @@ impl LineWithInvisibles {
         text_style: &TextStyle,
         max_line_len: usize,
         max_line_count: usize,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         editor_mode: EditorMode,
         cx: &WindowContext,
     ) -> Vec<Self> {
@@ -2231,11 +2224,12 @@ impl LineWithInvisibles {
         }]) {
             for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
                 if ix > 0 {
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line, font_size, &styles, None);
+                        .shape_line(line.clone().into(), font_size, &styles)
+                        .unwrap();
                     layouts.push(Self {
-                        line: layout.unwrap().pop().unwrap(),
+                        line: shaped_line,
                         invisibles: invisibles.drain(..).collect(),
                     });
 
@@ -2269,6 +2263,7 @@ impl LineWithInvisibles {
                         len: line_chunk.len(),
                         font: text_style.font(),
                         color: text_style.color,
+                        background_color: None,
                         underline: text_style.underline,
                     });
 
@@ -3089,7 +3084,7 @@ pub struct LayoutState {
     visible_display_row_range: Range<u32>,
     active_rows: BTreeMap<u32, bool>,
     highlighted_rows: Option<Range<u32>>,
-    line_number_layouts: Vec<Option<gpui::Line>>,
+    line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3102,8 +3097,8 @@ pub struct LayoutState {
     code_actions_indicator: Option<CodeActionsIndicator>,
     // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
     fold_indicators: Vec<Option<AnyElement<Editor>>>,
-    tab_invisible: Line,
-    space_invisible: Line,
+    tab_invisible: ShapedLine,
+    space_invisible: ShapedLine,
 }
 
 struct CodeActionsIndicator {
@@ -3203,7 +3198,7 @@ fn layout_line(
     snapshot: &EditorSnapshot,
     style: &EditorStyle,
     cx: &WindowContext,
-) -> Result<Line> {
+) -> Result<ShapedLine> {
     let mut line = snapshot.line(row);
 
     if line.len() > MAX_LINE_LEN {
@@ -3215,21 +3210,17 @@ fn layout_line(
         line.truncate(len);
     }
 
-    Ok(cx
-        .text_system()
-        .layout_text(
-            &line,
-            style.text.font_size.to_pixels(cx.rem_size()),
-            &[TextRun {
-                len: snapshot.line_len(row) as usize,
-                font: style.text.font(),
-                color: Hsla::default(),
-                underline: None,
-            }],
-            None,
-        )?
-        .pop()
-        .unwrap())
+    cx.text_system().shape_line(
+        line.into(),
+        style.text.font_size.to_pixels(cx.rem_size()),
+        &[TextRun {
+            len: snapshot.line_len(row) as usize,
+            font: style.text.font(),
+            color: Hsla::default(),
+            background_color: None,
+            underline: None,
+        }],
+    )
 }
 
 #[derive(Debug)]
@@ -3239,7 +3230,7 @@ pub struct Cursor {
     line_height: Pixels,
     color: Hsla,
     shape: CursorShape,
-    block_text: Option<Line>,
+    block_text: Option<ShapedLine>,
 }
 
 impl Cursor {
@@ -3249,7 +3240,7 @@ impl Cursor {
         line_height: Pixels,
         color: Hsla,
         shape: CursorShape,
-        block_text: Option<Line>,
+        block_text: Option<ShapedLine>,
     ) -> Cursor {
         Cursor {
             origin,

crates/editor2/src/movement.rs 🔗

@@ -98,7 +98,7 @@ pub fn up_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -107,7 +107,7 @@ pub fn up_by_rows(
         Bias::Left,
     );
     if point.row() < start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_start {
         return (start, goal);
     } else {
@@ -137,18 +137,18 @@ pub fn down_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(),
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let new_row = start.row() + row_count;
     let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
     if point.row() > start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_end {
         return (start, goal);
     } else {
         point = map.max_point();
-        goal_x = map.x_for_point(point, text_layout_details)
+        goal_x = map.x_for_display_point(point, text_layout_details)
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Right);

crates/editor2/src/selections_collection.rs 🔗

@@ -313,14 +313,14 @@ impl SelectionsCollection {
         let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
 
-        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+        let line = display_map.layout_row(row, &text_layout_details);
 
         dbg!("****START COL****");
-        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
-        if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
+        let start_col = line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == line.width) {
             let start = DisplayPoint::new(row, start_col);
             dbg!("****END COL****");
-            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end_col = line.closest_index_for_x(positions.end) as u32;
             let end = DisplayPoint::new(row, end_col);
             dbg!(start_col, end_col);
 

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,9 +2,9 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
-    ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
+    ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -19,7 +19,7 @@ use text::Point;
 use theme::ActiveTheme;
 use ui::{v_stack, HighlightedLabel, StyledExt};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -111,10 +111,9 @@ impl FileFinder {
     }
 }
 
-impl EventEmitter<ModalEvent> for FileFinder {}
-impl Modal for FileFinder {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx))
+impl ManagedView for FileFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 impl Render for FileFinder {
@@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
                                 .log_err();
                         }
                     }
-                    finder
-                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
-                        .ok()?;
+                    finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
 
                     Some(())
                 })
@@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,13 +1,13 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
-    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
+    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -23,10 +23,9 @@ pub struct GoToLine {
     _subscriptions: Vec<Subscription>,
 }
 
-impl EventEmitter<ModalEvent> for GoToLine {}
-impl Modal for GoToLine {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.line_editor.update(cx, |editor, cx| editor.focus(cx))
+impl ManagedView for GoToLine {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.line_editor.focus_handle(cx)
     }
 }
 
@@ -88,7 +87,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
+            editor::Event::Blurred => cx.emit(Dismiss),
             editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +122,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +139,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 }
 

crates/gpui2/src/element.rs 🔗

@@ -13,7 +13,7 @@ pub trait Element<V: 'static> {
     fn layout(
         &mut self,
         view_state: &mut V,
-        previous_element_state: Option<Self::ElementState>,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> (LayoutId, Self::ElementState);
 

crates/gpui2/src/elements/div.rs 🔗

@@ -960,11 +960,11 @@ where
                             cx.background_executor().timer(TOOLTIP_DELAY).await;
                             view.update(&mut cx, move |view_state, cx| {
                                 active_tooltip.borrow_mut().replace(ActiveTooltip {
-                                    waiting: None,
                                     tooltip: Some(AnyTooltip {
                                         view: tooltip_builder(view_state, cx),
                                         cursor_offset: cx.mouse_position(),
                                     }),
+                                    _task: None,
                                 });
                                 cx.notify();
                             })
@@ -972,12 +972,17 @@ where
                         }
                     });
                     active_tooltip.borrow_mut().replace(ActiveTooltip {
-                        waiting: Some(task),
                         tooltip: None,
+                        _task: Some(task),
                     });
                 }
             });
 
+            let active_tooltip = element_state.active_tooltip.clone();
+            cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+                active_tooltip.borrow_mut().take();
+            });
+
             if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
                 if active_tooltip.tooltip.is_some() {
                     cx.active_tooltip = active_tooltip.tooltip.clone()
@@ -1207,9 +1212,8 @@ pub struct InteractiveElementState {
 }
 
 pub struct ActiveTooltip {
-    #[allow(unused)] // used to drop the task
-    waiting: Option<Task<()>>,
     tooltip: Option<AnyTooltip>,
+    _task: Option<Task<()>>,
 }
 
 /// Whether or not the element or a group that contains it is clicked by the mouse.

crates/gpui2/src/elements/overlay.rs 🔗

@@ -1,8 +1,9 @@
 use smallvec::SmallVec;
+use taffy::style::{Display, Position};
 
 use crate::{
-    point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point,
-    Size, Style,
+    point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
+    Point, Size, Style,
 };
 
 pub struct OverlayState {
@@ -14,7 +15,7 @@ pub struct Overlay<V> {
     anchor_corner: AnchorCorner,
     fit_mode: OverlayFitMode,
     // todo!();
-    // anchor_position: Option<Vector2F>,
+    anchor_position: Option<Point<Pixels>>,
     // position_mode: OverlayPositionMode,
 }
 
@@ -25,6 +26,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
         children: SmallVec::new(),
         anchor_corner: AnchorCorner::TopLeft,
         fit_mode: OverlayFitMode::SwitchAnchor,
+        anchor_position: None,
     }
 }
 
@@ -35,6 +37,13 @@ impl<V> Overlay<V> {
         self
     }
 
+    /// Sets the position in window co-ordinates
+    /// (otherwise the location the overlay is rendered is used)
+    pub fn position(mut self, anchor: Point<Pixels>) -> Self {
+        self.anchor_position = Some(anchor);
+        self
+    }
+
     /// Snap to window edge instead of switching anchor corner when an overflow would occur.
     pub fn snap_to_window(mut self) -> Self {
         self.fit_mode = OverlayFitMode::SnapToWindow;
@@ -48,6 +57,12 @@ impl<V: 'static> ParentComponent<V> for Overlay<V> {
     }
 }
 
+impl<V: 'static> Component<V> for Overlay<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
 impl<V: 'static> Element<V> for Overlay<V> {
     type ElementState = OverlayState;
 
@@ -66,7 +81,12 @@ impl<V: 'static> Element<V> for Overlay<V> {
             .iter_mut()
             .map(|child| child.layout(view_state, cx))
             .collect::<SmallVec<_>>();
-        let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied());
+
+        let mut overlay_style = Style::default();
+        overlay_style.position = Position::Absolute;
+        overlay_style.display = Display::Flex;
+
+        let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
 
         (layout_id, OverlayState { child_layout_ids })
     }
@@ -90,7 +110,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
             child_max = child_max.max(&child_bounds.lower_right());
         }
         let size: Size<Pixels> = (child_max - child_min).into();
-        let origin = bounds.origin;
+        let origin = self.anchor_position.unwrap_or(bounds.origin);
 
         let mut desired = self.anchor_corner.get_bounds(origin, size);
         let limits = Bounds {
@@ -184,6 +204,15 @@ impl AnchorCorner {
         Bounds { origin, size }
     }
 
+    pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
+        match self {
+            Self::TopLeft => bounds.origin,
+            Self::TopRight => bounds.upper_right(),
+            Self::BottomLeft => bounds.lower_left(),
+            Self::BottomRight => bounds.lower_right(),
+        }
+    }
+
     fn switch_axis(self, axis: Axis) -> Self {
         match axis {
             Axis::Vertical => match self {

crates/gpui2/src/elements/text.rs 🔗

@@ -1,76 +1,39 @@
 use crate::{
-    AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
-    Size, TextRun, ViewContext,
+    AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
+    SharedString, Size, TextRun, ViewContext, WrappedLine,
 };
-use parking_lot::Mutex;
+use parking_lot::{Mutex, MutexGuard};
 use smallvec::SmallVec;
-use std::{marker::PhantomData, sync::Arc};
+use std::{cell::Cell, rc::Rc, sync::Arc};
 use util::ResultExt;
 
-impl<V: 'static> Component<V> for SharedString {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self,
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-impl<V: 'static> Component<V> for &'static str {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-pub struct Text<V> {
+pub struct Text {
     text: SharedString,
     runs: Option<Vec<TextRun>>,
-    state_type: PhantomData<V>,
 }
 
-impl<V: 'static> Text<V> {
-    /// styled renders text that has different runs of different styles.
-    /// callers are responsible for setting the correct style for each run.
-    ////
-    /// For uniform text you can usually just pass a string as a child, and
-    /// cx.text_style() will be used automatically.
+impl Text {
+    /// Renders text with runs of different styles.
+    ///
+    /// Callers are responsible for setting the correct style for each run.
+    /// For text with a uniform style, you can usually avoid calling this constructor
+    /// and just pass text directly.
     pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
         Text {
             text,
             runs: Some(runs),
-            state_type: Default::default(),
         }
     }
 }
 
-impl<V: 'static> Component<V> for Text<V> {
+impl<V: 'static> Component<V> for Text {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V: 'static> Element<V> for Text<V> {
-    type ElementState = Arc<Mutex<Option<TextElementState>>>;
+impl<V: 'static> Element<V> for Text {
+    type ElementState = TextState;
 
     fn element_id(&self) -> Option<crate::ElementId> {
         None
@@ -103,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
                 let Some(lines) = text_system
-                    .layout_text(
+                    .shape_text(
                         &text,
                         font_size,
                         &runs[..],
@@ -111,30 +74,23 @@ impl<V: 'static> Element<V> for Text<V> {
                     )
                     .log_err()
                 else {
-                    element_state.lock().replace(TextElementState {
+                    element_state.lock().replace(TextStateInner {
                         lines: Default::default(),
                         line_height,
                     });
                     return Size::default();
                 };
 
-                let line_count = lines
-                    .iter()
-                    .map(|line| line.wrap_count() + 1)
-                    .sum::<usize>();
-                let size = Size {
-                    width: lines
-                        .iter()
-                        .map(|line| line.layout.width)
-                        .max()
-                        .unwrap()
-                        .ceil(),
-                    height: line_height * line_count,
-                };
+                let mut size: Size<Pixels> = Size::default();
+                for line in &lines {
+                    let line_size = line.size(line_height);
+                    size.height += line_size.height;
+                    size.width = size.width.max(line_size.width);
+                }
 
                 element_state
                     .lock()
-                    .replace(TextElementState { lines, line_height });
+                    .replace(TextStateInner { lines, line_height });
 
                 size
             }
@@ -165,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
     }
 }
 
-pub struct TextElementState {
-    lines: SmallVec<[Line; 1]>,
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+impl TextState {
+    fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+        self.0.lock()
+    }
+}
+
+struct TextStateInner {
+    lines: SmallVec<[WrappedLine; 1]>,
     line_height: Pixels,
 }
+
+struct InteractiveText {
+    id: ElementId,
+    text: Text,
+}
+
+struct InteractiveTextState {
+    text_state: TextState,
+    clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+}
+
+impl<V: 'static> Element<V> for InteractiveText {
+    type ElementState = InteractiveTextState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> (LayoutId, Self::ElementState) {
+        if let Some(InteractiveTextState {
+            text_state,
+            clicked_range_ixs,
+        }) = element_state
+        {
+            let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs,
+            };
+            (layout_id, element_state)
+        } else {
+            let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs: Rc::default(),
+            };
+            (layout_id, element_state)
+        }
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.text
+            .paint(bounds, view_state, &mut element_state.text_state, cx)
+    }
+}
+
+impl<V: 'static> Component<V> for SharedString {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self,
+            runs: None,
+        }
+        .render()
+    }
+}
+
+impl<V: 'static> Component<V> for &'static str {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}
+
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<V: 'static> Component<V> for String {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}

crates/gpui2/src/platform/mac/text_system.rs 🔗

@@ -343,10 +343,10 @@ impl MacTextSystemState {
         // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
         let mut string = CFMutableAttributedString::new();
         {
-            string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+            string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
             let utf16_line_len = string.char_len() as usize;
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             for run in font_runs {
                 let utf8_end = ix_converter.utf8_ix + run.len;
                 let utf16_start = ix_converter.utf16_ix;
@@ -390,7 +390,7 @@ impl MacTextSystemState {
             };
             let font_id = self.id_for_native_font(font);
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             let mut glyphs = SmallVec::new();
             for ((glyph_id, position), glyph_utf16_ix) in run
                 .glyphs()
@@ -413,11 +413,11 @@ impl MacTextSystemState {
 
         let typographic_bounds = line.get_typographic_bounds();
         LineLayout {
+            runs,
+            font_size,
             width: typographic_bounds.width.into(),
             ascent: typographic_bounds.ascent.into(),
             descent: typographic_bounds.descent.into(),
-            runs,
-            font_size,
             len: text.len(),
         }
     }

crates/gpui2/src/platform/mac/window.rs 🔗

@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
 
     if let Some(mut event) = event {
-        let synthesized_second_event = match &mut event {
+        match &mut event {
             InputEvent::MouseDown(
                 event @ MouseDownEvent {
                     button: MouseButton::Left,
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     ..
                 },
             ) => {
+                // On mac, a ctrl-left click should be handled as a right click.
                 *event = MouseDownEvent {
                     button: MouseButton::Right,
                     modifiers: Modifiers {
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     click_count: 1,
                     ..*event
                 };
-
-                Some(InputEvent::MouseDown(MouseDownEvent {
-                    button: MouseButton::Right,
-                    ..*event
-                }))
             }
 
             // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
             // the ctrl-left_up to avoid having a mismatch in button down/up events if the
             // user is still holding ctrl when releasing the left mouse button
-            InputEvent::MouseUp(MouseUpEvent {
-                button: MouseButton::Left,
-                modifiers: Modifiers { control: true, .. },
-                ..
-            }) => {
-                lock.synthetic_drag_counter += 1;
-                return;
+            InputEvent::MouseUp(
+                event @ MouseUpEvent {
+                    button: MouseButton::Left,
+                    modifiers: Modifiers { control: true, .. },
+                    ..
+                },
+            ) => {
+                *event = MouseUpEvent {
+                    button: MouseButton::Right,
+                    modifiers: Modifiers {
+                        control: false,
+                        ..event.modifiers
+                    },
+                    click_count: 1,
+                    ..*event
+                };
             }
 
-            _ => None,
+            _ => {}
         };
 
         match &event {
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         if let Some(mut callback) = lock.event_callback.take() {
             drop(lock);
             callback(event);
-            if let Some(event) = synthesized_second_event {
-                callback(event);
-            }
             window_state.lock().event_callback = Some(callback);
         }
     }

crates/gpui2/src/style.rs 🔗

@@ -203,6 +203,7 @@ impl TextStyle {
                 style: self.font_style,
             },
             color: self.color,
+            background_color: None,
             underline: self.underline.clone(),
         }
     }

crates/gpui2/src/text_system.rs 🔗

@@ -3,20 +3,20 @@ mod line;
 mod line_layout;
 mod line_wrapper;
 
-use anyhow::anyhow;
 pub use font_features::*;
 pub use line::*;
 pub use line_layout::*;
 pub use line_wrapper::*;
-use smallvec::SmallVec;
 
 use crate::{
     px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
     UnderlineStyle,
 };
+use anyhow::anyhow;
 use collections::HashMap;
 use core::fmt;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
 use std::{
     cmp,
     fmt::{Debug, Display, Formatter},
@@ -151,13 +151,79 @@ impl TextSystem {
         }
     }
 
-    pub fn layout_text(
+    pub fn layout_line(
         &self,
         text: &str,
         font_size: Pixels,
         runs: &[TextRun],
+    ) -> Result<Arc<LineLayout>> {
+        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+        for run in runs.iter() {
+            let font_id = self.font_id(&run.font)?;
+            if let Some(last_run) = font_runs.last_mut() {
+                if last_run.font_id == font_id {
+                    last_run.len += run.len;
+                    continue;
+                }
+            }
+            font_runs.push(FontRun {
+                len: run.len,
+                font_id,
+            });
+        }
+
+        let layout = self
+            .line_layout_cache
+            .layout_line(&text, font_size, &font_runs);
+
+        font_runs.clear();
+        self.font_runs_pool.lock().push(font_runs);
+
+        Ok(layout)
+    }
+
+    pub fn shape_line(
+        &self,
+        text: SharedString,
+        font_size: Pixels,
+        runs: &[TextRun],
+    ) -> Result<ShapedLine> {
+        debug_assert!(
+            text.find('\n').is_none(),
+            "text argument should not contain newlines"
+        );
+
+        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
+        for run in runs {
+            if let Some(last_run) = decoration_runs.last_mut() {
+                if last_run.color == run.color && last_run.underline == run.underline {
+                    last_run.len += run.len as u32;
+                    continue;
+                }
+            }
+            decoration_runs.push(DecorationRun {
+                len: run.len as u32,
+                color: run.color,
+                underline: run.underline.clone(),
+            });
+        }
+
+        let layout = self.layout_line(text.as_ref(), font_size, runs)?;
+
+        Ok(ShapedLine {
+            layout,
+            text,
+            decoration_runs,
+        })
+    }
+
+    pub fn shape_text(
+        &self,
+        text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
+        font_size: Pixels,
+        runs: &[TextRun],
         wrap_width: Option<Pixels>,
-    ) -> Result<SmallVec<[Line; 1]>> {
+    ) -> Result<SmallVec<[WrappedLine; 1]>> {
         let mut runs = runs.iter().cloned().peekable();
         let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
 
@@ -210,10 +276,11 @@ impl TextSystem {
 
             let layout = self
                 .line_layout_cache
-                .layout_line(&line_text, font_size, &font_runs, wrap_width);
-            lines.push(Line {
+                .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
+            lines.push(WrappedLine {
                 layout,
-                decorations: decoration_runs,
+                decoration_runs,
+                text: SharedString::from(line_text),
             });
 
             line_start = line_end + 1; // Skip `\n` character.
@@ -384,6 +451,7 @@ pub struct TextRun {
     pub len: usize,
     pub font: Font,
     pub color: Hsla,
+    pub background_color: Option<Hsla>,
     pub underline: Option<UnderlineStyle>,
 }
 

crates/gpui2/src/text_system/line.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
+    black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
     UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
@@ -14,23 +14,51 @@ pub struct DecorationRun {
 }
 
 #[derive(Clone, Default, Debug, Deref, DerefMut)]
-pub struct Line {
+pub struct ShapedLine {
     #[deref]
     #[deref_mut]
-    pub(crate) layout: Arc<WrappedLineLayout>,
-    pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
+    pub(crate) layout: Arc<LineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 }
 
-impl Line {
-    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
-        size(
-            self.layout.width,
-            line_height * (self.layout.wrap_boundaries.len() + 1),
-        )
+impl ShapedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len
     }
 
-    pub fn wrap_count(&self) -> usize {
-        self.layout.wrap_boundaries.len()
+    pub fn paint(
+        &self,
+        origin: Point<Pixels>,
+        line_height: Pixels,
+        cx: &mut WindowContext,
+    ) -> Result<()> {
+        paint_line(
+            origin,
+            &self.layout,
+            line_height,
+            &self.decoration_runs,
+            None,
+            &[],
+            cx,
+        )?;
+
+        Ok(())
+    }
+}
+
+#[derive(Clone, Default, Debug, Deref, DerefMut)]
+pub struct WrappedLine {
+    #[deref]
+    #[deref_mut]
+    pub(crate) layout: Arc<WrappedLineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
+}
+
+impl WrappedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len()
     }
 
     pub fn paint(
@@ -39,75 +67,50 @@ impl Line {
         line_height: Pixels,
         cx: &mut WindowContext,
     ) -> Result<()> {
-        let padding_top =
-            (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
-        let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
-
-        let mut style_runs = self.decorations.iter();
-        let mut wraps = self.layout.wrap_boundaries.iter().peekable();
-        let mut run_end = 0;
-        let mut color = black();
-        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-        let text_system = cx.text_system().clone();
-
-        let mut glyph_origin = origin;
-        let mut prev_glyph_position = Point::default();
-        for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
-            let max_glyph_size = text_system
-                .bounding_box(run.font_id, self.layout.layout.font_size)?
-                .size;
-
-            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
-                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
-
-                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
-                    wraps.next();
-                    if let Some((underline_origin, underline_style)) = current_underline.take() {
-                        cx.paint_underline(
-                            underline_origin,
-                            glyph_origin.x - underline_origin.x,
-                            &underline_style,
-                        )?;
-                    }
-
-                    glyph_origin.x = origin.x;
-                    glyph_origin.y += line_height;
-                }
-                prev_glyph_position = glyph.position;
-
-                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-                if glyph.index >= run_end {
-                    if let Some(style_run) = style_runs.next() {
-                        if let Some((_, underline_style)) = &mut current_underline {
-                            if style_run.underline.as_ref() != Some(underline_style) {
-                                finished_underline = current_underline.take();
-                            }
-                        }
-                        if let Some(run_underline) = style_run.underline.as_ref() {
-                            current_underline.get_or_insert((
-                                point(
-                                    glyph_origin.x,
-                                    origin.y
-                                        + baseline_offset.y
-                                        + (self.layout.layout.descent * 0.618),
-                                ),
-                                UnderlineStyle {
-                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
-                                    thickness: run_underline.thickness,
-                                    wavy: run_underline.wavy,
-                                },
-                            ));
-                        }
+        paint_line(
+            origin,
+            &self.layout.unwrapped_layout,
+            line_height,
+            &self.decoration_runs,
+            self.wrap_width,
+            &self.wrap_boundaries,
+            cx,
+        )?;
 
-                        run_end += style_run.len as usize;
-                        color = style_run.color;
-                    } else {
-                        run_end = self.layout.text.len();
-                        finished_underline = current_underline.take();
-                    }
-                }
+        Ok(())
+    }
+}
 
-                if let Some((underline_origin, underline_style)) = finished_underline {
+fn paint_line(
+    origin: Point<Pixels>,
+    layout: &LineLayout,
+    line_height: Pixels,
+    decoration_runs: &[DecorationRun],
+    wrap_width: Option<Pixels>,
+    wrap_boundaries: &[WrapBoundary],
+    cx: &mut WindowContext<'_>,
+) -> Result<()> {
+    let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
+    let baseline_offset = point(px(0.), padding_top + layout.ascent);
+    let mut decoration_runs = decoration_runs.iter();
+    let mut wraps = wrap_boundaries.iter().peekable();
+    let mut run_end = 0;
+    let mut color = black();
+    let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+    let text_system = cx.text_system().clone();
+    let mut glyph_origin = origin;
+    let mut prev_glyph_position = Point::default();
+    for (run_ix, run) in layout.runs.iter().enumerate() {
+        let max_glyph_size = text_system
+            .bounding_box(run.font_id, layout.font_size)?
+            .size;
+
+        for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+            glyph_origin.x += glyph.position.x - prev_glyph_position.x;
+
+            if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
+                wraps.next();
+                if let Some((underline_origin, underline_style)) = current_underline.take() {
                     cx.paint_underline(
                         underline_origin,
                         glyph_origin.x - underline_origin.x,
@@ -115,42 +118,84 @@ impl Line {
                     )?;
                 }
 
-                let max_glyph_bounds = Bounds {
-                    origin: glyph_origin,
-                    size: max_glyph_size,
-                };
-
-                let content_mask = cx.content_mask();
-                if max_glyph_bounds.intersects(&content_mask.bounds) {
-                    if glyph.is_emoji {
-                        cx.paint_emoji(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                        )?;
-                    } else {
-                        cx.paint_glyph(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                            color,
-                        )?;
+                glyph_origin.x = origin.x;
+                glyph_origin.y += line_height;
+            }
+            prev_glyph_position = glyph.position;
+
+            let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+            if glyph.index >= run_end {
+                if let Some(style_run) = decoration_runs.next() {
+                    if let Some((_, underline_style)) = &mut current_underline {
+                        if style_run.underline.as_ref() != Some(underline_style) {
+                            finished_underline = current_underline.take();
+                        }
                     }
+                    if let Some(run_underline) = style_run.underline.as_ref() {
+                        current_underline.get_or_insert((
+                            point(
+                                glyph_origin.x,
+                                origin.y + baseline_offset.y + (layout.descent * 0.618),
+                            ),
+                            UnderlineStyle {
+                                color: Some(run_underline.color.unwrap_or(style_run.color)),
+                                thickness: run_underline.thickness,
+                                wavy: run_underline.wavy,
+                            },
+                        ));
+                    }
+
+                    run_end += style_run.len as usize;
+                    color = style_run.color;
+                } else {
+                    run_end = layout.len;
+                    finished_underline = current_underline.take();
                 }
             }
-        }
 
-        if let Some((underline_start, underline_style)) = current_underline.take() {
-            let line_end_x = origin.x + self.layout.layout.width;
-            cx.paint_underline(
-                underline_start,
-                line_end_x - underline_start.x,
-                &underline_style,
-            )?;
+            if let Some((underline_origin, underline_style)) = finished_underline {
+                cx.paint_underline(
+                    underline_origin,
+                    glyph_origin.x - underline_origin.x,
+                    &underline_style,
+                )?;
+            }
+
+            let max_glyph_bounds = Bounds {
+                origin: glyph_origin,
+                size: max_glyph_size,
+            };
+
+            let content_mask = cx.content_mask();
+            if max_glyph_bounds.intersects(&content_mask.bounds) {
+                if glyph.is_emoji {
+                    cx.paint_emoji(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                    )?;
+                } else {
+                    cx.paint_glyph(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                        color,
+                    )?;
+                }
+            }
         }
+    }
 
-        Ok(())
+    if let Some((underline_start, underline_style)) = current_underline.take() {
+        let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
+        cx.paint_underline(
+            underline_start,
+            line_end_x - underline_start.x,
+            &underline_style,
+        )?;
     }
+
+    Ok(())
 }

crates/gpui2/src/text_system/line_layout.rs 🔗

@@ -1,5 +1,4 @@
-use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
-use derive_more::{Deref, DerefMut};
+use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
 use std::{
@@ -149,13 +148,11 @@ impl LineLayout {
     }
 }
 
-#[derive(Deref, DerefMut, Default, Debug)]
+#[derive(Default, Debug)]
 pub struct WrappedLineLayout {
-    #[deref]
-    #[deref_mut]
-    pub layout: LineLayout,
-    pub text: SharedString,
+    pub unwrapped_layout: Arc<LineLayout>,
     pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
+    pub wrap_width: Option<Pixels>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
     pub glyph_ix: usize,
 }
 
+impl WrappedLineLayout {
+    pub fn len(&self) -> usize {
+        self.unwrapped_layout.len
+    }
+
+    pub fn width(&self) -> Pixels {
+        self.wrap_width
+            .unwrap_or(Pixels::MAX)
+            .min(self.unwrapped_layout.width)
+    }
+
+    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
+        Size {
+            width: self.width(),
+            height: line_height * (self.wrap_boundaries.len() + 1),
+        }
+    }
+
+    pub fn ascent(&self) -> Pixels {
+        self.unwrapped_layout.ascent
+    }
+
+    pub fn descent(&self) -> Pixels {
+        self.unwrapped_layout.descent
+    }
+
+    pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
+        &self.wrap_boundaries
+    }
+
+    pub fn font_size(&self) -> Pixels {
+        self.unwrapped_layout.font_size
+    }
+
+    pub fn runs(&self) -> &[ShapedRun] {
+        &self.unwrapped_layout.runs
+    }
+}
+
 pub(crate) struct LineLayoutCache {
-    prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
-    curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
+    current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
+    previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
     platform_text_system: Arc<dyn PlatformTextSystem>,
 }
 
 impl LineLayoutCache {
     pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         Self {
-            prev_frame: Mutex::new(HashMap::new()),
-            curr_frame: RwLock::new(HashMap::new()),
+            previous_frame: Mutex::default(),
+            current_frame: RwLock::default(),
+            previous_frame_wrapped: Mutex::default(),
+            current_frame_wrapped: RwLock::default(),
             platform_text_system,
         }
     }
 
     pub fn start_frame(&self) {
-        let mut prev_frame = self.prev_frame.lock();
-        let mut curr_frame = self.curr_frame.write();
+        let mut prev_frame = self.previous_frame.lock();
+        let mut curr_frame = self.current_frame.write();
         std::mem::swap(&mut *prev_frame, &mut *curr_frame);
         curr_frame.clear();
     }
 
-    pub fn layout_line(
+    pub fn layout_wrapped_line(
         &self,
-        text: &SharedString,
+        text: &str,
         font_size: Pixels,
         runs: &[FontRun],
         wrap_width: Option<Pixels>,
@@ -199,34 +239,66 @@ impl LineLayoutCache {
             runs,
             wrap_width,
         } as &dyn AsCacheKeyRef;
-        let curr_frame = self.curr_frame.upgradable_read();
-        if let Some(layout) = curr_frame.get(key) {
+
+        let current_frame = self.current_frame_wrapped.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
             return layout.clone();
         }
 
-        let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
-        if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
-            curr_frame.insert(key, layout.clone());
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
             layout
         } else {
-            let layout = self.platform_text_system.layout_line(text, font_size, runs);
-            let wrap_boundaries = wrap_width
-                .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
-                .unwrap_or_default();
-            let wrapped_line = Arc::new(WrappedLineLayout {
-                layout,
-                text: text.clone(),
+            let unwrapped_layout = self.layout_line(text, font_size, runs);
+            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
+                unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
+            } else {
+                SmallVec::new()
+            };
+            let layout = Arc::new(WrappedLineLayout {
+                unwrapped_layout,
                 wrap_boundaries,
+                wrap_width,
             });
-
             let key = CacheKey {
-                text: text.clone(),
+                text: text.into(),
                 font_size,
                 runs: SmallVec::from(runs),
                 wrap_width,
             };
-            curr_frame.insert(key, wrapped_line.clone());
-            wrapped_line
+            current_frame.insert(key, layout.clone());
+            layout
+        }
+    }
+
+    pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
+        let key = &CacheKeyRef {
+            text,
+            font_size,
+            runs,
+            wrap_width: None,
+        } as &dyn AsCacheKeyRef;
+
+        let current_frame = self.current_frame.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
+            return layout.clone();
+        }
+
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
+            layout
+        } else {
+            let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
+            let key = CacheKey {
+                text: text.into(),
+                font_size,
+                runs: SmallVec::from(runs),
+                wrap_width: None,
+            };
+            current_frame.insert(key, layout.clone());
+            layout
         }
     }
 }
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
 
 #[derive(Eq)]
 struct CacheKey {
-    text: SharedString,
+    text: String,
     font_size: Pixels,
     runs: SmallVec<[FontRun; 1]>,
     wrap_width: Option<Pixels>,

crates/gpui2/src/window.rs 🔗

@@ -185,10 +185,27 @@ impl Drop for FocusHandle {
     }
 }
 
+/// FocusableView allows users of your view to easily
+/// focus it (using cx.focus_view(view))
 pub trait FocusableView: Render {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
 }
 
+/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
+/// where the lifecycle of the view is handled by another view.
+pub trait ManagedView: Render {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+}
+
+pub struct Dismiss;
+impl<T: ManagedView> EventEmitter<Dismiss> for T {}
+
+impl<T: ManagedView> FocusableView for T {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.focus_handle(cx)
+    }
+}
+
 // Holds the state for a specific window.
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
@@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    #[must_use]
     /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
     /// layout is being requested, along with the layout ids of any children. This method is called during
     /// calls to the `Element::layout` trait method and enables any element to participate in layout.
@@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> {
                 self.window.mouse_position = mouse_move.position;
                 InputEvent::MouseMove(mouse_move)
             }
+            InputEvent::MouseDown(mouse_down) => {
+                self.window.mouse_position = mouse_down.position;
+                InputEvent::MouseDown(mouse_down)
+            }
+            InputEvent::MouseUp(mouse_up) => {
+                self.window.mouse_position = mouse_up.position;
+                InputEvent::MouseUp(mouse_up)
+            }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {

crates/picker2/src/picker2.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{
-    div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
-    UniformListScrollHandle, View, ViewContext, WindowContext,
+    div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
+    MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Divider, Label, TextColor};
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
     ) -> Self::ListItem;
 }
 
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
         let editor = cx.build_view(|cx| {

crates/storybook2/src/storybook2.rs 🔗

@@ -66,7 +66,6 @@ fn main() {
             story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
 
         let theme_registry = cx.global::<ThemeRegistry>();
-
         let mut theme_settings = ThemeSettings::get_global(cx).clone();
         theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
         ThemeSettings::override_global(theme_settings, cx);
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
             .flex()
             .flex_col()
             .size_full()
+            .font("Zed Mono")
             .child(self.story.clone())
     }
 }

crates/storybook3/Cargo.toml 🔗

@@ -0,0 +1,17 @@
+[package]
+name = "storybook3"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook3.rs"
+
+[dependencies]
+anyhow.workspace = true
+
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2", features = ["stories"] }
+theme = { package = "theme2", path = "../theme2", features = ["stories"] }
+settings = { package = "settings2", path = "../settings2"}

crates/storybook3/src/storybook3.rs 🔗

@@ -0,0 +1,73 @@
+use anyhow::Result;
+use gpui::AssetSource;
+use gpui::{
+    div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
+    WindowOptions,
+};
+use settings::{default_settings, Settings, SettingsStore};
+use std::borrow::Cow;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{prelude::*, ContextMenuStory};
+
+struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
+        todo!();
+    }
+
+    fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
+        Ok(vec![])
+    }
+}
+
+fn main() {
+    let asset_source = Arc::new(Assets);
+    gpui::App::production(asset_source).run(move |cx| {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        ui::settings::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
+
+        cx.open_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(Bounds {
+                    origin: Default::default(),
+                    size: size(px(1500.), px(780.)).into(),
+                }),
+                ..Default::default()
+            },
+            move |cx| {
+                let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
+                cx.set_rem_size(ui_font_size);
+
+                cx.build_view(|cx| TestView {
+                    story: cx.build_view(|_| ContextMenuStory).into(),
+                })
+            },
+        );
+
+        cx.activate(true);
+    })
+}
+
+struct TestView {
+    story: AnyView,
+}
+
+impl Render for TestView {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .font("Helvetica")
+            .child(self.story.clone())
+    }
+}

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -32,7 +32,7 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem},
-    ui::{ContextMenu, ContextMenuItem, Label},
+    ui::{ContextMenu, Label},
     CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
@@ -85,7 +85,7 @@ pub struct TerminalView {
     has_new_content: bool,
     //Currently using iTerm bell, show bell emoji in tab until input is received
     has_bell: bool,
-    context_menu: Option<ContextMenu>,
+    context_menu: Option<View<ContextMenu>>,
     blink_state: bool,
     blinking_on: bool,
     blinking_paused: bool,
@@ -300,10 +300,14 @@ impl TerminalView {
         position: gpui::Point<Pixels>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.context_menu = Some(ContextMenu::new(vec![
-            ContextMenuItem::entry(Label::new("Clear"), Clear),
-            ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }),
-        ]));
+        self.context_menu = Some(cx.build_view(|cx| {
+            ContextMenu::new(cx)
+                .entry(Label::new("Clear"), Box::new(Clear))
+                .entry(
+                    Label::new("Close"),
+                    Box::new(CloseActiveItem { save_intent: None }),
+                )
+        }));
         dbg!(&position);
         // todo!()
         //     self.context_menu

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

@@ -1,82 +1,258 @@
-use crate::{prelude::*, ListItemVariant};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::prelude::*;
 use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use gpui::{
+    overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
+    FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+};
 
-pub enum ContextMenuItem {
-    Header(SharedString),
-    Entry(Label, Box<dyn gpui::Action>),
-    Separator,
+pub struct ContextMenu {
+    items: Vec<ListItem>,
+    focus_handle: FocusHandle,
 }
 
-impl Clone for ContextMenuItem {
-    fn clone(&self) -> Self {
-        match self {
-            ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()),
-            ContextMenuItem::Entry(label, action) => {
-                ContextMenuItem::Entry(label.clone(), action.boxed_clone())
-            }
-            ContextMenuItem::Separator => ContextMenuItem::Separator,
-        }
+impl ManagedView for ContextMenu {
+    fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+        self.focus_handle.clone()
     }
 }
-impl ContextMenuItem {
-    fn to_list_item<V: 'static>(self) -> ListItem {
-        match self {
-            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
-            ContextMenuItem::Entry(label, action) => ListEntry::new(label)
-                .variant(ListItemVariant::Inset)
-                .on_click(action)
-                .into(),
-            ContextMenuItem::Separator => ListSeparator::new().into(),
+
+impl ContextMenu {
+    pub fn new(cx: &mut WindowContext) -> Self {
+        Self {
+            items: Default::default(),
+            focus_handle: cx.focus_handle(),
         }
     }
 
-    pub fn header(label: impl Into<SharedString>) -> Self {
-        Self::Header(label.into())
+    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
+        self.items.push(ListItem::Header(ListSubHeader::new(title)));
+        self
+    }
+
+    pub fn separator(mut self) -> Self {
+        self.items.push(ListItem::Separator(ListSeparator));
+        self
+    }
+
+    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
+        self.items.push(ListEntry::new(label).action(action).into());
+        self
     }
 
-    pub fn separator() -> Self {
-        Self::Separator
+    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        // todo!()
+        cx.emit(Dismiss);
     }
 
-    pub fn entry(label: Label, action: impl Action) -> Self {
-        Self::Entry(label, Box::new(action))
+    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(Dismiss);
     }
 }
 
-#[derive(Component, Clone)]
-pub struct ContextMenu {
-    items: Vec<ContextMenuItem>,
+impl Render for ContextMenu {
+    type Element = Div<Self>;
+    // todo!()
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().elevation_2(cx).flex().flex_row().child(
+            v_stack()
+                .min_w(px(200.))
+                .track_focus(&self.focus_handle)
+                .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+                // .on_action(ContextMenu::select_first)
+                // .on_action(ContextMenu::select_last)
+                // .on_action(ContextMenu::select_next)
+                // .on_action(ContextMenu::select_prev)
+                .on_action(ContextMenu::confirm)
+                .on_action(ContextMenu::cancel)
+                .flex_none()
+                // .bg(cx.theme().colors().elevated_surface_background)
+                // .border()
+                // .border_color(cx.theme().colors().border)
+                .child(List::new(self.items.clone())),
+        )
+    }
 }
 
-impl ContextMenu {
-    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
-        Self {
-            items: items.into_iter().collect(),
+pub struct MenuHandle<V: 'static, M: ManagedView> {
+    id: Option<ElementId>,
+    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
+    menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
+
+    anchor: Option<AnchorCorner>,
+    attach: Option<AnchorCorner>,
+}
+
+impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
+    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
+        self.id = Some(id.into());
+        self
+    }
+
+    pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+        self.menu_builder = Some(Rc::new(f));
+        self
+    }
+
+    pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+        self.child_builder = Some(Box::new(|b| f(b).render()));
+        self
+    }
+
+    /// anchor defines which corner of the menu to anchor to the attachment point
+    /// (by default the cursor position, but see attach)
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor = Some(anchor);
+        self
+    }
+
+    /// attach defines which corner of the handle to attach the menu's anchor to
+    pub fn attach(mut self, attach: AnchorCorner) -> Self {
+        self.attach = Some(attach);
+        self
+    }
+}
+
+pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+    MenuHandle {
+        id: None,
+        child_builder: None,
+        menu_builder: None,
+        anchor: None,
+        attach: None,
+    }
+}
+
+pub struct MenuHandleState<V, M> {
+    menu: Rc<RefCell<Option<View<M>>>>,
+    position: Rc<RefCell<Point<Pixels>>>,
+    child_layout_id: Option<LayoutId>,
+    child_element: Option<AnyElement<V>>,
+    menu_element: Option<AnyElement<V>>,
+}
+impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
+    type ElementState = MenuHandleState<V, M>;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone().expect("menu_handle must have an id()"))
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut crate::ViewContext<V>,
+    ) -> (gpui::LayoutId, Self::ElementState) {
+        let (menu, position) = if let Some(element_state) = element_state {
+            (element_state.menu, element_state.position)
+        } else {
+            (Rc::default(), Rc::default())
+        };
+
+        let mut menu_layout_id = None;
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut overlay = overlay::<V>().snap_to_window();
+            if let Some(anchor) = self.anchor {
+                overlay = overlay.anchor(anchor);
+            }
+            overlay = overlay.position(*position.borrow());
+
+            let mut view = overlay.child(menu.clone()).render();
+            menu_layout_id = Some(view.layout(view_state, cx));
+            view
+        });
+
+        let mut child_element = self
+            .child_builder
+            .take()
+            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+        let child_layout_id = child_element
+            .as_mut()
+            .map(|child_element| child_element.layout(view_state, cx));
+
+        let layout_id = cx.request_layout(
+            &gpui::Style::default(),
+            menu_layout_id.into_iter().chain(child_layout_id),
+        );
+
+        (
+            layout_id,
+            MenuHandleState {
+                menu,
+                position,
+                child_element,
+                child_layout_id,
+                menu_element,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<gpui::Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut crate::ViewContext<V>,
+    ) {
+        if let Some(child) = element_state.child_element.as_mut() {
+            child.paint(view_state, cx);
         }
+
+        if let Some(menu) = element_state.menu_element.as_mut() {
+            menu.paint(view_state, cx);
+            return;
+        }
+
+        let Some(builder) = self.menu_builder.clone() else {
+            return;
+        };
+        let menu = element_state.menu.clone();
+        let position = element_state.position.clone();
+        let attach = self.attach.clone();
+        let child_layout_id = element_state.child_layout_id.clone();
+
+        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble
+                && event.button == MouseButton::Right
+                && bounds.contains_point(&event.position)
+            {
+                cx.stop_propagation();
+                cx.prevent_default();
+
+                let new_menu = (builder)(view_state, cx);
+                let menu2 = menu.clone();
+                cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
+                    &Dismiss => {
+                        *menu2.borrow_mut() = None;
+                        cx.notify();
+                    }
+                })
+                .detach();
+                *menu.borrow_mut() = Some(new_menu);
+
+                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+                    attach
+                        .unwrap()
+                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
+                } else {
+                    cx.mouse_position()
+                };
+                cx.notify();
+            }
+        });
     }
-    // todo!()
-    // cx.add_action(ContextMenu::select_first);
-    // cx.add_action(ContextMenu::select_last);
-    // cx.add_action(ContextMenu::select_next);
-    // cx.add_action(ContextMenu::select_prev);
-    // cx.add_action(ContextMenu::confirm);
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        v_stack()
-            .flex()
-            .bg(cx.theme().colors().elevated_surface_background)
-            .border()
-            .border_color(cx.theme().colors().border)
-            .child(List::new(
-                self.items
-                    .into_iter()
-                    .map(ContextMenuItem::to_list_item::<V>)
-                    .collect(),
-            ))
-            .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
+}
+
+impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
     }
 }
 
-use gpui::Action;
 #[cfg(feature = "stories")]
 pub use stories::*;
 
@@ -84,8 +260,18 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{Div, Render};
-    use serde::Deserialize;
+    use gpui::{actions, Div, Render, VisualContext};
+
+    actions!(PrintCurrentDate);
+
+    fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+        cx.build_view(|cx| {
+            ContextMenu::new(cx).header(header).separator().entry(
+                Label::new("Print current time"),
+                PrintCurrentDate.boxed_clone(),
+            )
+        })
+    }
 
     pub struct ContextMenuStory;
 
@@ -93,22 +279,84 @@ mod stories {
         type Element = Div<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            #[derive(PartialEq, Clone, Deserialize, gpui::Action)]
-            struct PrintCurrentDate {}
-
             Story::container(cx)
-                .child(Story::title_for::<_, ContextMenu>(cx))
-                .child(Story::label(cx, "Default"))
-                .child(ContextMenu::new([
-                    ContextMenuItem::header("Section header"),
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}),
-                ]))
                 .on_action(|_, _: &PrintCurrentDate, _| {
                     if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
                         println!("Current Unix time is {:?}", unix_time.as_secs());
                     }
                 })
+                .flex()
+                .flex_row()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test2")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .menu(move |_, cx| build_menu(cx, "top left")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test1")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomLeft)
+                                .attach(AnchorCorner::TopLeft)
+                                .menu(move |_, cx| build_menu(cx, "bottom left")),
+                        ),
+                )
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test3")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "top right")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test4")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomRight)
+                                .attach(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "bottom right")),
+                        ),
+                )
         }
     }
 }

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

@@ -1,5 +1,5 @@
 use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
-use gpui::{prelude::*, AnyView, MouseButton};
+use gpui::{prelude::*, Action, AnyView, MouseButton};
 use std::sync::Arc;
 
 struct IconButtonHandlers<V: 'static> {
@@ -19,6 +19,7 @@ pub struct IconButton<V: 'static> {
     color: TextColor,
     variant: ButtonVariant,
     state: InteractionState,
+    selected: bool,
     tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
     handlers: IconButtonHandlers<V>,
 }
@@ -31,6 +32,7 @@ impl<V: 'static> IconButton<V> {
             color: TextColor::default(),
             variant: ButtonVariant::default(),
             state: InteractionState::default(),
+            selected: false,
             tooltip: None,
             handlers: IconButtonHandlers::default(),
         }
@@ -56,6 +58,11 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
     pub fn tooltip(
         mut self,
         tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
@@ -69,6 +76,10 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn action(self, action: Box<dyn Action>) -> Self {
+        self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+
     fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
             (InteractionState::Disabled, _) => TextColor::Disabled,
@@ -76,7 +87,7 @@ impl<V: 'static> IconButton<V> {
             _ => self.color,
         };
 
-        let (bg_color, bg_hover_color, bg_active_color) = match self.variant {
+        let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
             ButtonVariant::Filled => (
                 cx.theme().colors().element_background,
                 cx.theme().colors().element_hover,
@@ -89,6 +100,10 @@ impl<V: 'static> IconButton<V> {
             ),
         };
 
+        if self.selected {
+            bg_color = bg_hover_color;
+        }
+
         let mut button = h_stack()
             .id(self.id.clone())
             .justify_center()
@@ -108,7 +123,9 @@ impl<V: 'static> IconButton<V> {
         }
 
         if let Some(tooltip) = self.tooltip.take() {
-            button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            if !self.selected {
+                button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            }
         }
 
         button

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

@@ -117,7 +117,7 @@ impl ListHeader {
     }
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub struct ListSubHeader {
     label: SharedString,
     left_icon: Option<Icon>,
@@ -172,7 +172,7 @@ pub enum ListEntrySize {
     Medium,
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub enum ListItem {
     Entry(ListEntry),
     Separator(ListSeparator),
@@ -234,6 +234,24 @@ pub struct ListEntry {
     on_click: Option<Box<dyn Action>>,
 }
 
+impl Clone for ListEntry {
+    fn clone(&self) -> Self {
+        Self {
+            disabled: self.disabled,
+            // TODO: Reintroduce this
+            // disclosure_control_style: DisclosureControlVisibility,
+            indent_level: self.indent_level,
+            label: self.label.clone(),
+            left_slot: self.left_slot.clone(),
+            overflow: self.overflow,
+            size: self.size,
+            toggle: self.toggle,
+            variant: self.variant,
+            on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+        }
+    }
+}
+
 impl ListEntry {
     pub fn new(label: Label) -> Self {
         Self {
@@ -249,7 +267,7 @@ impl ListEntry {
         }
     }
 
-    pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
+    pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
         self.on_click = Some(action.into());
         self
     }

crates/ui2/src/story.rs 🔗

@@ -12,7 +12,6 @@ impl Story {
             .flex_col()
             .pt_2()
             .px_4()
-            .font("Zed Mono")
             .bg(cx.theme().colors().background)
     }
 

crates/ui2/src/styled_ext.rs 🔗

@@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize};
 
 fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
+        .z_index(index.z_index())
         .rounded_lg()
         .border()
         .border_color(cx.theme().colors().border_variant)

crates/workspace2/src/dock.rs 🔗

@@ -1,14 +1,14 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext,
-    WeakView, WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use theme2::ActiveTheme;
-use ui::{h_stack, IconButton, InteractionState, Tooltip};
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -416,6 +416,14 @@ impl Dock {
             cx.notify();
         }
     }
+
+    pub fn toggle_action(&self) -> Box<dyn Action> {
+        match self.position {
+            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
+        }
+    }
 }
 
 impl Render for Dock {
@@ -603,6 +611,7 @@ impl PanelButtons {
 //     }
 // }
 
+// here be kittens
 impl Render for PanelButtons {
     type Element = Div<Self>;
 
@@ -612,6 +621,13 @@ impl Render for PanelButtons {
         let active_index = dock.active_panel_index;
         let is_open = dock.is_open;
 
+        let (menu_anchor, menu_attach) = match dock.position {
+            DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
+            DockPosition::Bottom | DockPosition::Right => {
+                (AnchorCorner::BottomRight, AnchorCorner::TopRight)
+            }
+        };
+
         let buttons = dock
             .panel_entries
             .iter()
@@ -619,15 +635,33 @@ impl Render for PanelButtons {
             .filter_map(|(i, panel)| {
                 let icon = panel.panel.icon(cx)?;
                 let name = panel.panel.persistent_name();
-                let action = panel.panel.toggle_action(cx);
-                let action2 = action.boxed_clone();
-
-                let mut button = IconButton::new(panel.panel.persistent_name(), icon)
-                    .when(i == active_index, |el| el.state(InteractionState::Active))
-                    .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
-                    .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx));
 
-                Some(button)
+                let mut button: IconButton<Self> = if i == active_index && is_open {
+                    let action = dock.toggle_action();
+                    let tooltip: SharedString =
+                        format!("Close {} dock", dock.position.to_label()).into();
+                    IconButton::new(name, icon)
+                        .state(InteractionState::Active)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+                } else {
+                    let action = panel.panel.toggle_action(cx);
+
+                    IconButton::new(name, icon)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+                };
+
+                Some(
+                    menu_handle()
+                        .id(name)
+                        .menu(move |_, cx| {
+                            cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+                        })
+                        .anchor(menu_anchor)
+                        .attach(menu_attach)
+                        .child(|is_open| button.selected(is_open)),
+                )
             });
 
         h_stack().gap_0p5().children(buttons)

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
-    ViewContext, WindowContext,
+    div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View,
+    ViewContext,
 };
 use ui::{h_stack, v_stack};
 
@@ -15,14 +15,6 @@ pub struct ModalLayer {
     active_modal: Option<ActiveModal>,
 }
 
-pub trait Modal: Render + EventEmitter<ModalEvent> {
-    fn focus(&self, cx: &mut WindowContext);
-}
-
-pub enum ModalEvent {
-    Dismissed,
-}
-
 impl ModalLayer {
     pub fn new() -> Self {
         Self { active_modal: None }
@@ -30,7 +22,7 @@ impl ModalLayer {
 
     pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
     where
-        V: Modal,
+        V: ManagedView,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
         if let Some(active_modal) = &self.active_modal {
@@ -46,17 +38,15 @@ impl ModalLayer {
 
     pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
     where
-        V: Modal,
+        V: ManagedView,
     {
         self.active_modal = Some(ActiveModal {
             modal: new_modal.clone().into(),
-            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e {
-                ModalEvent::Dismissed => this.hide_modal(cx),
-            }),
+            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)),
             previous_focus_handle: cx.focused(),
             focus_handle: cx.focus_handle(),
         });
-        new_modal.update(cx, |modal, cx| modal.focus(cx));
+        cx.focus_view(&new_modal);
         cx.notify();
     }
 

crates/workspace2/src/workspace2.rs 🔗

@@ -31,10 +31,10 @@ use futures::{
 use gpui::{
     actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
     AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, ModelContext,
-    ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription,
-    Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
-    WindowOptions,
+    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
+    ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
+    WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -3202,8 +3202,9 @@ impl Workspace {
         })
     }
 
-    fn actions(div: Div<Self>) -> Div<Self> {
-        div.on_action(Self::open)
+    fn actions(&self, div: Div<Self>) -> Div<Self> {
+        self.add_workspace_actions_listeners(div)
+            //     cx.add_async_action(Workspace::open);
             //     cx.add_async_action(Workspace::follow_next_collaborator);
             //     cx.add_async_action(Workspace::close);
             .on_action(Self::close_inactive_items_and_panes)
@@ -3239,15 +3240,15 @@ impl Workspace {
             .on_action(|this, e: &ToggleLeftDock, cx| {
                 this.toggle_dock(DockPosition::Left, cx);
             })
-        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
-        //         workspace.toggle_dock(DockPosition::Right, cx);
-        //     });
-        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
-        //         workspace.toggle_dock(DockPosition::Bottom, cx);
-        //     });
-        //     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
-        //         workspace.close_all_docks(cx);
-        //     });
+            .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+                workspace.toggle_dock(DockPosition::Right, cx);
+            })
+            .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+                workspace.toggle_dock(DockPosition::Bottom, cx);
+            })
+            .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+                workspace.close_all_docks(cx);
+            })
         //     cx.add_action(Workspace::activate_pane_at_index);
         //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         //         workspace.reopen_closed_item(cx).detach();
@@ -3363,11 +3364,14 @@ impl Workspace {
         div
     }
 
-    pub fn active_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
+    pub fn active_modal<V: ManagedView + 'static>(
+        &mut self,
+        cx: &ViewContext<Self>,
+    ) -> Option<View<V>> {
         self.modal_layer.read(cx).active_modal()
     }
 
-    pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
+    pub fn toggle_modal<V: ManagedView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
     where
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
@@ -3605,7 +3609,7 @@ impl Render for Workspace {
 
         cx.set_rem_size(ui_font_size);
 
-        Self::actions(self.add_workspace_actions_listeners(div()))
+        self.actions(div())
             .key_context(context)
             .relative()
             .size_full()