Merge branch 'main' into feedback-modal-ui

Joseph T. Lyons created

Change summary

crates/assistant2/src/assistant_panel.rs         |  49 +-
crates/editor2/src/editor.rs                     |  24 
crates/editor2/src/element.rs                    | 115 +++---
crates/gpui2/src/app.rs                          |  17 
crates/gpui2/src/element.rs                      |  18 -
crates/gpui2/src/interactive.rs                  |   6 
crates/gpui2/src/platform/mac/metal_renderer.rs  |   5 
crates/gpui2/src/window.rs                       | 266 ++++++++---------
crates/gpui2_macros/src/action.rs                |   1 
crates/quick_action_bar2/src/quick_action_bar.rs |   8 
crates/recent_projects2/src/projects.rs          |   1 
crates/recent_projects2/src/recent_projects.rs   |   7 
crates/search2/src/buffer_search.rs              |  33 +
crates/storybook2/src/story_selector.rs          |   2 
crates/terminal2/src/terminal2.rs                |  12 
crates/terminal_view2/src/terminal_element.rs    |  41 ++
crates/terminal_view2/src/terminal_view.rs       |  10 
crates/theme_selector2/src/theme_selector.rs     |   4 
crates/ui2/src/components.rs                     |   2 
crates/ui2/src/components/keybinding.rs          |   6 
crates/ui2/src/components/stories.rs             |   2 
crates/ui2/src/components/stories/tab.rs         | 114 +++++++
crates/ui2/src/components/tab.rs                 | 198 +++++++++++++
crates/workspace2/src/dock.rs                    |  19 
crates/workspace2/src/pane.rs                    | 133 ++------
crates/workspace2/src/workspace2.rs              |  22 
26 files changed, 701 insertions(+), 414 deletions(-)

Detailed changes

crates/assistant2/src/assistant_panel.rs 🔗

@@ -331,7 +331,7 @@ impl AssistantPanel {
 
         let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
         let inline_assistant = cx.build_view(|cx| {
-            let assistant = InlineAssistant::new(
+            InlineAssistant::new(
                 inline_assist_id,
                 measurements.clone(),
                 self.include_conversation_in_next_inline_assist,
@@ -342,9 +342,7 @@ impl AssistantPanel {
                 self.retrieve_context_in_next_inline_assist,
                 self.semantic_index.clone(),
                 project.clone(),
-            );
-            assistant.focus_handle.focus(cx);
-            assistant
+            )
         });
         let block_id = editor.update(cx, |editor, cx| {
             editor.change_selections(None, cx, |selections| {
@@ -391,10 +389,7 @@ impl AssistantPanel {
                             if let Some(inline_assistant) = inline_assistant.upgrade() {
                                 if let EditorEvent::SelectionsChanged { local } = event {
                                     if *local
-                                        && inline_assistant
-                                            .read(cx)
-                                            .focus_handle
-                                            .contains_focused(cx)
+                                        && inline_assistant.focus_handle(cx).contains_focused(cx)
                                     {
                                         cx.focus_view(&editor);
                                     }
@@ -553,9 +548,12 @@ impl AssistantPanel {
     fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
         if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
             if let Some(editor) = pending_assist.editor.upgrade() {
-                if let Some((block_id, _)) = pending_assist.inline_assistant.take() {
+                if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() {
                     editor.update(cx, |editor, cx| {
                         editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
+                        if inline_assistant.focus_handle(cx).contains_focused(cx) {
+                            editor.focus(cx);
+                        }
                     });
                 }
             }
@@ -891,8 +889,9 @@ impl AssistantPanel {
                 if search_bar.show(cx) {
                     search_bar.search_suggested(cx);
                     if action.focus {
+                        let focus_handle = search_bar.focus_handle(cx);
                         search_bar.select_query(cx);
-                        cx.focus_self();
+                        cx.focus(&focus_handle);
                     }
                     propagate = false
                 }
@@ -956,7 +955,7 @@ impl AssistantPanel {
     }
 
     fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("split_button", Icon::Menu)
+        IconButton::new("split_button", Icon::SplitMessage)
             .on_click(cx.listener(|this, _event, cx| {
                 if let Some(active_editor) = this.active_editor() {
                     active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
@@ -966,7 +965,7 @@ impl AssistantPanel {
     }
 
     fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("assist_button", Icon::Menu)
+        IconButton::new("assist_button", Icon::MagicWand)
             .on_click(cx.listener(|this, _event, cx| {
                 if let Some(active_editor) = this.active_editor() {
                     active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
@@ -976,7 +975,7 @@ impl AssistantPanel {
     }
 
     fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("quote_button", Icon::Menu)
+        IconButton::new("quote_button", Icon::Quote)
             .on_click(cx.listener(|this, _event, cx| {
                 if let Some(workspace) = this.workspace.upgrade() {
                     cx.window_context().defer(move |cx| {
@@ -990,7 +989,7 @@ impl AssistantPanel {
     }
 
     fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
-        IconButton::new("plus_button", Icon::Menu)
+        IconButton::new("plus_button", Icon::Plus)
             .on_click(cx.listener(|this, _event, cx| {
                 this.new_conversation(cx);
             }))
@@ -999,7 +998,7 @@ impl AssistantPanel {
 
     fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let zoomed = self.zoomed;
-        IconButton::new("zoom_button", Icon::Menu)
+        IconButton::new("zoom_button", Icon::MagnifyingGlass)
             .on_click(cx.listener(|this, _event, cx| {
                 this.toggle_zoom(&ToggleZoom, cx);
             }))
@@ -1028,6 +1027,8 @@ impl AssistantPanel {
     }
 
     fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+        cx.focus(&self.focus_handle);
+
         if let Some(ix) = self.editor_index_for_path(&path, cx) {
             self.set_active_editor_index(Some(ix), cx);
             return Task::ready(Ok(()));
@@ -2026,7 +2027,6 @@ struct ConversationEditor {
     editor: View<Editor>,
     blocks: HashSet<BlockId>,
     scroll_position: Option<ScrollPosition>,
-    focus_handle: FocusHandle,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -2057,13 +2057,10 @@ impl ConversationEditor {
             editor
         });
 
-        let focus_handle = cx.focus_handle();
-
         let _subscriptions = vec![
             cx.observe(&conversation, |_, _, cx| cx.notify()),
             cx.subscribe(&conversation, Self::handle_conversation_event),
             cx.subscribe(&editor, Self::handle_editor_event),
-            cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.editor)),
         ];
 
         let mut this = Self {
@@ -2073,7 +2070,6 @@ impl ConversationEditor {
             scroll_position: None,
             fs,
             workspace,
-            focus_handle,
             _subscriptions,
         };
         this.update_message_headers(cx);
@@ -2484,8 +2480,8 @@ impl Render for ConversationEditor {
 }
 
 impl FocusableView for ConversationEditor {
-    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
-        self.focus_handle.clone()
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.editor.focus_handle(cx)
     }
 }
 
@@ -2539,7 +2535,6 @@ struct InlineAssistant {
     prompt_editor: View<Editor>,
     workspace: WeakView<Workspace>,
     confirmed: bool,
-    focus_handle: FocusHandle,
     include_conversation: bool,
     measurements: Rc<Cell<BlockMeasurements>>,
     prompt_history: VecDeque<String>,
@@ -2635,8 +2630,8 @@ impl Render for InlineAssistant {
 }
 
 impl FocusableView for InlineAssistant {
-    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
-        self.focus_handle.clone()
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.prompt_editor.focus_handle(cx)
     }
 }
 
@@ -2662,12 +2657,11 @@ impl InlineAssistant {
             editor.set_placeholder_text(placeholder, cx);
             editor
         });
+        cx.focus_view(&prompt_editor);
 
-        let focus_handle = cx.focus_handle();
         let mut subscriptions = vec![
             cx.observe(&codegen, Self::handle_codegen_changed),
             cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
-            cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.prompt_editor)),
         ];
 
         if let Some(semantic_index) = semantic_index.clone() {
@@ -2679,7 +2673,6 @@ impl InlineAssistant {
             prompt_editor,
             workspace,
             confirmed: false,
-            focus_handle,
             include_conversation,
             measurements,
             prompt_history,

crates/editor2/src/editor.rs 🔗

@@ -1814,34 +1814,34 @@ impl Editor {
         this
     }
 
-    fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
-        let mut dispatch_context = KeyContext::default();
-        dispatch_context.add("Editor");
+    fn key_context(&self, cx: &AppContext) -> KeyContext {
+        let mut key_context = KeyContext::default();
+        key_context.add("Editor");
         let mode = match self.mode {
             EditorMode::SingleLine => "single_line",
             EditorMode::AutoHeight { .. } => "auto_height",
             EditorMode::Full => "full",
         };
-        dispatch_context.set("mode", mode);
+        key_context.set("mode", mode);
         if self.pending_rename.is_some() {
-            dispatch_context.add("renaming");
+            key_context.add("renaming");
         }
         if self.context_menu_visible() {
             match self.context_menu.read().as_ref() {
                 Some(ContextMenu::Completions(_)) => {
-                    dispatch_context.add("menu");
-                    dispatch_context.add("showing_completions")
+                    key_context.add("menu");
+                    key_context.add("showing_completions")
                 }
                 Some(ContextMenu::CodeActions(_)) => {
-                    dispatch_context.add("menu");
-                    dispatch_context.add("showing_code_actions")
+                    key_context.add("menu");
+                    key_context.add("showing_code_actions")
                 }
                 None => {}
             }
         }
 
         for layer in self.keymap_context_layers.values() {
-            dispatch_context.extend(layer);
+            key_context.extend(layer);
         }
 
         if let Some(extension) = self
@@ -1850,10 +1850,10 @@ impl Editor {
             .as_singleton()
             .and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
         {
-            dispatch_context.set("extension", extension.to_string());
+            key_context.set("extension", extension.to_string());
         }
 
-        dispatch_context
+        key_context
     }
 
     pub fn new_file(

crates/editor2/src/element.rs 🔗

@@ -275,36 +275,48 @@ impl EditorElement {
         register_action(view, cx, Editor::copy_relative_path);
         register_action(view, cx, Editor::copy_highlight_json);
         register_action(view, cx, |editor, action, cx| {
-            editor
-                .format(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
+            if let Some(task) = editor.format(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
         });
         register_action(view, cx, Editor::restart_language_server);
         register_action(view, cx, Editor::show_character_palette);
         register_action(view, cx, |editor, action, cx| {
-            editor
-                .confirm_completion(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
+            if let Some(task) = editor.confirm_completion(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
         });
         register_action(view, cx, |editor, action, cx| {
-            editor
-                .confirm_code_action(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
+            if let Some(task) = editor.confirm_code_action(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
         });
         register_action(view, cx, |editor, action, cx| {
-            editor
-                .rename(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
+            if let Some(task) = editor.rename(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
         });
         register_action(view, cx, |editor, action, cx| {
-            editor
-                .confirm_rename(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
+            if let Some(task) = editor.confirm_rename(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
         });
         register_action(view, cx, |editor, action, cx| {
-            editor
-                .find_all_references(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
+            if let Some(task) = editor.find_all_references(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
         });
         register_action(view, cx, Editor::next_copilot_suggestion);
         register_action(view, cx, Editor::previous_copilot_suggestion);
@@ -2802,49 +2814,38 @@ impl Element for EditorElement {
         };
 
         let focus_handle = editor.focus_handle(cx);
-        let dispatch_context = self.editor.read(cx).dispatch_context(cx);
-        cx.with_key_dispatch(
-            Some(dispatch_context),
-            Some(focus_handle.clone()),
-            |_, cx| {
-                self.register_actions(cx);
-                self.register_key_listeners(cx);
-
-                // We call with_z_index to establish a new stacking context.
-                cx.with_z_index(0, |cx| {
-                    cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                        // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
-                        // take precedence.
-                        cx.with_z_index(0, |cx| {
-                            self.paint_mouse_listeners(
-                                bounds,
-                                gutter_bounds,
-                                text_bounds,
-                                &layout,
-                                cx,
-                            );
-                        });
-                        let input_handler =
-                            ElementInputHandler::new(bounds, self.editor.clone(), cx);
-                        cx.handle_input(&focus_handle, input_handler);
+        let key_context = self.editor.read(cx).key_context(cx);
+        cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
+            self.register_actions(cx);
+            self.register_key_listeners(cx);
+
+            // We call with_z_index to establish a new stacking context.
+            cx.with_z_index(0, |cx| {
+                cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                    // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
+                    // take precedence.
+                    cx.with_z_index(0, |cx| {
+                        self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
+                    });
+                    let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
+                    cx.handle_input(&focus_handle, input_handler);
 
-                        self.paint_background(gutter_bounds, text_bounds, &layout, cx);
-                        if layout.gutter_size.width > Pixels::ZERO {
-                            self.paint_gutter(gutter_bounds, &mut layout, cx);
-                        }
-                        self.paint_text(text_bounds, &mut layout, cx);
+                    self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+                    if layout.gutter_size.width > Pixels::ZERO {
+                        self.paint_gutter(gutter_bounds, &mut layout, cx);
+                    }
+                    self.paint_text(text_bounds, &mut layout, cx);
 
-                        if !layout.blocks.is_empty() {
-                            cx.with_z_index(1, |cx| {
-                                cx.with_element_id(Some("editor_blocks"), |cx| {
-                                    self.paint_blocks(bounds, &mut layout, cx);
-                                })
+                    if !layout.blocks.is_empty() {
+                        cx.with_z_index(1, |cx| {
+                            cx.with_element_id(Some("editor_blocks"), |cx| {
+                                self.paint_blocks(bounds, &mut layout, cx);
                             })
-                        }
-                    });
+                        })
+                    }
                 });
-            },
-        )
+            });
+        })
     }
 }
 

crates/gpui2/src/app.rs 🔗

@@ -704,14 +704,13 @@ impl AppContext {
                     let focus_changed = focused.is_some() || blurred.is_some();
                     let event = FocusEvent { focused, blurred };
 
-                    let mut listeners = mem::take(&mut cx.window.current_frame.focus_listeners);
+                    let mut listeners = mem::take(&mut cx.window.rendered_frame.focus_listeners);
                     if focus_changed {
                         for listener in &mut listeners {
                             listener(&event, cx);
                         }
                     }
-                    listeners.extend(cx.window.current_frame.focus_listeners.drain(..));
-                    cx.window.current_frame.focus_listeners = listeners;
+                    cx.window.rendered_frame.focus_listeners = listeners;
 
                     if focus_changed {
                         cx.window
@@ -1029,9 +1028,13 @@ impl AppContext {
             window
                 .update(self, |_, cx| {
                     cx.window
-                        .current_frame
+                        .rendered_frame
                         .dispatch_tree
-                        .clear_pending_keystrokes()
+                        .clear_pending_keystrokes();
+                    cx.window
+                        .next_frame
+                        .dispatch_tree
+                        .clear_pending_keystrokes();
                 })
                 .ok();
         }
@@ -1107,6 +1110,10 @@ impl AppContext {
             }
         }
     }
+
+    pub fn has_active_drag(&self) -> bool {
+        self.active_drag.is_some()
+    }
 }
 
 impl Context for AppContext {

crates/gpui2/src/element.rs 🔗

@@ -69,24 +69,6 @@ pub trait IntoElement: Sized {
         self.map(|this| if condition { then(this) } else { this })
     }
 
-    fn when_else(
-        self,
-        condition: bool,
-        then: impl FnOnce(Self) -> Self,
-        otherwise: impl FnOnce(Self) -> Self,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.map(|this| {
-            if condition {
-                then(this)
-            } else {
-                otherwise(this)
-            }
-        })
-    }
-
     fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
     where
         Self: Sized,

crates/gpui2/src/interactive.rs 🔗

@@ -193,6 +193,12 @@ impl Deref for MouseExitEvent {
 #[derive(Debug, Clone, Default)]
 pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
 
+impl ExternalPaths {
+    pub fn paths(&self) -> &[PathBuf] {
+        &self.0
+    }
+}
+
 impl Render for ExternalPaths {
     type Element = Div;
 

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

@@ -325,7 +325,7 @@ impl MetalRenderer {
                 .entry(tile.texture_id)
                 .or_insert(Vec::new())
                 .extend(path.vertices.iter().map(|vertex| PathVertex {
-                    xy_position: vertex.xy_position - path.bounds.origin
+                    xy_position: vertex.xy_position - clipped_bounds.origin
                         + tile.bounds.origin.map(Into::into),
                     st_position: vertex.st_position,
                     content_mask: ContentMask {
@@ -544,9 +544,10 @@ impl MetalRenderer {
             if let Some((path, tile)) = paths_and_tiles.peek() {
                 if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) {
                     prev_texture_id = Some(tile.texture_id);
+                    let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
                     sprites.push(PathSprite {
                         bounds: Bounds {
-                            origin: path.bounds.origin.map(|p| p.floor()),
+                            origin: origin.map(|p| p.floor()),
                             size: tile.bounds.size.map(Into::into),
                         },
                         color: path.color,

crates/gpui2/src/window.rs 🔗

@@ -4,12 +4,12 @@ use crate::{
     DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
     EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
     ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
-    ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
-    MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
-    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
-    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
-    Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline,
-    UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path,
+    Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
+    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
+    RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
+    Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
+    WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::HashMap;
@@ -90,7 +90,7 @@ impl FocusId {
     /// Obtains whether this handle contains the given handle in the most recently rendered frame.
     pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool {
         cx.window
-            .current_frame
+            .rendered_frame
             .dispatch_tree
             .focus_contains(*self, other)
     }
@@ -212,8 +212,8 @@ pub struct Window {
     layout_engine: Option<TaffyLayoutEngine>,
     pub(crate) root_view: Option<AnyView>,
     pub(crate) element_id_stack: GlobalElementId,
-    pub(crate) previous_frame: Frame,
-    pub(crate) current_frame: Frame,
+    pub(crate) rendered_frame: Frame,
+    pub(crate) next_frame: Frame,
     pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
     pub(crate) focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
     default_prevented: bool,
@@ -249,7 +249,7 @@ pub(crate) struct Frame {
 }
 
 impl Frame {
-    pub fn new(dispatch_tree: DispatchTree) -> Self {
+    fn new(dispatch_tree: DispatchTree) -> Self {
         Frame {
             element_states: HashMap::default(),
             mouse_listeners: HashMap::default(),
@@ -262,6 +262,14 @@ impl Frame {
             element_offset_stack: Vec::new(),
         }
     }
+
+    fn clear(&mut self) {
+        self.element_states.clear();
+        self.mouse_listeners.values_mut().for_each(Vec::clear);
+        self.focus_listeners.clear();
+        self.dispatch_tree.clear();
+        self.depth_map.clear();
+    }
 }
 
 impl Window {
@@ -330,8 +338,8 @@ impl Window {
             layout_engine: Some(TaffyLayoutEngine::new()),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
-            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+            rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+            next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             default_prevented: true,
@@ -428,7 +436,7 @@ impl<'a> WindowContext<'a> {
 
         self.window.focus = Some(focus_id);
         self.window
-            .current_frame
+            .rendered_frame
             .dispatch_tree
             .clear_pending_keystrokes();
         self.app.push_effect(Effect::FocusChanged {
@@ -459,11 +467,11 @@ impl<'a> WindowContext<'a> {
             let node_id = focus_handle
                 .and_then(|handle| {
                     cx.window
-                        .current_frame
+                        .rendered_frame
                         .dispatch_tree
                         .focusable_node_id(handle.id)
                 })
-                .unwrap_or_else(|| cx.window.current_frame.dispatch_tree.root_node_id());
+                .unwrap_or_else(|| cx.window.rendered_frame.dispatch_tree.root_node_id());
 
             cx.propagate_event = true;
             cx.dispatch_action_on_node(node_id, action);
@@ -743,7 +751,7 @@ impl<'a> WindowContext<'a> {
         self.window.default_prevented
     }
 
-    /// Register a mouse event listener on the window for the current frame. The type of event
+    /// Register a mouse event listener on the window for the next frame. The type of event
     /// is determined by the first parameter of the given listener. When the next frame is rendered
     /// the listener will be cleared.
     ///
@@ -753,9 +761,9 @@ impl<'a> WindowContext<'a> {
         &mut self,
         handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
-        let order = self.window.current_frame.z_index_stack.clone();
+        let order = self.window.next_frame.z_index_stack.clone();
         self.window
-            .current_frame
+            .next_frame
             .mouse_listeners
             .entry(TypeId::of::<Event>())
             .or_default()
@@ -767,7 +775,7 @@ impl<'a> WindowContext<'a> {
             ))
     }
 
-    /// Register a key event listener on the window for the current frame. The type of event
+    /// Register a key event listener on the window for the next frame. The type of event
     /// is determined by the first parameter of the given listener. When the next frame is rendered
     /// the listener will be cleared.
     ///
@@ -778,7 +786,7 @@ impl<'a> WindowContext<'a> {
         handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
         self.window
-            .current_frame
+            .next_frame
             .dispatch_tree
             .on_key_event(Rc::new(move |event, phase, cx| {
                 if let Some(event) = event.downcast_ref::<Event>() {
@@ -787,7 +795,7 @@ impl<'a> WindowContext<'a> {
             }));
     }
 
-    /// Register an action listener on the window for the current frame. The type of action
+    /// Register an action listener on the window for the next frame. The type of action
     /// is determined by the first parameter of the given listener. When the next frame is rendered
     /// the listener will be cleared.
     ///
@@ -798,7 +806,7 @@ impl<'a> WindowContext<'a> {
         action_type: TypeId,
         handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static,
     ) {
-        self.window.current_frame.dispatch_tree.on_action(
+        self.window.next_frame.dispatch_tree.on_action(
             action_type,
             Rc::new(move |action, phase, cx| handler(action, phase, cx)),
         );
@@ -809,13 +817,13 @@ impl<'a> WindowContext<'a> {
             .focused()
             .and_then(|focused_handle| {
                 self.window
-                    .current_frame
+                    .rendered_frame
                     .dispatch_tree
                     .focusable_node_id(focused_handle.id)
             })
-            .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
+            .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
         self.window
-            .current_frame
+            .rendered_frame
             .dispatch_tree
             .is_action_available(action, target)
     }
@@ -832,16 +840,16 @@ impl<'a> WindowContext<'a> {
     /// Called during painting to invoke the given closure in a new stacking context. The given
     /// z-index is interpreted relative to the previous call to `stack`.
     pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
-        self.window.current_frame.z_index_stack.push(z_index);
+        self.window.next_frame.z_index_stack.push(z_index);
         let result = f(self);
-        self.window.current_frame.z_index_stack.pop();
+        self.window.next_frame.z_index_stack.pop();
         result
     }
 
     /// Called during painting to track which z-index is on top at each pixel position
     pub fn add_opaque_layer(&mut self, bounds: Bounds<Pixels>) {
-        let stacking_order = self.window.current_frame.z_index_stack.clone();
-        let depth_map = &mut self.window.current_frame.depth_map;
+        let stacking_order = self.window.next_frame.z_index_stack.clone();
+        let depth_map = &mut self.window.next_frame.depth_map;
         match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(&level)) {
             Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)),
         }
@@ -850,7 +858,7 @@ impl<'a> WindowContext<'a> {
     /// Returns true if the top-most opaque layer painted over this point was part of the
     /// same layer as the given stacking order.
     pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
-        for (stack, bounds) in self.window.previous_frame.depth_map.iter() {
+        for (stack, bounds) in self.window.rendered_frame.depth_map.iter() {
             if bounds.contains_point(point) {
                 return level.starts_with(stack) || stack.starts_with(level);
             }
@@ -861,10 +869,10 @@ impl<'a> WindowContext<'a> {
 
     /// Called during painting to get the current stacking order.
     pub fn stacking_order(&self) -> &StackingOrder {
-        &self.window.current_frame.z_index_stack
+        &self.window.next_frame.z_index_stack
     }
 
-    /// Paint one or more drop shadows into the scene for the current frame at the current z-index.
+    /// Paint one or more drop shadows into the scene for the next frame at the current z-index.
     pub fn paint_shadows(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -878,8 +886,8 @@ impl<'a> WindowContext<'a> {
             let mut shadow_bounds = bounds;
             shadow_bounds.origin += shadow.offset;
             shadow_bounds.dilate(shadow.spread_radius);
-            window.current_frame.scene_builder.insert(
-                &window.current_frame.z_index_stack,
+            window.next_frame.scene_builder.insert(
+                &window.next_frame.z_index_stack,
                 Shadow {
                     order: 0,
                     bounds: shadow_bounds.scale(scale_factor),
@@ -892,7 +900,7 @@ impl<'a> WindowContext<'a> {
         }
     }
 
-    /// Paint one or more quads into the scene for the current frame at the current stacking context.
+    /// Paint one or more quads into the scene for the next frame at the current stacking context.
     /// Quads are colored rectangular regions with an optional background, border, and corner radius.
     pub fn paint_quad(
         &mut self,
@@ -906,8 +914,8 @@ impl<'a> WindowContext<'a> {
         let content_mask = self.content_mask();
 
         let window = &mut *self.window;
-        window.current_frame.scene_builder.insert(
-            &window.current_frame.z_index_stack,
+        window.next_frame.scene_builder.insert(
+            &window.next_frame.z_index_stack,
             Quad {
                 order: 0,
                 bounds: bounds.scale(scale_factor),
@@ -920,20 +928,20 @@ impl<'a> WindowContext<'a> {
         );
     }
 
-    /// Paint the given `Path` into the scene for the current frame at the current z-index.
+    /// Paint the given `Path` into the scene for the next frame at the current z-index.
     pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
         path.content_mask = content_mask;
         path.color = color.into();
         let window = &mut *self.window;
-        window.current_frame.scene_builder.insert(
-            &window.current_frame.z_index_stack,
-            path.scale(scale_factor),
-        );
+        window
+            .next_frame
+            .scene_builder
+            .insert(&window.next_frame.z_index_stack, path.scale(scale_factor));
     }
 
-    /// Paint an underline into the scene for the current frame at the current z-index.
+    /// Paint an underline into the scene for the next frame at the current z-index.
     pub fn paint_underline(
         &mut self,
         origin: Point<Pixels>,
@@ -952,8 +960,8 @@ impl<'a> WindowContext<'a> {
         };
         let content_mask = self.content_mask();
         let window = &mut *self.window;
-        window.current_frame.scene_builder.insert(
-            &window.current_frame.z_index_stack,
+        window.next_frame.scene_builder.insert(
+            &window.next_frame.z_index_stack,
             Underline {
                 order: 0,
                 bounds: bounds.scale(scale_factor),
@@ -965,7 +973,7 @@ impl<'a> WindowContext<'a> {
         );
     }
 
-    /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
+    /// Paint a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
     /// The y component of the origin is the baseline of the glyph.
     pub fn paint_glyph(
         &mut self,
@@ -1005,8 +1013,8 @@ impl<'a> WindowContext<'a> {
             };
             let content_mask = self.content_mask().scale(scale_factor);
             let window = &mut *self.window;
-            window.current_frame.scene_builder.insert(
-                &window.current_frame.z_index_stack,
+            window.next_frame.scene_builder.insert(
+                &window.next_frame.z_index_stack,
                 MonochromeSprite {
                     order: 0,
                     bounds,
@@ -1019,7 +1027,7 @@ impl<'a> WindowContext<'a> {
         Ok(())
     }
 
-    /// Paint an emoji glyph into the scene for the current frame at the current z-index.
+    /// Paint an emoji glyph into the scene for the next frame at the current z-index.
     /// The y component of the origin is the baseline of the glyph.
     pub fn paint_emoji(
         &mut self,
@@ -1056,8 +1064,8 @@ impl<'a> WindowContext<'a> {
             let content_mask = self.content_mask().scale(scale_factor);
             let window = &mut *self.window;
 
-            window.current_frame.scene_builder.insert(
-                &window.current_frame.z_index_stack,
+            window.next_frame.scene_builder.insert(
+                &window.next_frame.z_index_stack,
                 PolychromeSprite {
                     order: 0,
                     bounds,
@@ -1071,7 +1079,7 @@ impl<'a> WindowContext<'a> {
         Ok(())
     }
 
-    /// Paint a monochrome SVG into the scene for the current frame at the current stacking context.
+    /// Paint a monochrome SVG into the scene for the next frame at the current stacking context.
     pub fn paint_svg(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -1098,8 +1106,8 @@ impl<'a> WindowContext<'a> {
         let content_mask = self.content_mask().scale(scale_factor);
 
         let window = &mut *self.window;
-        window.current_frame.scene_builder.insert(
-            &window.current_frame.z_index_stack,
+        window.next_frame.scene_builder.insert(
+            &window.next_frame.z_index_stack,
             MonochromeSprite {
                 order: 0,
                 bounds,
@@ -1112,7 +1120,7 @@ impl<'a> WindowContext<'a> {
         Ok(())
     }
 
-    /// Paint an image into the scene for the current frame at the current z-index.
+    /// Paint an image into the scene for the next frame at the current z-index.
     pub fn paint_image(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -1134,8 +1142,8 @@ impl<'a> WindowContext<'a> {
         let corner_radii = corner_radii.scale(scale_factor);
 
         let window = &mut *self.window;
-        window.current_frame.scene_builder.insert(
-            &window.current_frame.z_index_stack,
+        window.next_frame.scene_builder.insert(
+            &window.next_frame.z_index_stack,
             PolychromeSprite {
                 order: 0,
                 bounds,
@@ -1148,14 +1156,14 @@ impl<'a> WindowContext<'a> {
         Ok(())
     }
 
-    /// Paint a surface into the scene for the current frame at the current z-index.
+    /// Paint a surface into the scene for the next frame at the current z-index.
     pub fn paint_surface(&mut self, bounds: Bounds<Pixels>, image_buffer: CVImageBuffer) {
         let scale_factor = self.scale_factor();
         let bounds = bounds.scale(scale_factor);
         let content_mask = self.content_mask().scale(scale_factor);
         let window = &mut *self.window;
-        window.current_frame.scene_builder.insert(
-            &window.current_frame.z_index_stack,
+        window.next_frame.scene_builder.insert(
+            &window.next_frame.z_index_stack,
             Surface {
                 order: 0,
                 bounds,
@@ -1167,15 +1175,17 @@ 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) {
+        self.text_system().start_frame();
+        self.window.platform_window.clear_input_handler();
+        self.window.layout_engine.as_mut().unwrap().clear();
+        self.window.next_frame.clear();
         let root_view = self.window.root_view.take().unwrap();
 
-        self.start_frame();
-
         self.with_z_index(0, |cx| {
             cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| {
                 for (action_type, action_listeners) in &cx.app.global_action_listeners {
                     for action_listener in action_listeners.iter().cloned() {
-                        cx.window.current_frame.dispatch_tree.on_action(
+                        cx.window.next_frame.dispatch_tree.on_action(
                             *action_type,
                             Rc::new(move |action, phase, cx| action_listener(action, phase, cx)),
                         )
@@ -1204,16 +1214,18 @@ impl<'a> WindowContext<'a> {
         }
 
         self.window
-            .current_frame
+            .next_frame
             .dispatch_tree
             .preserve_pending_keystrokes(
-                &mut self.window.previous_frame.dispatch_tree,
+                &mut self.window.rendered_frame.dispatch_tree,
                 self.window.focus,
             );
-
         self.window.root_view = Some(root_view);
-        let scene = self.window.current_frame.scene_builder.build();
 
+        let window = &mut self.window;
+        mem::swap(&mut window.rendered_frame, &mut window.next_frame);
+
+        let scene = self.window.rendered_frame.scene_builder.build();
         self.window.platform_window.draw(scene);
         let cursor_style = self
             .window
@@ -1225,24 +1237,6 @@ impl<'a> WindowContext<'a> {
         self.window.dirty = false;
     }
 
-    /// Rotate the current frame and the previous frame, then clear the current frame.
-    /// We repopulate all state in the current frame during each paint.
-    fn start_frame(&mut self) {
-        self.window.platform_window.clear_input_handler();
-        self.text_system().start_frame();
-
-        let window = &mut *self.window;
-        window.layout_engine.as_mut().unwrap().clear();
-
-        mem::swap(&mut window.previous_frame, &mut window.current_frame);
-        let frame = &mut window.current_frame;
-        frame.element_states.clear();
-        frame.mouse_listeners.values_mut().for_each(Vec::clear);
-        frame.focus_listeners.clear();
-        frame.dispatch_tree.clear();
-        frame.depth_map.clear();
-    }
-
     /// Dispatch a mouse or keyboard event on the window.
     pub fn dispatch_event(&mut self, event: InputEvent) -> bool {
         // Handlers may set this to false by calling `stop_propagation`
@@ -1275,10 +1269,9 @@ impl<'a> WindowContext<'a> {
                             cursor_offset: position,
                         });
                     }
-                    InputEvent::MouseDown(MouseDownEvent {
+                    InputEvent::MouseMove(MouseMoveEvent {
                         position,
-                        button: MouseButton::Left,
-                        click_count: 1,
+                        pressed_button: Some(MouseButton::Left),
                         modifiers: Modifiers::default(),
                     })
                 }
@@ -1291,6 +1284,7 @@ impl<'a> WindowContext<'a> {
                     })
                 }
                 FileDropEvent::Submit { position } => {
+                    self.activate(true);
                     self.window.mouse_position = position;
                     InputEvent::MouseUp(MouseUpEvent {
                         button: MouseButton::Left,
@@ -1321,7 +1315,7 @@ impl<'a> WindowContext<'a> {
     fn dispatch_mouse_event(&mut self, event: &dyn Any) {
         if let Some(mut handlers) = self
             .window
-            .current_frame
+            .rendered_frame
             .mouse_listeners
             .remove(&event.type_id())
         {
@@ -1351,17 +1345,8 @@ impl<'a> WindowContext<'a> {
                 self.active_drag = None;
             }
 
-            // Just in case any handlers added new handlers, which is weird, but possible.
-            handlers.extend(
-                self.window
-                    .current_frame
-                    .mouse_listeners
-                    .get_mut(&event.type_id())
-                    .into_iter()
-                    .flat_map(|handlers| handlers.drain(..)),
-            );
             self.window
-                .current_frame
+                .rendered_frame
                 .mouse_listeners
                 .insert(event.type_id(), handlers);
         }
@@ -1373,15 +1358,15 @@ impl<'a> WindowContext<'a> {
             .focus
             .and_then(|focus_id| {
                 self.window
-                    .current_frame
+                    .rendered_frame
                     .dispatch_tree
                     .focusable_node_id(focus_id)
             })
-            .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
+            .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
 
         let dispatch_path = self
             .window
-            .current_frame
+            .rendered_frame
             .dispatch_tree
             .dispatch_path(node_id);
 
@@ -1392,7 +1377,7 @@ impl<'a> WindowContext<'a> {
         self.propagate_event = true;
 
         for node_id in &dispatch_path {
-            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
 
             if let Some(context) = node.context.clone() {
                 context_stack.push(context);
@@ -1409,7 +1394,7 @@ impl<'a> WindowContext<'a> {
         // Bubble phase
         for node_id in dispatch_path.iter().rev() {
             // Handle low level key events
-            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
             for key_listener in node.key_listeners.clone() {
                 key_listener(event, DispatchPhase::Bubble, self);
                 if !self.propagate_event {
@@ -1418,12 +1403,12 @@ impl<'a> WindowContext<'a> {
             }
 
             // Match keystrokes
-            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
             if node.context.is_some() {
                 if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
                     if let Some(found) = self
                         .window
-                        .current_frame
+                        .rendered_frame
                         .dispatch_tree
                         .dispatch_key(&key_down_event.keystroke, &context_stack)
                     {
@@ -1446,13 +1431,13 @@ impl<'a> WindowContext<'a> {
     fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
         let dispatch_path = self
             .window
-            .current_frame
+            .rendered_frame
             .dispatch_tree
             .dispatch_path(node_id);
 
         // Capture phase
         for node_id in &dispatch_path {
-            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
             for DispatchActionListener {
                 action_type,
                 listener,
@@ -1469,7 +1454,7 @@ impl<'a> WindowContext<'a> {
         }
         // Bubble phase
         for node_id in dispatch_path.iter().rev() {
-            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
             for DispatchActionListener {
                 action_type,
                 listener,
@@ -1529,25 +1514,25 @@ impl<'a> WindowContext<'a> {
             .focus
             .and_then(|focus_id| {
                 self.window
-                    .current_frame
+                    .rendered_frame
                     .dispatch_tree
                     .focusable_node_id(focus_id)
             })
-            .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id());
+            .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
 
         self.window
-            .current_frame
+            .rendered_frame
             .dispatch_tree
             .available_actions(node_id)
     }
 
     pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
         self.window
-            .previous_frame
+            .rendered_frame
             .dispatch_tree
             .bindings_for_action(
                 action,
-                &self.window.previous_frame.dispatch_tree.context_stack,
+                &self.window.rendered_frame.dispatch_tree.context_stack,
             )
     }
 
@@ -1556,7 +1541,7 @@ impl<'a> WindowContext<'a> {
         action: &dyn Action,
         focus_handle: &FocusHandle,
     ) -> Vec<KeyBinding> {
-        let dispatch_tree = &self.window.previous_frame.dispatch_tree;
+        let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
 
         let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else {
             return vec![];
@@ -1599,24 +1584,21 @@ impl<'a> WindowContext<'a> {
         f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
     ) -> R {
         let window = &mut self.window;
-        window
-            .current_frame
-            .dispatch_tree
-            .push_node(context.clone());
+        window.next_frame.dispatch_tree.push_node(context.clone());
         if let Some(focus_handle) = focus_handle.as_ref() {
             window
-                .current_frame
+                .next_frame
                 .dispatch_tree
                 .make_focusable(focus_handle.id);
         }
         let result = f(focus_handle, self);
 
-        self.window.current_frame.dispatch_tree.pop_node();
+        self.window.next_frame.dispatch_tree.pop_node();
 
         result
     }
 
-    /// Register a focus listener for the current frame only. It will be cleared
+    /// Register a focus listener for the next frame only. It will be cleared
     /// on the next frame render. You should use this method only from within elements,
     /// and we may want to enforce that better via a different context type.
     // todo!() Move this to `FrameContext` to emphasize its individuality?
@@ -1625,7 +1607,7 @@ impl<'a> WindowContext<'a> {
         listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static,
     ) {
         self.window
-            .current_frame
+            .next_frame
             .focus_listeners
             .push(Box::new(move |event, cx| {
                 listener(event, cx);
@@ -1876,12 +1858,9 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     ) -> R {
         if let Some(mask) = mask {
             let mask = mask.intersect(&self.content_mask());
-            self.window_mut()
-                .current_frame
-                .content_mask_stack
-                .push(mask);
+            self.window_mut().next_frame.content_mask_stack.push(mask);
             let result = f(self);
-            self.window_mut().current_frame.content_mask_stack.pop();
+            self.window_mut().next_frame.content_mask_stack.pop();
             result
         } else {
             f(self)
@@ -1897,12 +1876,9 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
                 size: self.window().viewport_size,
             },
         };
-        self.window_mut()
-            .current_frame
-            .content_mask_stack
-            .push(mask);
+        self.window_mut().next_frame.content_mask_stack.push(mask);
         let result = f(self);
-        self.window_mut().current_frame.content_mask_stack.pop();
+        self.window_mut().next_frame.content_mask_stack.pop();
         result
     }
 
@@ -1929,26 +1905,26 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
         self.window_mut()
-            .current_frame
+            .next_frame
             .element_offset_stack
             .push(offset);
         let result = f(self);
-        self.window_mut().current_frame.element_offset_stack.pop();
+        self.window_mut().next_frame.element_offset_stack.pop();
         result
     }
 
     /// Obtain the current element offset.
     fn element_offset(&self) -> Point<Pixels> {
         self.window()
-            .current_frame
+            .next_frame
             .element_offset_stack
             .last()
             .copied()
             .unwrap_or_default()
     }
 
-    /// Update or intialize state for an element with the given id that lives across multiple
-    /// frames. If an element with this id existed in the previous frame, its state will be passed
+    /// Update or initialize state for an element with the given id that lives across multiple
+    /// frames. If an element with this id existed in the rendered frame, its state will be passed
     /// to the given closure. The state returned by the closure will be stored so it can be referenced
     /// when drawing the next frame.
     fn with_element_state<S, R>(
@@ -1964,12 +1940,12 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
 
             if let Some(any) = cx
                 .window_mut()
-                .current_frame
+                .next_frame
                 .element_states
                 .remove(&global_id)
                 .or_else(|| {
                     cx.window_mut()
-                        .previous_frame
+                        .rendered_frame
                         .element_states
                         .remove(&global_id)
                 })
@@ -2011,7 +1987,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
                 let (result, state) = f(Some(state), cx);
                 state_box.replace(state);
                 cx.window_mut()
-                    .current_frame
+                    .next_frame
                     .element_states
                     .insert(global_id, ElementStateBox {
                         inner: state_box,
@@ -2023,7 +1999,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
             } else {
                 let (result, state) = f(None, cx);
                 cx.window_mut()
-                    .current_frame
+                    .next_frame
                     .element_states
                     .insert(global_id,
                         ElementStateBox {
@@ -2042,7 +2018,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     /// Obtain the current content mask.
     fn content_mask(&self) -> ContentMask<Pixels> {
         self.window()
-            .current_frame
+            .next_frame
             .content_mask_stack
             .last()
             .cloned()
@@ -2130,9 +2106,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
-        self.window.current_frame.z_index_stack.push(z_index);
+        self.window.next_frame.z_index_stack.push(z_index);
         let result = f(self);
-        self.window.current_frame.z_index_stack.pop();
+        self.window.next_frame.z_index_stack.pop();
         result
     }
 

crates/gpui2_macros/src/action.rs 🔗

@@ -38,6 +38,7 @@ pub fn action(input: TokenStream) -> TokenStream {
 
     let build_impl = if is_unit_struct {
         quote! {
+            let _ = value;
             Ok(std::boxed::Box::new(Self {}))
         }
     } else {

crates/quick_action_bar2/src/quick_action_bar.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
     Action, ClickEvent, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render,
     Stateful, Styled, Subscription, View, ViewContext, WeakView,
 };
-use search::BufferSearchBar;
+use search::{buffer_search, BufferSearchBar};
 use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
 use workspace::{
     item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -64,12 +64,14 @@ impl Render for QuickActionBar {
             "toggle buffer search",
             Icon::MagnifyingGlass,
             !self.buffer_search_bar.read(cx).is_dismissed(),
-            Box::new(search::buffer_search::Deploy { focus: false }),
+            Box::new(buffer_search::Deploy { focus: false }),
             "Buffer Search",
             {
                 let buffer_search_bar = self.buffer_search_bar.clone();
                 move |_, cx| {
-                    buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx));
+                    buffer_search_bar.update(cx, |search_bar, cx| {
+                        search_bar.toggle(&buffer_search::Deploy { focus: true }, cx)
+                    });
                 }
             },
         ))

crates/recent_projects2/src/recent_projects.rs 🔗

@@ -1,9 +1,10 @@
 mod highlighted_workspace_location;
+mod projects;
 
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task,
-    View, ViewContext, WeakView,
+    AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, View,
+    ViewContext, WeakView,
 };
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
@@ -16,7 +17,7 @@ use workspace::{
     WORKSPACE_DB,
 };
 
-actions!(OpenRecent);
+pub use projects::OpenRecent;
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(RecentProjects::register).detach();

crates/search2/src/buffer_search.rs 🔗

@@ -170,14 +170,25 @@ impl Render for BufferSearchBar {
 
         h_stack()
             .key_context("BufferSearchBar")
-            .when(in_replace, |this| {
-                this.key_context("in_replace")
-                    .on_action(cx.listener(Self::replace_next))
-                    .on_action(cx.listener(Self::replace_all))
-            })
             .on_action(cx.listener(Self::previous_history_query))
             .on_action(cx.listener(Self::next_history_query))
             .on_action(cx.listener(Self::dismiss))
+            .on_action(cx.listener(Self::select_next_match))
+            .on_action(cx.listener(Self::select_prev_match))
+            .when(self.supported_options().replacement, |this| {
+                this.on_action(cx.listener(Self::toggle_replace))
+                    .when(in_replace, |this| {
+                        this.key_context("in_replace")
+                            .on_action(cx.listener(Self::replace_next))
+                            .on_action(cx.listener(Self::replace_all))
+                    })
+            })
+            .when(self.supported_options().case, |this| {
+                this.on_action(cx.listener(Self::toggle_case_sensitive))
+            })
+            .when(self.supported_options().word, |this| {
+                this.on_action(cx.listener(Self::toggle_whole_word))
+            })
             .w_full()
             .p_1()
             .child(
@@ -305,7 +316,7 @@ impl BufferSearchBar {
 
         let handle = cx.view().downgrade();
 
-        editor.register_action(move |a: &Deploy, cx| {
+        editor.register_action(move |deploy: &Deploy, cx| {
             let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else {
                 return;
             };
@@ -313,12 +324,12 @@ 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(cx));
+                        search_bar.update(cx, |this, cx| this.toggle(deploy, cx));
                         return;
                     }
                     let view = cx.build_view(|cx| BufferSearchBar::new(cx));
                     this.add_item(view.clone(), cx);
-                    view.update(cx, |this, cx| this.deploy(a, cx));
+                    view.update(cx, |this, cx| this.deploy(deploy, cx));
                     cx.notify();
                 })
             });
@@ -468,7 +479,7 @@ impl BufferSearchBar {
             self.search_suggested(cx);
             if deploy.focus {
                 self.select_query(cx);
-                let handle = cx.focus_handle();
+                let handle = self.query_editor.focus_handle(cx);
                 cx.focus(&handle);
             }
             return true;
@@ -477,9 +488,9 @@ impl BufferSearchBar {
         false
     }
 
-    pub fn toggle(&mut self, cx: &mut ViewContext<Self>) {
+    pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext<Self>) {
         if self.is_dismissed() {
-            self.show(cx);
+            self.deploy(action, cx);
         } else {
             self.dismiss(&Dismiss, cx);
         }

crates/storybook2/src/story_selector.rs 🔗

@@ -28,6 +28,7 @@ pub enum ComponentStory {
     ListHeader,
     ListItem,
     Scroll,
+    Tab,
     Text,
     ZIndex,
     Picker,
@@ -53,6 +54,7 @@ impl ComponentStory {
             Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
             Self::Scroll => ScrollStory::view(cx).into(),
             Self::Text => TextStory::view(cx).into(),
+            Self::Tab => cx.build_view(|_| ui::TabStory).into(),
             Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
             Self::Picker => PickerStory::new(cx).into(),
         }

crates/terminal2/src/terminal2.rs 🔗

@@ -50,7 +50,7 @@ use std::{
 use thiserror::Error;
 
 use gpui::{
-    px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
+    actions, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
     ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
     Point, ScrollWheelEvent, Size, Task, TouchPhase,
 };
@@ -58,6 +58,16 @@ use gpui::{
 use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
 use lazy_static::lazy_static;
 
+actions!(
+    Clear,
+    Copy,
+    Paste,
+    ShowCharacterPalette,
+    SearchTest,
+    SendText,
+    SendKeystroke,
+);
+
 ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
 ///Scroll multiplier that is set to 3 by default. This will be removed when I
 ///Implement scroll bars.

crates/terminal_view2/src/terminal_element.rs 🔗

@@ -1,9 +1,9 @@
 use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
 use gpui::{
     black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext,
-    AvailableSpace, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle,
-    FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement,
-    LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
+    AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
+    FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState,
+    IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
     PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
     TextRun, TextStyle, TextSystem, UnderlineStyle, View, WhiteSpace, WindowContext,
 };
@@ -643,13 +643,11 @@ impl TerminalElement {
                 let connection = connection.clone();
                 let focus = focus.clone();
                 move |e, cx| {
-                    if e.pressed_button.is_some() {
-                        if focus.is_focused(cx) {
-                            connection.update(cx, |terminal, cx| {
-                                terminal.mouse_drag(e, origin, bounds);
-                                cx.notify();
-                            })
-                        }
+                    if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
+                        connection.update(cx, |terminal, cx| {
+                            terminal.mouse_drag(e, origin, bounds);
+                            cx.notify();
+                        })
                     }
                 }
             })
@@ -806,7 +804,28 @@ impl Element for TerminalElement {
                 .map(|cursor| cursor.bounding_rect(origin)),
         };
 
-        let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx);
+        let terminal_focus_handle = self.focus.clone();
+        let terminal_handle = self.terminal.clone();
+        let mut this: TerminalElement = self
+            .register_mouse_listeners(origin, layout.mode, bounds, cx)
+            .drag_over::<ExternalPaths>(|style| {
+                // todo!() why does not it work? z-index of elements?
+                style.bg(cx.theme().colors().ghost_element_hover)
+            })
+            .on_drop::<ExternalPaths>(move |external_paths, cx| {
+                cx.focus(&terminal_focus_handle);
+                let mut new_text = external_paths
+                    .read(cx)
+                    .paths()
+                    .iter()
+                    .map(|path| format!(" {path:?}"))
+                    .join("");
+                new_text.push(' ');
+                terminal_handle.update(cx, |terminal, _| {
+                    // todo!() long paths are not displayed properly albeit the text is there
+                    terminal.paste(&new_text);
+                });
+            });
 
         let interactivity = mem::take(&mut this.interactivity);
 

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -9,9 +9,9 @@ pub mod terminal_panel;
 // use crate::terminal_element::TerminalElement;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle,
-    Focusable, FocusableElement, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model,
-    MouseButton, MouseDownEvent, Pixels, Render, Subscription, Task, View, VisualContext, WeakView,
+    div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle, Focusable,
+    FocusableElement, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
+    MouseDownEvent, Pixels, Render, Subscription, Task, View, VisualContext, WeakView,
 };
 use language::Bias;
 use persistence::TERMINAL_DB;
@@ -22,7 +22,7 @@ use terminal::{
         term::{search::RegexSearch, TermMode},
     },
     terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
-    Event, MaybeNavigationTarget, Terminal,
+    Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
 };
 use terminal_element::TerminalElement;
 use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
@@ -60,8 +60,6 @@ pub struct SendText(String);
 #[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendKeystroke(String);
 
-actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
-
 pub fn init(cx: &mut AppContext) {
     terminal_panel::init(cx);
     terminal::init(cx);

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -187,6 +187,10 @@ impl PickerDelegate for ThemeSelectorDelegate {
             Self::set_theme(self.original_theme.clone(), cx);
             self.selection_completed = true;
         }
+
+        self.view
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
     }
 
     fn selected_index(&self) -> usize {

crates/ui2/src/components.rs 🔗

@@ -13,6 +13,7 @@ mod popover;
 mod popover_menu;
 mod right_click_menu;
 mod stack;
+mod tab;
 mod tooltip;
 
 #[cfg(feature = "stories")]
@@ -33,6 +34,7 @@ pub use popover::*;
 pub use popover_menu::*;
 pub use right_click_menu::*;
 pub use stack::*;
+pub use tab::*;
 pub use tooltip::*;
 
 #[cfg(feature = "stories")]

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

@@ -98,11 +98,11 @@ impl RenderOnce for Key {
 
         div()
             .py_0()
-            .map(|el| {
+            .map(|this| {
                 if single_char {
-                    el.w(rems(14. / 16.)).flex().flex_none().justify_center()
+                    this.w(rems(14. / 16.)).flex().flex_none().justify_center()
                 } else {
-                    el.px_0p5()
+                    this.px_0p5()
                 }
             })
             .h(rems(14. / 16.))

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

@@ -10,6 +10,7 @@ mod label;
 mod list;
 mod list_header;
 mod list_item;
+mod tab;
 
 pub use avatar::*;
 pub use button::*;
@@ -23,3 +24,4 @@ pub use label::*;
 pub use list::*;
 pub use list_header::*;
 pub use list_item::*;
+pub use tab::*;

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

@@ -0,0 +1,114 @@
+use std::cmp::Ordering;
+
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, TabPosition};
+use crate::{Indicator, Tab};
+
+pub struct TabStory;
+
+impl Render for TabStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<Tab>())
+            .child(Story::label("Default"))
+            .child(h_stack().child(Tab::new("tab_1").child("Tab 1")))
+            .child(Story::label("With indicator"))
+            .child(
+                h_stack().child(
+                    Tab::new("tab_1")
+                        .start_slot(Indicator::dot().color(Color::Warning))
+                        .child("Tab 1"),
+                ),
+            )
+            .child(Story::label("With close button"))
+            .child(
+                h_stack().child(
+                    Tab::new("tab_1")
+                        .end_slot(
+                            IconButton::new("close_button", Icon::Close)
+                                .icon_color(Color::Muted)
+                                .size(ButtonSize::None)
+                                .icon_size(IconSize::XSmall),
+                        )
+                        .child("Tab 1"),
+                ),
+            )
+            .child(Story::label("List of tabs"))
+            .child(
+                h_stack()
+                    .child(Tab::new("tab_1").child("Tab 1"))
+                    .child(Tab::new("tab_2").child("Tab 2")),
+            )
+            .child(Story::label("List of tabs with first tab selected"))
+            .child(
+                h_stack()
+                    .child(
+                        Tab::new("tab_1")
+                            .selected(true)
+                            .position(TabPosition::First)
+                            .child("Tab 1"),
+                    )
+                    .child(
+                        Tab::new("tab_2")
+                            .position(TabPosition::Middle(Ordering::Greater))
+                            .child("Tab 2"),
+                    )
+                    .child(
+                        Tab::new("tab_3")
+                            .position(TabPosition::Middle(Ordering::Greater))
+                            .child("Tab 3"),
+                    )
+                    .child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
+            )
+            .child(Story::label("List of tabs with last tab selected"))
+            .child(
+                h_stack()
+                    .child(
+                        Tab::new("tab_1")
+                            .position(TabPosition::First)
+                            .child("Tab 1"),
+                    )
+                    .child(
+                        Tab::new("tab_2")
+                            .position(TabPosition::Middle(Ordering::Less))
+                            .child("Tab 2"),
+                    )
+                    .child(
+                        Tab::new("tab_3")
+                            .position(TabPosition::Middle(Ordering::Less))
+                            .child("Tab 3"),
+                    )
+                    .child(
+                        Tab::new("tab_4")
+                            .position(TabPosition::Last)
+                            .selected(true)
+                            .child("Tab 4"),
+                    ),
+            )
+            .child(Story::label("List of tabs with second tab selected"))
+            .child(
+                h_stack()
+                    .child(
+                        Tab::new("tab_1")
+                            .position(TabPosition::First)
+                            .child("Tab 1"),
+                    )
+                    .child(
+                        Tab::new("tab_2")
+                            .position(TabPosition::Middle(Ordering::Equal))
+                            .selected(true)
+                            .child("Tab 2"),
+                    )
+                    .child(
+                        Tab::new("tab_3")
+                            .position(TabPosition::Middle(Ordering::Greater))
+                            .child("Tab 3"),
+                    )
+                    .child(Tab::new("tab_4").position(TabPosition::Last).child("Tab 4")),
+            )
+    }
+}

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

@@ -0,0 +1,198 @@
+use std::cmp::Ordering;
+use std::rc::Rc;
+
+use gpui::{AnyElement, AnyView, ClickEvent, IntoElement, MouseButton};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+
+/// The position of a [`Tab`] within a list of tabs.
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum TabPosition {
+    /// The tab is first in the list.
+    First,
+
+    /// The tab is in the middle of the list (i.e., it is not the first or last tab).
+    ///
+    /// The [`Ordering`] is where this tab is positioned with respect to the selected tab.
+    Middle(Ordering),
+
+    /// The tab is last in the list.
+    Last,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum TabCloseSide {
+    Start,
+    End,
+}
+
+#[derive(IntoElement)]
+pub struct Tab {
+    id: ElementId,
+    selected: bool,
+    position: TabPosition,
+    close_side: TabCloseSide,
+    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
+    start_slot: Option<AnyElement>,
+    end_slot: Option<AnyElement>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl Tab {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            selected: false,
+            position: TabPosition::First,
+            close_side: TabCloseSide::End,
+            on_click: None,
+            tooltip: None,
+            start_slot: None,
+            end_slot: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn position(mut self, position: TabPosition) -> Self {
+        self.position = position;
+        self
+    }
+
+    pub fn close_side(mut self, close_side: TabCloseSide) -> Self {
+        self.close_side = close_side;
+        self
+    }
+
+    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+        self.on_click = Some(Rc::new(handler));
+        self
+    }
+
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+
+    pub fn start_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
+        self.start_slot = element.into().map(IntoElement::into_any_element);
+        self
+    }
+
+    pub fn end_slot<E: IntoElement>(mut self, element: impl Into<Option<E>>) -> Self {
+        self.end_slot = element.into().map(IntoElement::into_any_element);
+        self
+    }
+}
+
+impl Selectable for Tab {
+    fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
+impl ParentElement for Tab {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for Tab {
+    type Rendered = Div;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        const HEIGHT_IN_REMS: f32 = 30. / 16.;
+
+        let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected {
+            false => (
+                cx.theme().colors().text_muted,
+                cx.theme().colors().tab_inactive_background,
+                cx.theme().colors().ghost_element_hover,
+                cx.theme().colors().ghost_element_active,
+            ),
+            true => (
+                cx.theme().colors().text,
+                cx.theme().colors().tab_active_background,
+                cx.theme().colors().element_hover,
+                cx.theme().colors().element_active,
+            ),
+        };
+
+        div()
+            .h(rems(HEIGHT_IN_REMS))
+            .bg(tab_bg)
+            .border_color(cx.theme().colors().border)
+            .map(|this| match self.position {
+                TabPosition::First => {
+                    if self.selected {
+                        this.pl_px().border_r().pb_px()
+                    } else {
+                        this.pl_px().pr_px().border_b()
+                    }
+                }
+                TabPosition::Last => {
+                    if self.selected {
+                        this.border_l().border_r().pb_px()
+                    } else {
+                        this.pr_px().pl_px().border_b()
+                    }
+                }
+                TabPosition::Middle(Ordering::Equal) => this.border_l().border_r().pb_px(),
+                TabPosition::Middle(Ordering::Less) => this.border_l().pr_px().border_b(),
+                TabPosition::Middle(Ordering::Greater) => this.border_r().pl_px().border_b(),
+            })
+            .child(
+                h_stack()
+                    .group("")
+                    .id(self.id)
+                    .relative()
+                    .h_full()
+                    .px_5()
+                    .gap_1()
+                    .text_color(text_color)
+                    // .hover(|style| style.bg(tab_hover_bg))
+                    // .active(|style| style.bg(tab_active_bg))
+                    .when_some(self.on_click, |tab, on_click| {
+                        tab.cursor_pointer().on_click(move |event, cx| {
+                            // HACK: GPUI currently fires `on_click` with any mouse button,
+                            // but we only care about the left button.
+                            if event.down.button == MouseButton::Left {
+                                (on_click)(event, cx)
+                            }
+                        })
+                    })
+                    .when_some(self.tooltip, |tab, tooltip| {
+                        tab.tooltip(move |cx| tooltip(cx))
+                    })
+                    .child(
+                        h_stack()
+                            .w_3()
+                            .h_3()
+                            .justify_center()
+                            .absolute()
+                            .map(|this| match self.close_side {
+                                TabCloseSide::Start => this.right_1(),
+                                TabCloseSide::End => this.left_1(),
+                            })
+                            .children(self.start_slot),
+                    )
+                    .child(
+                        h_stack()
+                            .invisible()
+                            .w_3()
+                            .h_3()
+                            .justify_center()
+                            .absolute()
+                            .map(|this| match self.close_side {
+                                TabCloseSide::Start => this.left_1(),
+                                TabCloseSide::End => this.right_1(),
+                            })
+                            .group_hover("", |style| style.visible())
+                            .children(self.end_slot),
+                    )
+                    .children(self.children),
+            )
+    }
+}

crates/workspace2/src/dock.rs 🔗

@@ -133,13 +133,13 @@ pub struct Dock {
     panel_entries: Vec<PanelEntry>,
     is_open: bool,
     active_panel_index: usize,
+    focus_handle: FocusHandle,
+    focus_subscription: Subscription,
 }
 
 impl FocusableView for Dock {
-    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.panel_entries[self.active_panel_index]
-            .panel
-            .focus_handle(cx)
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
     }
 }
 
@@ -190,12 +190,20 @@ pub struct PanelButtons {
 }
 
 impl Dock {
-    pub fn new(position: DockPosition) -> Self {
+    pub fn new(position: DockPosition, cx: &mut ViewContext<'_, Self>) -> Self {
+        let focus_handle = cx.focus_handle();
+        let focus_subscription = cx.on_focus(&focus_handle, |dock, cx| {
+            if let Some(active_entry) = dock.panel_entries.get(dock.active_panel_index) {
+                active_entry.panel.focus_handle(cx).focus(cx)
+            }
+        });
         Self {
             position,
             panel_entries: Default::default(),
             active_panel_index: 0,
             is_open: false,
+            focus_handle,
+            focus_subscription,
         }
     }
 
@@ -207,6 +215,7 @@ impl Dock {
         self.is_open
     }
 
+    // todo!()
     //     pub fn has_focus(&self, cx: &WindowContext) -> bool {
     //         self.visible_panel()
     //             .map_or(false, |panel| panel.has_focus(cx))

crates/workspace2/src/pane.rs 🔗

@@ -28,10 +28,10 @@ use std::{
 
 use ui::{
     h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
-    Indicator, Label, Tooltip,
+    Indicator, Label, Tab, TabPosition, Tooltip,
 };
 use ui::{v_stack, ContextMenu};
-use util::truncate_and_remove_front;
+use util::{maybe, truncate_and_remove_front};
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
@@ -1438,42 +1438,49 @@ impl Pane {
 
         let is_active = ix == self.active_item_index;
 
-        let indicator = {
+        let indicator = maybe!({
             let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
-                (true, _) => Some(Color::Warning),
-                (_, true) => Some(Color::Accent),
-                (false, false) => None,
+                (true, _) => Color::Warning,
+                (_, true) => Color::Accent,
+                (false, false) => return None,
             };
 
-            h_stack()
-                .w_3()
-                .h_3()
-                .justify_center()
-                .absolute()
-                .map(|this| match close_side {
-                    ClosePosition::Left => this.right_1(),
-                    ClosePosition::Right => this.left_1(),
+            Some(Indicator::dot().color(indicator_color))
+        });
+
+        let id = item.item_id();
+
+        let is_first_item = ix == 0;
+        let is_last_item = ix == self.items.len() - 1;
+        let position_relative_to_active_item = ix.cmp(&self.active_item_index);
+
+        let tab =
+            Tab::new(ix)
+                .position(if is_first_item {
+                    TabPosition::First
+                } else if is_last_item {
+                    TabPosition::Last
+                } else {
+                    TabPosition::Middle(position_relative_to_active_item)
                 })
-                .when_some(indicator_color, |this, indicator_color| {
-                    this.child(Indicator::dot().color(indicator_color))
+                .close_side(match close_side {
+                    ClosePosition::Left => ui::TabCloseSide::Start,
+                    ClosePosition::Right => ui::TabCloseSide::End,
                 })
-        };
-
-        let close_button = {
-            let id = item.item_id();
-
-            h_stack()
-                .invisible()
-                .w_3()
-                .h_3()
-                .justify_center()
-                .absolute()
-                .map(|this| match close_side {
-                    ClosePosition::Left => this.left_1(),
-                    ClosePosition::Right => this.right_1(),
+                .selected(ix == self.active_item_index())
+                .on_click(cx.listener(move |pane: &mut Self, event, cx| {
+                    pane.activate_item(ix, true, true, cx)
+                }))
+                // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
+                // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
+                // .on_drop(|_view, state: View<DraggedTab>, cx| {
+                //     eprintln!("{:?}", state.read(cx));
+                // })
+                .when_some(item.tab_tooltip_text(cx), |tab, text| {
+                    tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
                 })
-                .group_hover("", |style| style.visible())
-                .child(
+                .start_slot::<Indicator>(indicator)
+                .end_slot(
                     // TODO: Fix button size
                     IconButton::new("close tab", Icon::Close)
                         .icon_color(Color::Muted)
@@ -1484,67 +1491,7 @@ impl Pane {
                                 .detach_and_log_err(cx);
                         })),
                 )
-        };
-
-        let tab = div()
-            .border_color(cx.theme().colors().border)
-            .bg(tab_bg)
-            // 30px @ 16px/rem
-            .h(rems(1.875))
-            .map(|this| {
-                let is_first_item = ix == 0;
-                let is_last_item = ix == self.items.len() - 1;
-                match ix.cmp(&self.active_item_index) {
-                    cmp::Ordering::Less => {
-                        if is_first_item {
-                            this.pl_px().pr_px().border_b()
-                        } else {
-                            this.border_l().pr_px().border_b()
-                        }
-                    }
-                    cmp::Ordering::Greater => {
-                        if is_last_item {
-                            this.pr_px().pl_px().border_b()
-                        } else {
-                            this.border_r().pl_px().border_b()
-                        }
-                    }
-                    cmp::Ordering::Equal => {
-                        if is_first_item {
-                            this.pl_px().border_r().pb_px()
-                        } else {
-                            this.border_l().border_r().pb_px()
-                        }
-                    }
-                }
-            })
-            .child(
-                h_stack()
-                    .group("")
-                    .id(ix)
-                    .relative()
-                    .h_full()
-                    .cursor_pointer()
-                    .when_some(item.tab_tooltip_text(cx), |div, text| {
-                        div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
-                    })
-                    .on_click(
-                        cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)),
-                    )
-                    // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
-                    // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
-                    // .on_drop(|_view, state: View<DraggedTab>, cx| {
-                    //     eprintln!("{:?}", state.read(cx));
-                    // })
-                    .px_5()
-                    // .hover(|h| h.bg(tab_hover_bg))
-                    // .active(|a| a.bg(tab_active_bg))
-                    .gap_1()
-                    .text_color(text_color)
-                    .child(indicator)
-                    .child(close_button)
-                    .child(label),
-            );
+                .child(label);
 
         right_click_menu(ix).trigger(tab).menu(|cx| {
             ContextMenu::build(cx, |menu, cx| {

crates/workspace2/src/workspace2.rs 🔗

@@ -566,9 +566,9 @@ impl Workspace {
 
         cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
 
-        let left_dock = cx.build_view(|_| Dock::new(DockPosition::Left));
-        let bottom_dock = cx.build_view(|_| Dock::new(DockPosition::Bottom));
-        let right_dock = cx.build_view(|_| Dock::new(DockPosition::Right));
+        let left_dock = cx.build_view(|cx| Dock::new(DockPosition::Left, cx));
+        let bottom_dock = cx.build_view(|cx| Dock::new(DockPosition::Bottom, cx));
+        let right_dock = cx.build_view(|cx| Dock::new(DockPosition::Right, cx));
         let left_dock_buttons =
             cx.build_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
         let bottom_dock_buttons =
@@ -4188,14 +4188,14 @@ pub fn open_paths(
     });
     cx.spawn(move |mut cx| async move {
         if let Some(existing) = existing {
-            // // Ok((
-            //     existing.clone(),
-            //     cx.update_window_root(&existing, |workspace, cx| {
-            //         workspace.open_paths(abs_paths, true, cx)
-            //     })?
-            //     .await,
-            // ))
-            todo!()
+            Ok((
+                existing.clone(),
+                existing
+                    .update(&mut cx, |workspace, cx| {
+                        workspace.open_paths(abs_paths, true, cx)
+                    })?
+                    .await,
+            ))
         } else {
             cx.update(move |cx| {
                 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)