Merge branch 'main' into arena

Nathan Sobo created

Change summary

Cargo.lock                                       |   3 
crates/collab_ui2/src/collab_panel.rs            | 356 +++++-----------
crates/collab_ui2/src/collab_titlebar_item.rs    |  22 
crates/command_palette2/src/command_palette.rs   |   4 
crates/editor2/src/editor.rs                     |  14 
crates/editor2/src/element.rs                    | 110 ++--
crates/gpui2/src/elements/list.rs                |  73 +++
crates/gpui2/src/platform/mac/metal_renderer.rs  |   5 
crates/gpui2/src/view.rs                         |  11 
crates/gpui2/src/window.rs                       |   2 
crates/project_panel2/src/project_panel.rs       |   6 
crates/recent_projects2/src/recent_projects.rs   |   5 
crates/search2/src/buffer_search.rs              |  11 
crates/search2/src/project_search.rs             |  21 
crates/story/Cargo.toml                          |   2 
crates/story/src/story.rs                        | 376 +++++++++++++++++
crates/storybook2/Cargo.toml                     |   1 
crates/storybook2/src/stories/text.rs            | 215 +++++++--
crates/storybook2/src/storybook2.rs              |   1 
crates/ui2/src/components/stories/icon_button.rs | 214 +++++++--
crates/ui2/src/components/tooltip.rs             |   4 
crates/vcs_menu2/src/lib.rs                      |   9 
crates/workspace2/src/pane.rs                    |  15 
crates/workspace2/src/pane_group.rs              |  22 
24 files changed, 1,015 insertions(+), 487 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9263,6 +9263,8 @@ name = "story"
 version = "0.1.0"
 dependencies = [
  "gpui2",
+ "itertools 0.10.5",
+ "smallvec",
 ]
 
 [[package]]
@@ -9277,6 +9279,7 @@ dependencies = [
  "editor2",
  "fuzzy2",
  "gpui2",
+ "indoc",
  "itertools 0.11.0",
  "language2",
  "log",

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -165,7 +165,7 @@ struct ChannelMoveClipboard {
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-use std::{iter::once, mem, sync::Arc};
+use std::{mem, sync::Arc};
 
 use call::ActiveCall;
 use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
@@ -175,12 +175,12 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
-    serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
-    DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
-    InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
-    PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
-    Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    actions, canvas, div, fill, impl_actions, list, overlay, point, prelude::*, px, serde_json,
+    AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
+    EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement,
+    ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
+    Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -188,7 +188,7 @@ use settings::{Settings, SettingsStore};
 use ui::prelude::*;
 use ui::{
     h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
-    Label, List, ListHeader, ListItem, Tooltip,
+    Label, ListHeader, ListItem, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -303,6 +303,7 @@ pub struct CollabPanel {
     channel_clipboard: Option<ChannelMoveClipboard>,
     pending_serialization: Task<Option<()>>,
     context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
+    list_state: ListState,
     filter_editor: View<Editor>,
     channel_name_editor: View<Editor>,
     channel_editing_state: Option<ChannelEditingState>,
@@ -313,7 +314,6 @@ pub struct CollabPanel {
     client: Arc<Client>,
     project: Model<Project>,
     match_candidates: Vec<StringMatchCandidate>,
-    scroll_handle: ScrollHandle,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
     collapsed_channels: Vec<ChannelId>,
@@ -398,7 +398,7 @@ enum ListEntry {
 impl CollabPanel {
     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         cx.build_view(|cx| {
-            //             let view_id = cx.view_id();
+            let view = cx.view().clone();
 
             let filter_editor = cx.build_view(|cx| {
                 let mut editor = Editor::single_line(cx);
@@ -445,136 +445,10 @@ impl CollabPanel {
             })
             .detach();
 
-            //             let list_state =
-            //                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-            //                     let theme = theme::current(cx).clone();
-            //                     let is_selected = this.selection == Some(ix);
-            //                     let current_project_id = this.project.read(cx).remote_id();
-
-            //                     match &this.entries[ix] {
-            //                         ListEntry::Header(section) => {
-            //                             let is_collapsed = this.collapsed_sections.contains(section);
-            //                             this.render_header(*section, &theme, is_selected, is_collapsed, cx)
-            //                         }
-            //                         ListEntry::CallParticipant {
-            //                             user,
-            //                             peer_id,
-            //                             is_pending,
-            //                         } => Self::render_call_participant(
-            //                             user,
-            //                             *peer_id,
-            //                             this.user_store.clone(),
-            //                             *is_pending,
-            //                             is_selected,
-            //                             &theme,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::ParticipantProject {
-            //                             project_id,
-            //                             worktree_root_names,
-            //                             host_user_id,
-            //                             is_last,
-            //                         } => Self::render_participant_project(
-            //                             *project_id,
-            //                             worktree_root_names,
-            //                             *host_user_id,
-            //                             Some(*project_id) == current_project_id,
-            //                             *is_last,
-            //                             is_selected,
-            //                             &theme,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::ParticipantScreen { peer_id, is_last } => {
-            //                             Self::render_participant_screen(
-            //                                 *peer_id,
-            //                                 *is_last,
-            //                                 is_selected,
-            //                                 &theme.collab_panel,
-            //                                 cx,
-            //                             )
-            //                         }
-            //                         ListEntry::Channel {
-            //                             channel,
-            //                             depth,
-            //                             has_children,
-            //                         } => {
-            //                             let channel_row = this.render_channel(
-            //                                 &*channel,
-            //                                 *depth,
-            //                                 &theme,
-            //                                 is_selected,
-            //                                 *has_children,
-            //                                 ix,
-            //                                 cx,
-            //                             );
-
-            //                             if is_selected && this.context_menu_on_selected {
-            //                                 Stack::new()
-            //                                     .with_child(channel_row)
-            //                                     .with_child(
-            //                                         ChildView::new(&this.context_menu, cx)
-            //                                             .aligned()
-            //                                             .bottom()
-            //                                             .right(),
-            //                                     )
-            //                                     .into_any()
-            //                             } else {
-            //                                 return channel_row;
-            //                             }
-            //                         }
-            //                         ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
-            //                             *channel_id,
-            //                             &theme.collab_panel,
-            //                             is_selected,
-            //                             ix,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
-            //                             *channel_id,
-            //                             &theme.collab_panel,
-            //                             is_selected,
-            //                             ix,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
-            //                             channel.clone(),
-            //                             this.channel_store.clone(),
-            //                             &theme.collab_panel,
-            //                             is_selected,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::IncomingRequest(user) => Self::render_contact_request(
-            //                             user.clone(),
-            //                             this.user_store.clone(),
-            //                             &theme.collab_panel,
-            //                             true,
-            //                             is_selected,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::OutgoingRequest(user) => Self::render_contact_request(
-            //                             user.clone(),
-            //                             this.user_store.clone(),
-            //                             &theme.collab_panel,
-            //                             false,
-            //                             is_selected,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::Contact { contact, calling } => Self::render_contact(
-            //                             contact,
-            //                             *calling,
-            //                             &this.project,
-            //                             &theme,
-            //                             is_selected,
-            //                             cx,
-            //                         ),
-            //                         ListEntry::ChannelEditor { depth } => {
-            //                             this.render_channel_editor(&theme, *depth, cx)
-            //                         }
-            //                         ListEntry::ContactPlaceholder => {
-            //                             this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
-            //                         }
-            //                     }
-            //                 });
+            let list_state =
+                ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
+                    view.update(cx, |view, cx| view.render_list_entry(ix, cx))
+                });
 
             let mut this = Self {
                 width: None,
@@ -583,6 +457,7 @@ impl CollabPanel {
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
                 context_menu: None,
+                list_state,
                 channel_name_editor,
                 filter_editor,
                 entries: Vec::default(),
@@ -593,7 +468,6 @@ impl CollabPanel {
                 project: workspace.project().clone(),
                 subscriptions: Vec::default(),
                 match_candidates: Vec::default(),
-                scroll_handle: ScrollHandle::new(),
                 collapsed_sections: vec![Section::Offline],
                 collapsed_channels: Vec::default(),
                 workspace: workspace.weak_handle(),
@@ -709,6 +583,10 @@ impl CollabPanel {
         );
     }
 
+    fn scroll_to_item(&mut self, ix: usize) {
+        self.list_state.scroll_to_reveal_item(ix)
+    }
+
     fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
         let channel_store = self.channel_store.read(cx);
         let user_store = self.user_store.read(cx);
@@ -1084,13 +962,15 @@ impl CollabPanel {
             self.entries.push(ListEntry::ContactPlaceholder);
         }
 
+        self.list_state.reset(self.entries.len());
+
         if select_same_item {
             if let Some(prev_selected_entry) = prev_selected_entry {
                 self.selection.take();
                 for (ix, entry) in self.entries.iter().enumerate() {
                     if *entry == prev_selected_entry {
                         self.selection = Some(ix);
-                        self.scroll_handle.scroll_to_item(ix);
+                        self.scroll_to_item(ix);
                         break;
                     }
                 }
@@ -1101,16 +981,19 @@ impl CollabPanel {
                     None
                 } else {
                     let ix = prev_selection.min(self.entries.len() - 1);
-                    self.scroll_handle.scroll_to_item(ix);
+                    self.scroll_to_item(ix);
                     Some(ix)
                 }
             });
         }
 
         if scroll_to_top {
-            self.scroll_handle.scroll_to_item(0)
+            self.scroll_to_item(0)
         } else {
-            let (old_index, old_offset) = self.scroll_handle.logical_scroll_top();
+            let ListOffset {
+                item_ix: old_index,
+                offset_in_item: old_offset,
+            } = self.list_state.logical_scroll_top();
             // Attempt to maintain the same scroll position.
             if let Some(old_top_entry) = old_entries.get(old_index) {
                 let (new_index, new_offset) = self
@@ -1136,8 +1019,10 @@ impl CollabPanel {
                     })
                     .unwrap_or_else(|| (old_index, old_offset));
 
-                self.scroll_handle
-                    .set_logical_scroll_top(new_index, new_offset);
+                self.list_state.scroll_to(ListOffset {
+                    item_ix: new_index,
+                    offset_in_item: new_offset,
+                });
             }
         }
 
@@ -1628,7 +1513,7 @@ impl CollabPanel {
         }
 
         if let Some(ix) = self.selection {
-            self.scroll_handle.scroll_to_item(ix)
+            self.scroll_to_item(ix)
         }
         cx.notify();
     }
@@ -1640,7 +1525,7 @@ impl CollabPanel {
         }
 
         if let Some(ix) = self.selection {
-            self.scroll_handle.scroll_to_item(ix)
+            self.scroll_to_item(ix)
         }
         cx.notify();
     }
@@ -1965,7 +1850,7 @@ impl CollabPanel {
         };
         let Some(bounds) = self
             .selection
-            .and_then(|ix| self.scroll_handle.bounds_for_item(ix))
+            .and_then(|ix| self.list_state.bounds_for_item(ix))
         else {
             return;
         };
@@ -2158,78 +2043,75 @@ impl CollabPanel {
         )
     }
 
+    fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
+        let entry = &self.entries[ix];
+
+        let is_selected = self.selection == Some(ix);
+        match entry {
+            ListEntry::Header(section) => {
+                let is_collapsed = self.collapsed_sections.contains(section);
+                self.render_header(*section, is_selected, is_collapsed, cx)
+                    .into_any_element()
+            }
+            ListEntry::Contact { contact, calling } => self
+                .render_contact(contact, *calling, is_selected, cx)
+                .into_any_element(),
+            ListEntry::ContactPlaceholder => self
+                .render_contact_placeholder(is_selected, cx)
+                .into_any_element(),
+            ListEntry::IncomingRequest(user) => self
+                .render_contact_request(user, true, is_selected, cx)
+                .into_any_element(),
+            ListEntry::OutgoingRequest(user) => self
+                .render_contact_request(user, false, is_selected, cx)
+                .into_any_element(),
+            ListEntry::Channel {
+                channel,
+                depth,
+                has_children,
+            } => self
+                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
+                .into_any_element(),
+            ListEntry::ChannelEditor { depth } => {
+                self.render_channel_editor(*depth, cx).into_any_element()
+            }
+            ListEntry::CallParticipant {
+                user,
+                peer_id,
+                is_pending,
+            } => self
+                .render_call_participant(user, *peer_id, *is_pending, cx)
+                .into_any_element(),
+            ListEntry::ParticipantProject {
+                project_id,
+                worktree_root_names,
+                host_user_id,
+                is_last,
+            } => self
+                .render_participant_project(
+                    *project_id,
+                    &worktree_root_names,
+                    *host_user_id,
+                    *is_last,
+                    cx,
+                )
+                .into_any_element(),
+            ListEntry::ParticipantScreen { peer_id, is_last } => self
+                .render_participant_screen(*peer_id, *is_last, cx)
+                .into_any_element(),
+            ListEntry::ChannelNotes { channel_id } => self
+                .render_channel_notes(*channel_id, cx)
+                .into_any_element(),
+            ListEntry::ChannelChat { channel_id } => {
+                self.render_channel_chat(*channel_id, cx).into_any_element()
+            }
+        }
+    }
+
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
         v_stack()
             .size_full()
-            .child(
-                v_stack()
-                    .size_full()
-                    .id("scroll")
-                    .overflow_y_scroll()
-                    .track_scroll(&self.scroll_handle)
-                    .children(self.entries.iter().enumerate().map(|(ix, entry)| {
-                        let is_selected = self.selection == Some(ix);
-                        match entry {
-                            ListEntry::Header(section) => {
-                                let is_collapsed = self.collapsed_sections.contains(section);
-                                self.render_header(*section, is_selected, is_collapsed, cx)
-                                    .into_any_element()
-                            }
-                            ListEntry::Contact { contact, calling } => self
-                                .render_contact(contact, *calling, is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::ContactPlaceholder => self
-                                .render_contact_placeholder(is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::IncomingRequest(user) => self
-                                .render_contact_request(user, true, is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::OutgoingRequest(user) => self
-                                .render_contact_request(user, false, is_selected, cx)
-                                .into_any_element(),
-                            ListEntry::Channel {
-                                channel,
-                                depth,
-                                has_children,
-                            } => self
-                                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
-                                .into_any_element(),
-                            ListEntry::ChannelEditor { depth } => {
-                                self.render_channel_editor(*depth, cx).into_any_element()
-                            }
-                            ListEntry::CallParticipant {
-                                user,
-                                peer_id,
-                                is_pending,
-                            } => self
-                                .render_call_participant(user, *peer_id, *is_pending, cx)
-                                .into_any_element(),
-                            ListEntry::ParticipantProject {
-                                project_id,
-                                worktree_root_names,
-                                host_user_id,
-                                is_last,
-                            } => self
-                                .render_participant_project(
-                                    *project_id,
-                                    &worktree_root_names,
-                                    *host_user_id,
-                                    *is_last,
-                                    cx,
-                                )
-                                .into_any_element(),
-                            ListEntry::ParticipantScreen { peer_id, is_last } => self
-                                .render_participant_screen(*peer_id, *is_last, cx)
-                                .into_any_element(),
-                            ListEntry::ChannelNotes { channel_id } => self
-                                .render_channel_notes(*channel_id, cx)
-                                .into_any_element(),
-                            ListEntry::ChannelChat { channel_id } => {
-                                self.render_channel_chat(*channel_id, cx).into_any_element()
-                            }
-                        }
-                    })),
-            )
+            .child(list(self.list_state.clone()).full())
             .child(
                 div().p_2().child(
                     div()
@@ -2343,18 +2225,14 @@ impl CollabPanel {
                     .selected(is_selected),
             )
             .when(section == Section::Channels, |el| {
-                el.drag_over::<DraggedChannelView>(|style| {
-                    style.bg(cx.theme().colors().ghost_element_hover)
-                })
-                .on_drop(cx.listener(
-                    move |this, view: &View<DraggedChannelView>, cx| {
+                el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+                    .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
                         this.channel_store
                             .update(cx, |channel_store, cx| {
-                                channel_store.move_channel(view.read(cx).channel.id, None, cx)
+                                channel_store.move_channel(dragged_channel.id, None, cx)
                             })
                             .detach_and_log_err(cx)
-                    },
-                ))
+                    }))
             });
 
         if section == Section::Offline {
@@ -2569,22 +2447,14 @@ impl CollabPanel {
                     width,
                 })
             })
-            .drag_over::<DraggedChannelView>(|style| {
-                style.bg(cx.theme().colors().ghost_element_hover)
-            })
-            .on_drop(
-                cx.listener(move |this, view: &View<DraggedChannelView>, cx| {
-                    this.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.move_channel(
-                                view.read(cx).channel.id,
-                                Some(channel_id),
-                                cx,
-                            )
-                        })
-                        .detach_and_log_err(cx)
-                }),
-            )
+            .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+            .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
+                this.channel_store
+                    .update(cx, |channel_store, cx| {
+                        channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
+                    })
+                    .detach_and_log_err(cx)
+            }))
             .child(
                 ListItem::new(channel_id as usize)
                     // Offset the indent depth by one to give us room to show the disclosure.

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -74,12 +74,16 @@ impl Render for CollabTitlebarItem {
             // Set a non-scaling min-height here to ensure the titlebar is
             // always at least the height of the traffic lights.
             .min_h(px(32.))
-            .when(
-                !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
-                // Use pixels here instead of a rem-based size because the macOS traffic
-                // lights are a static size, and don't scale with the rest of the UI.
-                |s| s.pl(px(68.)),
-            )
+            .pl_2()
+            .map(|this| {
+                if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
+                    this.pl_2()
+                } else {
+                    // Use pixels here instead of a rem-based size because the macOS traffic
+                    // lights are a static size, and don't scale with the rest of the UI.
+                    this.pl(px(72.))
+                }
+            })
             .bg(cx.theme().colors().title_bar_background)
             .on_click(|event, cx| {
                 if event.up.click_count == 2 {
@@ -165,6 +169,7 @@ impl Render for CollabTitlebarItem {
             .child(
                 h_stack()
                     .gap_1()
+                    .pr_1()
                     .when_some(room, |this, room| {
                         let room = room.read(cx);
                         let is_shared = self.project.read(cx).is_shared();
@@ -325,8 +330,6 @@ impl CollabTitlebarItem {
         let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
 
         div()
-            .border()
-            .border_color(gpui::red())
             .child(
                 Button::new("project_name_trigger", name)
                     .style(ButtonStyle::Subtle)
@@ -365,10 +368,9 @@ impl CollabTitlebarItem {
 
         Some(
             div()
-                .border()
-                .border_color(gpui::red())
                 .child(
                     Button::new("project_branch_trigger", branch_name)
+                        .color(Color::Muted)
                         .style(ButtonStyle::Subtle)
                         .tooltip(move |cx| {
                             Tooltip::with_meta(

crates/command_palette2/src/command_palette.rs 🔗

@@ -101,6 +101,7 @@ pub struct CommandInterceptResult {
 
 pub struct CommandPaletteDelegate {
     command_palette: WeakView<CommandPalette>,
+    all_commands: Vec<Command>,
     commands: Vec<Command>,
     matches: Vec<StringMatch>,
     selected_ix: usize,
@@ -135,6 +136,7 @@ impl CommandPaletteDelegate {
     ) -> Self {
         Self {
             command_palette,
+            all_commands: commands.clone(),
             matches: vec![],
             commands,
             selected_ix: 0,
@@ -167,7 +169,7 @@ impl PickerDelegate for CommandPaletteDelegate {
         query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> gpui::Task<()> {
-        let mut commands = self.commands.clone();
+        let mut commands = self.all_commands.clone();
 
         cx.spawn(move |picker, mut cx| async move {
             cx.read_global::<HitCounts, _>(|hit_counts, _| {

crates/editor2/src/editor.rs 🔗

@@ -9739,12 +9739,8 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
         };
         highlighted_lines.push(line);
     }
-    let message = diagnostic.message;
     Arc::new(move |cx: &mut BlockContext| {
-        let message = message.clone();
         let copy_id: SharedString = format!("copy-{}", cx.block_id.clone()).to_string().into();
-        let write_to_clipboard = cx.write_to_clipboard(ClipboardItem::new(message.clone()));
-
         // TODO: Nate: We should tint the background of the block with the severity color
         // We need to extend the theme before we can do this
         v_stack()
@@ -9754,7 +9750,6 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
             .bg(gpui::red())
             .children(highlighted_lines.iter().map(|(line, highlights)| {
                 let group_id = cx.block_id.to_string();
-
                 h_stack()
                     .group(group_id.clone())
                     .gap_2()
@@ -9763,13 +9758,18 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
                     .px_1p5()
                     .child(HighlightedLabel::new(line.clone(), highlights.clone()))
                     .child(
-                        div().border().border_color(gpui::red()).child(
+                        div().z_index(1).child(
                             IconButton::new(copy_id.clone(), Icon::Copy)
                                 .icon_color(Color::Muted)
                                 .size(ButtonSize::Compact)
                                 .style(ButtonStyle::Transparent)
                                 .visible_on_hover(group_id)
-                                .on_click(cx.listener(move |_, _, cx| write_to_clipboard))
+                                .on_click(cx.listener({
+                                    let message = diagnostic.message.clone();
+                                    move |_, _, cx| {
+                                        cx.write_to_clipboard(ClipboardItem::new(message.clone()))
+                                    }
+                                }))
                                 .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
                         ),
                     )

crates/editor2/src/element.rs 🔗

@@ -2284,8 +2284,8 @@ impl EditorElement {
                                 .cursor_pointer()
                                 .hover(|style| style.bg(cx.theme().colors().element_hover))
                                 .on_click(cx.listener(|_editor, _event, _cx| {
-                                    // TODO: Implement collapsing path headers
-                                    todo!("Clicking path header")
+                                    // todo!() Implement collapsing path headers
+                                    // todo!("Clicking path header")
                                 }))
                                 .child(
                                     h_stack()
@@ -2447,13 +2447,13 @@ impl EditorElement {
             let interactive_bounds = interactive_bounds.clone();
 
             move |event: &ScrollWheelEvent, phase, cx| {
-                if phase != DispatchPhase::Bubble {
-                    return;
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, cx)
+                {
+                    editor.update(cx, |editor, cx| {
+                        Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
+                    });
                 }
-
-                editor.update(cx, |editor, cx| {
-                    Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
-                });
             }
         });
 
@@ -2461,29 +2461,30 @@ impl EditorElement {
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
             let stacking_order = cx.stacking_order().clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             move |event: &MouseDownEvent, phase, cx| {
-                if phase != DispatchPhase::Bubble {
-                    return;
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, cx)
+                {
+                    match event.button {
+                        MouseButton::Left => editor.update(cx, |editor, cx| {
+                            Self::mouse_left_down(
+                                editor,
+                                event,
+                                &position_map,
+                                text_bounds,
+                                gutter_bounds,
+                                &stacking_order,
+                                cx,
+                            );
+                        }),
+                        MouseButton::Right => editor.update(cx, |editor, cx| {
+                            Self::mouse_right_down(editor, event, &position_map, text_bounds, cx);
+                        }),
+                        _ => {}
+                    };
                 }
-
-                match event.button {
-                    MouseButton::Left => editor.update(cx, |editor, cx| {
-                        Self::mouse_left_down(
-                            editor,
-                            event,
-                            &position_map,
-                            text_bounds,
-                            gutter_bounds,
-                            &stacking_order,
-                            cx,
-                        );
-                    }),
-                    MouseButton::Right => editor.update(cx, |editor, cx| {
-                        Self::mouse_right_down(editor, event, &position_map, text_bounds, cx);
-                    }),
-                    _ => {}
-                };
             }
         });
 
@@ -2491,18 +2492,23 @@ impl EditorElement {
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
             let stacking_order = cx.stacking_order().clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             move |event: &MouseUpEvent, phase, cx| {
-                editor.update(cx, |editor, cx| {
-                    Self::mouse_up(
-                        editor,
-                        event,
-                        &position_map,
-                        text_bounds,
-                        &stacking_order,
-                        cx,
-                    )
-                });
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, cx)
+                {
+                    editor.update(cx, |editor, cx| {
+                        Self::mouse_up(
+                            editor,
+                            event,
+                            &position_map,
+                            text_bounds,
+                            &stacking_order,
+                            cx,
+                        )
+                    });
+                }
             }
         });
         cx.on_mouse_event({
@@ -2511,21 +2517,21 @@ impl EditorElement {
             let stacking_order = cx.stacking_order().clone();
 
             move |event: &MouseMoveEvent, phase, cx| {
-                if phase != DispatchPhase::Bubble {
-                    return;
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, cx)
+                {
+                    editor.update(cx, |editor, cx| {
+                        Self::mouse_moved(
+                            editor,
+                            event,
+                            &position_map,
+                            text_bounds,
+                            gutter_bounds,
+                            &stacking_order,
+                            cx,
+                        )
+                    });
                 }
-
-                editor.update(cx, |editor, cx| {
-                    Self::mouse_moved(
-                        editor,
-                        event,
-                        &position_map,
-                        text_bounds,
-                        gutter_bounds,
-                        &stacking_order,
-                        cx,
-                    )
-                });
             }
         });
     }

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

@@ -1,6 +1,7 @@
 use crate::{
-    px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
-    Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext,
+    point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element,
+    IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
+    WindowContext,
 };
 use collections::VecDeque;
 use refineable::Refineable as _;
@@ -23,7 +24,7 @@ pub struct List {
 pub struct ListState(Rc<RefCell<StateInner>>);
 
 struct StateInner {
-    last_layout_width: Option<Pixels>,
+    last_layout_bounds: Option<Bounds<Pixels>>,
     render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
     items: SumTree<ListItem>,
     logical_scroll_top: Option<ListOffset>,
@@ -83,7 +84,7 @@ impl ListState {
         let mut items = SumTree::new();
         items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
         Self(Rc::new(RefCell::new(StateInner {
-            last_layout_width: None,
+            last_layout_bounds: None,
             render_item: Box::new(render_item),
             items,
             logical_scroll_top: None,
@@ -152,6 +153,64 @@ impl ListState {
         }
         state.logical_scroll_top = Some(scroll_top);
     }
+
+    pub fn scroll_to_reveal_item(&self, ix: usize) {
+        let state = &mut *self.0.borrow_mut();
+        let mut scroll_top = state.logical_scroll_top();
+        let height = state
+            .last_layout_bounds
+            .map_or(px(0.), |bounds| bounds.size.height);
+
+        if ix <= scroll_top.item_ix {
+            scroll_top.item_ix = ix;
+            scroll_top.offset_in_item = px(0.);
+        } else {
+            let mut cursor = state.items.cursor::<ListItemSummary>();
+            cursor.seek(&Count(ix + 1), Bias::Right, &());
+            let bottom = cursor.start().height;
+            let goal_top = px(0.).max(bottom - height);
+
+            cursor.seek(&Height(goal_top), Bias::Left, &());
+            let start_ix = cursor.start().count;
+            let start_item_top = cursor.start().height;
+
+            if start_ix >= scroll_top.item_ix {
+                scroll_top.item_ix = start_ix;
+                scroll_top.offset_in_item = goal_top - start_item_top;
+            }
+        }
+
+        state.logical_scroll_top = Some(scroll_top);
+    }
+
+    /// Get the bounds for the given item in window coordinates.
+    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
+        let state = &*self.0.borrow();
+        let bounds = state.last_layout_bounds.unwrap_or_default();
+        let scroll_top = state.logical_scroll_top();
+
+        if ix < scroll_top.item_ix {
+            return None;
+        }
+
+        let mut cursor = state.items.cursor::<(Count, Height)>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+        let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
+
+        cursor.seek_forward(&Count(ix), Bias::Right, &());
+        if let Some(&ListItem::Rendered { height }) = cursor.item() {
+            let &(Count(count), Height(top)) = cursor.start();
+            if count == ix {
+                let top = bounds.top() + top - scroll_top;
+                return Some(Bounds::from_corners(
+                    point(bounds.left(), top),
+                    point(bounds.right(), top + height),
+                ));
+            }
+        }
+        None
+    }
 }
 
 impl StateInner {
@@ -265,7 +324,9 @@ impl Element for List {
         let state = &mut *self.state.0.borrow_mut();
 
         // If the width of the list has changed, invalidate all cached item heights
-        if state.last_layout_width != Some(bounds.size.width) {
+        if state.last_layout_bounds.map_or(true, |last_bounds| {
+            last_bounds.size.width != bounds.size.width
+        }) {
             state.items = SumTree::from_iter(
                 (0..state.items.summary().count).map(|_| ListItem::Unrendered),
                 &(),
@@ -392,7 +453,7 @@ impl Element for List {
         }
 
         state.items = new_items;
-        state.last_layout_width = Some(bounds.size.width);
+        state.last_layout_bounds = Some(bounds);
 
         let list_state = self.state.clone();
         let height = bounds.size.height;

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

@@ -187,8 +187,6 @@ impl MetalRenderer {
     }
 
     pub fn draw(&mut self, scene: &Scene) {
-        let start = std::time::Instant::now();
-
         let layer = self.layer.clone();
         let viewport_size = layer.drawable_size();
         let viewport_size: Size<DevicePixels> = size(
@@ -306,9 +304,6 @@ impl MetalRenderer {
         command_buffer.commit();
         self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
 
-        let duration_since_start = start.elapsed();
-        println!("renderer draw: {:?}", duration_since_start);
-
         command_buffer.wait_until_completed();
         drawable.present();
     }

crates/gpui2/src/view.rs 🔗

@@ -209,20 +209,9 @@ impl AnyView {
         cx: &mut WindowContext,
     ) {
         cx.with_absolute_element_offset(origin, |cx| {
-            let start_time = std::time::Instant::now();
             let (layout_id, mut rendered_element) = (self.layout)(self, cx);
-            let duration = start_time.elapsed();
-            println!("request layout: {:?}", duration);
-
-            let start_time = std::time::Instant::now();
             cx.compute_layout(layout_id, available_space);
-            let duration = start_time.elapsed();
-            println!("compute layout: {:?}", duration);
-
-            let start_time = std::time::Instant::now();
             (self.paint)(self, &mut rendered_element, cx);
-            let duration = start_time.elapsed();
-            println!("paint: {:?}", duration);
         })
     }
 }

crates/gpui2/src/window.rs 🔗

@@ -1267,7 +1267,6 @@ impl<'a> WindowContext<'a> {
 
     /// Draw pixels to the display for this window based on the contents of its scene.
     pub(crate) fn draw(&mut self) -> Scene {
-        let t0 = std::time::Instant::now();
         self.window.dirty = false;
         self.window.drawing = true;
 
@@ -1369,7 +1368,6 @@ impl<'a> WindowContext<'a> {
         }
 
         self.window.drawing = false;
-        eprintln!("window draw: {:?}", t0.elapsed());
 
         scene
     }

crates/project_panel2/src/project_panel.rs 🔗

@@ -1389,7 +1389,9 @@ impl ProjectPanel {
                     entry_id: *entry_id,
                 })
             })
-            .drag_over::<ProjectEntryId>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+            .drag_over::<ProjectEntryId>(|style| {
+                style.bg(cx.theme().colors().drop_target_background)
+            })
             .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
                 this.move_entry(*dragged_id, entry_id, kind.is_file(), cx);
             }))
@@ -1399,7 +1401,7 @@ impl ProjectPanel {
                     .indent_step_size(px(settings.indent_size))
                     .selected(is_selected)
                     .child(if let Some(icon) = &icon {
-                        div().child(IconElement::from_path(icon.to_string()))
+                        div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
                     } else {
                         div()
                     })

crates/recent_projects2/src/recent_projects.rs 🔗

@@ -76,7 +76,10 @@ impl RecentProjects {
                         let delegate =
                             RecentProjectsDelegate::new(weak_workspace, workspace_locations, true);
 
-                        RecentProjects::new(delegate, cx)
+                        let modal = RecentProjects::new(delegate, cx);
+                        cx.subscribe(&modal.picker, |_, _, _, cx| cx.emit(DismissEvent))
+                            .detach();
+                        modal
                     });
                 } else {
                     workspace.show_notification(0, cx, |cx| {

crates/search2/src/buffer_search.rs 🔗

@@ -338,7 +338,9 @@ impl BufferSearchBar {
             pane.update(cx, |this, cx| {
                 this.toolbar().update(cx, |this, cx| {
                     if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
-                        search_bar.update(cx, |this, cx| this.toggle(deploy, cx));
+                        search_bar.update(cx, |this, cx| {
+                            this.deploy(deploy, cx);
+                        });
                         return;
                     }
                     let view = cx.build_view(|cx| BufferSearchBar::new(cx));
@@ -1483,9 +1485,9 @@ mod tests {
                     search_bar.select_all_matches(&SelectAllMatches, cx);
                 });
                 assert!(
-                editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
-                "Should not switch focus to editor if SelectAllMatches does not find any matches"
-            );
+                    editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
+                    "Should not switch focus to editor if SelectAllMatches does not find any matches"
+                );
                 search_bar.update(cx, |search_bar, cx| {
                     let all_selections =
                         editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
@@ -1651,6 +1653,7 @@ mod tests {
             assert_eq!(search_bar.search_options, SearchOptions::NONE);
         });
     }
+
     #[gpui::test]
     async fn test_replace_simple(cx: &mut TestAppContext) {
         let (editor, search_bar, cx) = init_test(cx);

crates/search2/src/project_search.rs 🔗

@@ -1536,13 +1536,30 @@ impl Render for ProjectSearchBar {
                                         cx,
                                     )
                                 })
-                                .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
+                                .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx))
                                 .on_click(cx.listener(
                                     |this, _, cx| {
-                                        this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
+                                        this.toggle_search_option(
+                                            SearchOptions::CASE_SENSITIVE,
+                                            cx,
+                                        );
                                     },
                                 )),
                             )
+                            .child(
+                                IconButton::new("project-search-whole-word", Icon::WholeWord)
+                                    .tooltip(|cx| {
+                                        Tooltip::for_action(
+                                            "Toggle whole word",
+                                            &ToggleWholeWord,
+                                            cx,
+                                        )
+                                    })
+                                    .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
+                                    })),
+                            )
                         }),
                 ),
         );

crates/story/Cargo.toml 🔗

@@ -8,3 +8,5 @@ publish = false
 
 [dependencies]
 gpui = { package = "gpui2", path = "../gpui2" }
+smallvec.workspace = true
+itertools = {package = "itertools", version = "0.10"}

crates/story/src/story.rs 🔗

@@ -1,22 +1,199 @@
-use gpui::prelude::*;
-use gpui::{div, hsla, Div, SharedString};
+use gpui::{
+    div, hsla, prelude::*, px, rems, AnyElement, Div, ElementId, Hsla, SharedString, Stateful,
+    WindowContext,
+};
+use itertools::Itertools;
+use smallvec::SmallVec;
+
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+static COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+pub fn reasonably_unique_id() -> String {
+    let now = SystemTime::now();
+    let timestamp = now.duration_since(UNIX_EPOCH).unwrap();
+
+    let cnt = COUNTER.fetch_add(1, Ordering::Relaxed);
+
+    let id = format!("{}_{}", timestamp.as_nanos(), cnt);
+
+    id
+}
+
+pub struct StoryColor {
+    pub primary: Hsla,
+    pub secondary: Hsla,
+    pub border: Hsla,
+    pub background: Hsla,
+    pub card_background: Hsla,
+    pub divider: Hsla,
+    pub link: Hsla,
+}
+
+impl StoryColor {
+    pub fn new() -> Self {
+        Self {
+            primary: hsla(216. / 360., 11. / 100., 0. / 100., 1.),
+            secondary: hsla(216. / 360., 11. / 100., 16. / 100., 1.),
+            border: hsla(216. / 360., 11. / 100., 91. / 100., 1.),
+            background: hsla(0. / 360., 0. / 100., 100. / 100., 1.),
+            card_background: hsla(0. / 360., 0. / 100., 96. / 100., 1.),
+            divider: hsla(216. / 360., 11. / 100., 86. / 100., 1.),
+            link: hsla(206. / 360., 100. / 100., 50. / 100., 1.),
+        }
+    }
+}
+
+pub fn story_color() -> StoryColor {
+    StoryColor::new()
+}
+
+#[derive(IntoElement)]
+pub struct StoryContainer {
+    title: SharedString,
+    relative_path: &'static str,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl StoryContainer {
+    pub fn new(title: impl Into<SharedString>, relative_path: &'static str) -> Self {
+        Self {
+            title: title.into(),
+            relative_path,
+            children: SmallVec::new(),
+        }
+    }
+}
+
+impl ParentElement for StoryContainer {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for StoryContainer {
+    type Rendered = Stateful<Div>;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .id("story_container")
+            .bg(story_color().background)
+            .child(
+                div()
+                    .flex()
+                    .flex_none()
+                    .w_full()
+                    .justify_between()
+                    .p_2()
+                    .bg(story_color().background)
+                    .border_b()
+                    .border_color(story_color().border)
+                    .child(Story::title(self.title))
+                    .child(
+                        div()
+                            .text_xs()
+                            .text_color(story_color().primary)
+                            .child(Story::open_story_link(self.relative_path)),
+                    ),
+            )
+            .child(
+                div()
+                    .w_full()
+                    .h_px()
+                    .flex_1()
+                    .id("story_body")
+                    .overflow_hidden_x()
+                    .overflow_y_scroll()
+                    .flex()
+                    .flex_col()
+                    .pb_4()
+                    .children(self.children),
+            )
+    }
+}
 
 pub struct Story {}
 
 impl Story {
     pub fn container() -> Div {
-        div().size_full().flex().flex_col().pt_2().px_4().bg(hsla(
-            0. / 360.,
-            0. / 100.,
-            100. / 100.,
-            1.,
-        ))
+        div().size_full().overflow_hidden().child(
+            div()
+                .id("story_container")
+                .overflow_y_scroll()
+                .w_full()
+                .min_h_full()
+                .flex()
+                .flex_col()
+                .bg(story_color().background),
+        )
+    }
+
+    // TODO: Move all stories to container2, then rename
+    pub fn container2<T>(relative_path: &'static str) -> Div {
+        div().size_full().child(
+            div()
+                .size_full()
+                .id("story_container")
+                .overflow_y_scroll()
+                .flex()
+                .flex_col()
+                .flex_none()
+                .child(
+                    div()
+                        .flex()
+                        .justify_between()
+                        .p_2()
+                        .border_b()
+                        .border_color(story_color().border)
+                        .child(Story::title_for::<T>())
+                        .child(
+                            div()
+                                .text_xs()
+                                .text_color(story_color().primary)
+                                .child(Story::open_story_link(relative_path)),
+                        ),
+                )
+                .child(
+                    div()
+                        .w_full()
+                        .min_h_full()
+                        .flex()
+                        .flex_col()
+                        .bg(story_color().background),
+                ),
+        )
+    }
+
+    pub fn open_story_link(relative_path: &'static str) -> impl Element {
+        let path = PathBuf::from_iter([relative_path]);
+
+        div()
+            .flex()
+            .gap_2()
+            .text_xs()
+            .text_color(story_color().primary)
+            .id(SharedString::from(format!("id_{}", relative_path)))
+            .on_click({
+                let path = path.clone();
+
+                move |_event, _cx| {
+                    let path = format!("{}:0:0", path.to_string_lossy());
+
+                    std::process::Command::new("zed").arg(path).spawn().ok();
+                }
+            })
+            .children(vec![div().child(Story::link("Open in Zed →"))])
     }
 
     pub fn title(title: impl Into<SharedString>) -> impl Element {
         div()
-            .text_xl()
-            .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+            .text_xs()
+            .text_color(story_color().primary)
             .child(title.into())
     }
 
@@ -24,12 +201,185 @@ impl Story {
         Self::title(std::any::type_name::<T>())
     }
 
+    pub fn section() -> Div {
+        div()
+            .p_4()
+            .m_4()
+            .border()
+            .border_color(story_color().border)
+    }
+
+    pub fn section_title() -> Div {
+        div().text_lg().text_color(story_color().primary)
+    }
+
+    pub fn group() -> Div {
+        div().my_2().bg(story_color().background)
+    }
+
+    pub fn code_block(code: impl Into<SharedString>) -> Div {
+        div()
+            .size_full()
+            .p_2()
+            .max_w(rems(36.))
+            .bg(gpui::black())
+            .rounded_md()
+            .text_sm()
+            .text_color(gpui::white())
+            .overflow_hidden()
+            .child(code.into())
+    }
+
+    pub fn divider() -> Div {
+        div().my_2().h(px(1.)).bg(story_color().divider)
+    }
+
+    pub fn link(link: impl Into<SharedString>) -> impl Element {
+        div()
+            .id(ElementId::from(SharedString::from(reasonably_unique_id())))
+            .text_xs()
+            .text_color(story_color().link)
+            .cursor(gpui::CursorStyle::PointingHand)
+            .child(link.into())
+    }
+
+    pub fn description(description: impl Into<SharedString>) -> impl Element {
+        div()
+            .text_sm()
+            .text_color(story_color().secondary)
+            .min_w_96()
+            .child(description.into())
+    }
+
     pub fn label(label: impl Into<SharedString>) -> impl Element {
         div()
-            .mt_4()
-            .mb_2()
             .text_xs()
-            .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+            .text_color(story_color().primary)
             .child(label.into())
     }
+
+    /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate.
+    pub fn v_stack() -> Div {
+        div().flex().flex_col().gap_1()
+    }
+}
+
+#[derive(IntoElement)]
+pub struct StoryItem {
+    label: SharedString,
+    item: AnyElement,
+    description: Option<SharedString>,
+    usage: Option<SharedString>,
+}
+
+impl StoryItem {
+    pub fn new(label: impl Into<SharedString>, item: impl IntoElement) -> Self {
+        Self {
+            label: label.into(),
+            item: item.into_any_element(),
+            description: None,
+            usage: None,
+        }
+    }
+
+    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+
+    pub fn usage(mut self, code: impl Into<SharedString>) -> Self {
+        self.usage = Some(code.into());
+        self
+    }
+}
+
+impl RenderOnce for StoryItem {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .my_2()
+            .flex()
+            .gap_4()
+            .w_full()
+            .child(
+                Story::v_stack()
+                    .px_2()
+                    .w_1_2()
+                    .min_h_px()
+                    .child(Story::label(self.label))
+                    .child(
+                        div()
+                            .rounded_md()
+                            .bg(story_color().card_background)
+                            .border()
+                            .border_color(story_color().border)
+                            .py_1()
+                            .px_2()
+                            .overflow_hidden()
+                            .child(self.item),
+                    )
+                    .when_some(self.description, |this, description| {
+                        this.child(Story::description(description))
+                    }),
+            )
+            .child(
+                Story::v_stack()
+                    .px_2()
+                    .flex_none()
+                    .w_1_2()
+                    .min_h_px()
+                    .when_some(self.usage, |this, usage| {
+                        this.child(Story::label("Example Usage"))
+                            .child(Story::code_block(usage))
+                    }),
+            )
+    }
+}
+
+#[derive(IntoElement)]
+pub struct StorySection {
+    description: Option<SharedString>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl StorySection {
+    pub fn new() -> Self {
+        Self {
+            description: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+}
+
+impl RenderOnce for StorySection {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let children: SmallVec<[AnyElement; 2]> = SmallVec::from_iter(Itertools::intersperse_with(
+            self.children.into_iter(),
+            || Story::divider().into_any_element(),
+        ));
+
+        Story::section()
+            // Section title
+            .py_2()
+            // Section description
+            .when_some(self.description.clone(), |section, description| {
+                section.child(Story::description(description))
+            })
+            .child(div().flex().flex_col().gap_2().children(children))
+            .child(Story::divider())
+    }
+}
+
+impl ParentElement for StorySection {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
 }

crates/storybook2/Cargo.toml 🔗

@@ -18,6 +18,7 @@ dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
 editor = { package = "editor2", path = "../editor2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
+indoc.workspace = true
 itertools = "0.11.0"
 language = { package = "language2", path = "../language2" }
 log.workspace = true

crates/storybook2/src/stories/text.rs 🔗

@@ -1,8 +1,9 @@
 use gpui::{
-    blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render,
-    Styled, StyledText, View, VisualContext, WindowContext,
+    div, green, red, Component, HighlightStyle, InteractiveText, IntoElement, ParentElement,
+    Render, Styled, StyledText, View, VisualContext, WindowContext,
 };
-use ui::v_stack;
+use indoc::indoc;
+use story::*;
 
 pub struct TextStory;
 
@@ -13,62 +14,164 @@ impl TextStory {
 }
 
 impl Render for TextStory {
-    type Element = Div;
+    type Element = Component<StoryContainer>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        v_stack()
-            .bg(blue())
-            .child(
-                div()
-                    .flex()
-                    .child(div().max_w_96().bg(white()).child(concat!(
-        "max-width: 96. The quick brown fox jumps over the lazy dog. ",
-        "Meanwhile, the lazy dog decided it was time for a change. ",
-        "He started daily workout routines, ate healthier and became the fastest dog in town.",
-    ))),
-            )
-            .child(div().h_5())
-            .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
-        "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
-        "Meanwhile, the lazy dog decided it was time for a change. ",
-        "He started daily workout routines, ate healthier and became the fastest dog in town.",
-    )))
-            .child(div().h_5())
-            .child(
-                div()
-                    .flex()
-                    .child(div().min_w_96().bg(white()).child(concat!(
-    "min-width: 96. The quick brown fox jumps over the lazy dog. ",
-    "Meanwhile, the lazy dog decided it was time for a change. ",
-    "He started daily workout routines, ate healthier and became the fastest dog in town.",
-))))
-            .child(div().h_5())
-            .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
-        "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
-        "Meanwhile, the lazy dog decided it was time for a change. ",
-        "He started daily workout routines, ate healthier and became the fastest dog in town.",
-    ))))
-            // NOTE: When rendering text in a horizonal flex container,
-            // Taffy will not pass width constraints down from the parent.
-            // To fix this, render text in a praent with overflow: hidden, which
-                    .child(div().h_5())
-                    .child(div().flex().w_96().bg(red()).child(concat!(
-                "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
-                "Meanwhile, the lazy dog decided it was time for a change. ",
-                "He started daily workout routines, ate healthier and became the fastest dog in town.",
-            ))).child(
-                InteractiveText::new(
-                    "interactive",
-                    StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
-                        (6..11, HighlightStyle {
-                            background_color: Some(green()),
-                            ..Default::default()
+        StoryContainer::new("Text Story", "crates/storybook2/src/stories/text.rs")
+            .children(
+                vec![
+
+            StorySection::new()
+                .child(
+                    StoryItem::new("Default", div().bg(gpui::blue()).child("Hello World!"))
+                        .usage(indoc! {r##"
+                            div()
+                                .child("Hello World!")
+                            "##
                         }),
-                    ]),
                 )
-                .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
-                    println!("Clicked range {range_ix}");
-                })
-            )
+                .child(
+                    StoryItem::new("Wrapping Text",
+                        div().max_w_96()
+                            .child(
+                                concat!(
+                                    "The quick brown fox jumps over the lazy dog. ",
+                                    "Meanwhile, the lazy dog decided it was time for a change. ",
+                                    "He started daily workout routines, ate healthier and became the fastest dog in town.",
+                                )
+                            )
+                    )
+                    .description("Set a width or max-width to enable text wrapping.")
+                    .usage(indoc! {r##"
+                        div()
+                            .max_w_96()
+                            .child("Some text that you want to wrap.")
+                        "##
+                    })
+                )
+                .child(
+                    StoryItem::new("tbd",
+                    div().flex().w_96().child(div().overflow_hidden().child(concat!(
+                            "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+                            "Meanwhile, the lazy dog decided it was time for a change. ",
+                            "He started daily workout routines, ate healthier and became the fastest dog in town.",
+                        )))
+                    )
+                )
+                .child(
+                    StoryItem::new("Text in Horizontal Flex",
+                        div().flex().w_96().bg(red()).child(concat!(
+                                        "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+                                        "Meanwhile, the lazy dog decided it was time for a change. ",
+                                        "He started daily workout routines, ate healthier and became the fastest dog in town.",
+                                    ))
+                    )
+                    .usage(indoc! {r##"
+                        // NOTE: When rendering text in a horizonal flex container,
+                        // Taffy will not pass width constraints down from the parent.
+                        // To fix this, render text in a parent with overflow: hidden
+
+                        div()
+                            .max_w_96()
+                            .child("Some text that you want to wrap.")
+                        "##
+                    })
+                )
+                .child(
+                    StoryItem::new("Interactive Text",
+                        InteractiveText::new(
+                            "interactive",
+                            StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+                                (6..11, HighlightStyle {
+                                    background_color: Some(green()),
+                                    ..Default::default()
+                                }),
+                            ]),
+                        )
+                        .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+                            println!("Clicked range {range_ix}");
+                        })
+                    )
+                    .usage(indoc! {r##"
+                        InteractiveText::new(
+                            "interactive",
+                            StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+                                (6..11, HighlightStyle {
+                                    background_color: Some(green()),
+                                    ..Default::default()
+                                }),
+                            ]),
+                        )
+                        .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+                            println!("Clicked range {range_ix}");
+                        })
+                        "##
+                    })
+                )
+        ]
+            ).into_element()
     }
 }
+
+// TODO: Check all were updated to new style and remove
+
+// impl Render for TextStory {
+//     type Element = Div;
+
+//     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+//         v_stack()
+//             .bg(blue())
+//             .child(
+//                 div()
+//                     .flex()
+//                     .child(div().max_w_96().bg(white()).child(concat!(
+//         "max-width: 96. The quick brown fox jumps over the lazy dog. ",
+//         "Meanwhile, the lazy dog decided it was time for a change. ",
+//         "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//     ))),
+//             )
+//             .child(div().h_5())
+//             .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
+//         "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
+//         "Meanwhile, the lazy dog decided it was time for a change. ",
+//         "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//     )))
+//             .child(div().h_5())
+//             .child(
+//                 div()
+//                     .flex()
+//                     .child(div().min_w_96().bg(white()).child(concat!(
+//     "min-width: 96. The quick brown fox jumps over the lazy dog. ",
+//     "Meanwhile, the lazy dog decided it was time for a change. ",
+//     "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// ))))
+//             .child(div().h_5())
+//             .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
+//         "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+//         "Meanwhile, the lazy dog decided it was time for a change. ",
+//         "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//     ))))
+//             // NOTE: When rendering text in a horizonal flex container,
+//             // Taffy will not pass width constraints down from the parent.
+//             // To fix this, render text in a parent with overflow: hidden
+//                     .child(div().h_5())
+//                     .child(div().flex().w_96().bg(red()).child(concat!(
+//                 "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+//                 "Meanwhile, the lazy dog decided it was time for a change. ",
+//                 "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//             ))).child(
+//                 InteractiveText::new(
+//                     "interactive",
+//                     StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+//                         (6..11, HighlightStyle {
+//                             background_color: Some(green()),
+//                             ..Default::default()
+//                         }),
+//                     ]),
+//                 )
+//                 .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+//                     println!("Clicked range {range_ix}");
+//                 })
+//             )
+//     }
+// }

crates/storybook2/src/storybook2.rs 🔗

@@ -19,6 +19,7 @@ use ui::prelude::*;
 
 use crate::assets::Assets;
 use crate::story_selector::{ComponentStory, StorySelector};
+pub use indoc::indoc;
 
 // gpui::actions! {
 //     storybook,

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

@@ -1,5 +1,5 @@
-use gpui::{Div, Render};
-use story::Story;
+use gpui::{Component, Render};
+use story::{StoryContainer, StoryItem, StorySection};
 
 use crate::{prelude::*, Tooltip};
 use crate::{Icon, IconButton};
@@ -7,57 +7,167 @@ use crate::{Icon, IconButton};
 pub struct IconButtonStory;
 
 impl Render for IconButtonStory {
-    type Element = Div;
+    type Element = Component<StoryContainer>;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        Story::container()
-            .child(Story::title_for::<IconButton>())
-            .child(Story::label("Default"))
-            .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
-            .child(Story::label("Selected"))
-            .child(
-                div()
-                    .w_8()
-                    .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
-            )
-            .child(Story::label("Selected with `selected_icon`"))
-            .child(
-                div().w_8().child(
-                    IconButton::new("icon_a", Icon::AudioOn)
-                        .selected(true)
-                        .selected_icon(Icon::AudioOff),
-                ),
-            )
-            .child(Story::label("Disabled"))
-            .child(
-                div()
-                    .w_8()
-                    .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
-            )
-            .child(Story::label("With `on_click`"))
-            .child(
-                div()
-                    .w_8()
-                    .child(
-                        IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
-                            println!("Clicked!");
-                        }),
-                    ),
-            )
-            .child(Story::label("With `tooltip`"))
-            .child(
-                div().w_8().child(
-                    IconButton::new("with_tooltip", Icon::MessageBubbles)
-                        .tooltip(|cx| Tooltip::text("Open messages", cx)),
-                ),
-            )
-            .child(Story::label("Selected with `tooltip`"))
-            .child(
-                div().w_8().child(
-                    IconButton::new("selected_with_tooltip", Icon::InlayHint)
-                        .selected(true)
-                        .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
-                ),
-            )
+        let default_button = StoryItem::new(
+            "Default",
+            IconButton::new("default_icon_button", Icon::Hash),
+        )
+        .description("Displays an icon button.")
+        .usage(
+            r#"
+            IconButton::new("default_icon_button", Icon::Hash)
+        "#,
+        );
+
+        let selected_button = StoryItem::new(
+            "Selected",
+            IconButton::new("selected_icon_button", Icon::Hash).selected(true),
+        )
+        .description("Displays an icon button that is selected.")
+        .usage(
+            r#"
+            IconButton::new("selected_icon_button", Icon::Hash).selected(true)
+        "#,
+        );
+
+        let selected_with_selected_icon = StoryItem::new(
+            "Selected with `selected_icon`",
+            IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+                .selected(true)
+                .selected_icon(Icon::AudioOff),
+        )
+        .description(
+            "Displays an icon button that is selected and shows a different icon when selected.",
+        )
+        .usage(
+            r#"
+            IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+                .selected(true)
+                .selected_icon(Icon::AudioOff)
+        "#,
+        );
+
+        let disabled_button = StoryItem::new(
+            "Disabled",
+            IconButton::new("disabled_icon_button", Icon::Hash).disabled(true),
+        )
+        .description("Displays an icon button that is disabled.")
+        .usage(
+            r#"
+            IconButton::new("disabled_icon_button", Icon::Hash).disabled(true)
+        "#,
+        );
+
+        let with_on_click_button = StoryItem::new(
+            "With `on_click`",
+            IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+                println!("Clicked!");
+            }),
+        )
+        .description("Displays an icon button which triggers an event on click.")
+        .usage(
+            r#"
+            IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+                println!("Clicked!");
+            })
+        "#,
+        );
+
+        let with_tooltip_button = StoryItem::new(
+            "With `tooltip`",
+            IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+                .tooltip(|cx| Tooltip::text("Open messages", cx)),
+        )
+        .description("Displays an icon button that has a tooltip when hovered.")
+        .usage(
+            r#"
+            IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+                .tooltip(|cx| Tooltip::text("Open messages", cx))
+        "#,
+        );
+
+        let selected_with_tooltip_button = StoryItem::new(
+            "Selected with `tooltip`",
+            IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+                .selected(true)
+                .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
+        )
+        .description("Displays a selected icon button with tooltip.")
+        .usage(
+            r#"
+            IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+                .selected(true)
+                .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx))
+        "#,
+        );
+
+        let buttons = vec![
+            default_button,
+            selected_button,
+            selected_with_selected_icon,
+            disabled_button,
+            with_on_click_button,
+            with_tooltip_button,
+            selected_with_tooltip_button,
+        ];
+
+        StoryContainer::new(
+            "Icon Button",
+            "crates/ui2/src/components/stories/icon_button.rs",
+        )
+        .children(vec![StorySection::new().children(buttons)])
+        .into_element()
+
+        // Story::container()
+        //     .child(Story::title_for::<IconButton>())
+        //     .child(Story::label("Default"))
+        //     .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
+        //     .child(Story::label("Selected"))
+        //     .child(
+        //         div()
+        //             .w_8()
+        //             .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
+        //     )
+        //     .child(Story::label("Selected with `selected_icon`"))
+        //     .child(
+        //         div().w_8().child(
+        //             IconButton::new("icon_a", Icon::AudioOn)
+        //                 .selected(true)
+        //                 .selected_icon(Icon::AudioOff),
+        //         ),
+        //     )
+        //     .child(Story::label("Disabled"))
+        //     .child(
+        //         div()
+        //             .w_8()
+        //             .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
+        //     )
+        //     .child(Story::label("With `on_click`"))
+        //     .child(
+        //         div()
+        //             .w_8()
+        //             .child(
+        //                 IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
+        //                     println!("Clicked!");
+        //                 }),
+        //             ),
+        //     )
+        //     .child(Story::label("With `tooltip`"))
+        //     .child(
+        //         div().w_8().child(
+        //             IconButton::new("with_tooltip", Icon::MessageBubbles)
+        //                 .tooltip(|cx| Tooltip::text("Open messages", cx)),
+        //         ),
+        //     )
+        //     .child(Story::label("Selected with `tooltip`"))
+        //     .child(
+        //         div().w_8().child(
+        //             IconButton::new("selected_with_tooltip", Icon::InlayHint)
+        //                 .selected(true)
+        //                 .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
+        //         ),
+        //     )
     }
 }

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

@@ -78,13 +78,13 @@ impl Render for Tooltip {
                 v_stack()
                     .elevation_2(cx)
                     .font(ui_font)
-                    .text_ui_sm()
+                    .text_ui()
                     .text_color(cx.theme().colors().text)
                     .py_1()
                     .px_2()
                     .child(
                         h_stack()
-                            .gap_2()
+                            .gap_4()
                             .child(self.title.clone())
                             .when_some(self.key_binding.clone(), |this, key_binding| {
                                 this.justify_between().child(key_binding)

crates/vcs_menu2/src/lib.rs 🔗

@@ -65,8 +65,13 @@ impl ModalBranchList {
     ) -> Result<()> {
         // Modal branch picker has a longer trailoff than a popover one.
         let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
-        workspace.toggle_modal(cx, |cx| ModalBranchList {
-            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+        workspace.toggle_modal(cx, |cx| {
+            let modal = ModalBranchList {
+                picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+            };
+            cx.subscribe(&modal.picker, |_, _, _, cx| cx.emit(DismissEvent))
+                .detach();
+            modal
         });
 
         Ok(())

crates/workspace2/src/pane.rs 🔗

@@ -1897,19 +1897,14 @@ impl Render for Pane {
                     .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
                     .map(|div| {
                         if let Some(item) = self.active_item() {
-                            div.flex_col()
+                            div.v_flex()
                                 .child(self.toolbar.clone())
                                 .child(item.to_any())
                         } else {
-                            div.flex()
-                                .flex_row()
-                                .items_center()
-                                .size_full()
-                                .justify_center()
-                                .child(
-                                    Label::new("Open a file or project to get started.")
-                                        .color(Color::Muted),
-                                )
+                            div.h_flex().size_full().justify_center().child(
+                                Label::new("Open a file or project to get started.")
+                                    .color(Color::Muted),
+                            )
                         }
                     })
                     .child(

crates/workspace2/src/pane_group.rs 🔗

@@ -693,7 +693,8 @@ mod element {
 
     use gpui::{
         px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
-        MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Style, WindowContext,
+        MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style,
+        WindowContext,
     };
     use parking_lot::Mutex;
     use smallvec::SmallVec;
@@ -736,7 +737,8 @@ mod element {
             e: &MouseMoveEvent,
             ix: usize,
             axis: Axis,
-            axis_bounds: Bounds<Pixels>,
+            child_start: Point<Pixels>,
+            container_size: Size<Pixels>,
             cx: &mut WindowContext,
         ) {
             let min_size = match axis {
@@ -747,7 +749,7 @@ mod element {
             debug_assert!(flex_values_in_bounds(flexes.as_slice()));
 
             let size = move |ix, flexes: &[f32]| {
-                axis_bounds.size.along(axis) * (flexes[ix] / flexes.len() as f32)
+                container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
             };
 
             // Don't allow resizing to less than the minimum size, if elements are already too small
@@ -756,10 +758,10 @@ mod element {
             }
 
             let mut proposed_current_pixel_change =
-                (e.position - axis_bounds.origin).along(axis) - size(ix, flexes.as_slice());
+                (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
 
             let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
-                let flex_change = pixel_dx / axis_bounds.size.along(axis);
+                let flex_change = pixel_dx / container_size.along(axis);
                 let current_target_flex = flexes[target_ix] + flex_change;
                 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
                 (current_target_flex, next_target_flex)
@@ -854,7 +856,15 @@ mod element {
                 cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
                     let dragged_handle = dragged_handle.borrow();
                     if phase.bubble() && *dragged_handle == Some(ix) {
-                        Self::compute_resize(&flexes, e, ix, axis, axis_bounds, cx)
+                        Self::compute_resize(
+                            &flexes,
+                            e,
+                            ix,
+                            axis,
+                            pane_bounds.origin,
+                            axis_bounds.size,
+                            cx,
+                        )
                     }
                 });
             });