diff --git a/Cargo.lock b/Cargo.lock index 88340c716bc0fc3ec54bf4f54d2f7ebb6235b6d8..0a4801524eae5937fd476fd2cd6dac129cae228e 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 4edf5ef35ba4495d5f3ef4922414d0f79cd46d7c..ec723df25d740c5d020eecb378a35978dd179baf 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/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, pending_serialization: Task>, context_menu: Option<(View, Point, Subscription)>, + list_state: ListState, filter_editor: View, channel_name_editor: View, channel_editing_state: Option, @@ -313,7 +314,6 @@ pub struct CollabPanel { client: Arc, project: Model, match_candidates: Vec, - scroll_handle: ScrollHandle, subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, @@ -398,7 +398,7 @@ enum ListEntry { impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { 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::::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) { 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) -> 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) -> 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::(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .on_drop(cx.listener( - move |this, view: &View, cx| { + el.drag_over::(|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::(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .on_drop( - cx.listener(move |this, view: &View, 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::(|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. diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index b127708fa690dfc4976c8df246c9d3afa7b3f1a4..706749b28d349a2b9b30d51375c7818ec1031296 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/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( diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 393fb16f8148b5f61d3591a1af5e7189fc033aeb..04efe1df53ae200273dc6d35e9231b55304571c2 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -101,6 +101,7 @@ pub struct CommandInterceptResult { pub struct CommandPaletteDelegate { command_palette: WeakView, + all_commands: Vec, commands: Vec, matches: Vec, 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>, ) -> 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::(|hit_counts, _| { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index e58aa1000d475caa009b045b89e36145c69b3894..c6d83ef24e498cc85b0280a5d0f1fbc9eed0ed93 100644 --- a/crates/editor2/src/editor.rs +++ b/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)), ), ) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 0f1b565b9d818504e905ca2a668578ffafc04069..a04af377b432967ccf01046e824c027ace7549ec 100644 --- a/crates/editor2/src/element.rs +++ b/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, - ) - }); } }); } diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 6818c5c7a2e62baab27aad701dfa27cfa80dbce4..415a3b7368bb6a88049ec5377bb1aafc21b3596e 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/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>); struct StateInner { - last_layout_width: Option, + last_layout_bounds: Option>, render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, @@ -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::(); + 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> { + 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; diff --git a/crates/gpui2/src/platform/mac/metal_renderer.rs b/crates/gpui2/src/platform/mac/metal_renderer.rs index 3210a53c634e81e5410e732c2971c86a553c808f..68768521ee474cd25c2e98c8d38c7ea669fba1e4 100644 --- a/crates/gpui2/src/platform/mac/metal_renderer.rs +++ b/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 = 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(); } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index fb61190731bddf4859e3fcd5ebbc374976a4b9ca..1b4c2b6346fb56320737ab74a8057da589aba354 100644 --- a/crates/gpui2/src/view.rs +++ b/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); }) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 76fa36c68b1889baf073524e54c7025d9055180b..56c5466b6380e73891625a53d67ec01f154dc47a 100644 --- a/crates/gpui2/src/window.rs +++ b/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 } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 2d18d2a6c769396d699ee3059e48f2736a85370a..d8b5c1551f4ef33495e7d84b168a702182479017 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -1389,7 +1389,9 @@ impl ProjectPanel { entry_id: *entry_id, }) }) - .drag_over::(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .drag_over::(|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() }) diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs index dff6aa12ccc30f43766451d244619159c2a7c8bb..3ecf1180af535ea23190bd41ad2c8dd0e329810d 100644 --- a/crates/recent_projects2/src/recent_projects.rs +++ b/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| { diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 45495e502700ee65c3015a9f6fd78bc22fe5053c..5db7aff73670161719188a8cff5051c4a4a46604 100644 --- a/crates/search2/src/buffer_search.rs +++ b/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::() { - 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); diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index 167c6fece282f9d7619780497d8c357f7af2eebf..7bd60891ced8a78902d856a674c502c8c1546f4a 100644 --- a/crates/search2/src/project_search.rs +++ b/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); + })), + ) }), ), ); diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 384447af8fe51472a7da9b614e23b364bb664d25..9c195f77f05f591474e739834a8b8965c3822fb4 100644 --- a/crates/story/Cargo.toml +++ b/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"} diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs index d95c879ce0068c00fd24ffb64179d4e3d46a5c7a..3419af95b099807609b52b40b9775b17457975a0 100644 --- a/crates/story/src/story.rs +++ b/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, 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
; + + 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(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::()) + .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) -> 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::()) } + 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) -> 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) -> 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) -> impl Element { + div() + .text_sm() + .text_color(story_color().secondary) + .min_w_96() + .child(description.into()) + } + pub fn label(label: impl Into) -> 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, + usage: Option, +} + +impl StoryItem { + pub fn new(label: impl Into, 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) -> Self { + self.description = Some(description.into()); + self + } + + pub fn usage(mut self, code: impl Into) -> 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, + children: SmallVec<[AnyElement; 2]>, +} + +impl StorySection { + pub fn new() -> Self { + Self { + description: None, + children: SmallVec::new(), + } + } + + pub fn description(mut self, description: impl Into) -> 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 + } } diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index d0467df47111ade82c17047dfc7f5a6752ea1ff5..db79c9faca9052c383e8809b4ae064bf9a847609 100644 --- a/crates/storybook2/Cargo.toml +++ b/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 diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index ccd13cb4d80b1e739cef0671a3bfd54b78264fea..99d722988d086d932c6bd734992720273f02e2e3 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/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; fn render(&mut self, cx: &mut gpui::ViewContext) -> 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::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}"); +// }) +// ) +// } +// } diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 9cc2dd51f7c73388679bee01d480ecbac328cec8..523e93cf5267e310d691ee4ca19db1cc67cfbb4b 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/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, diff --git a/crates/ui2/src/components/stories/icon_button.rs b/crates/ui2/src/components/stories/icon_button.rs index 583f453d188b9df5aaaf71b5bb1897814c0a6f51..0d219efbc2fe72ac4e7695e2f86bf305d26c5537 100644 --- a/crates/ui2/src/components/stories/icon_button.rs +++ b/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; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - Story::container() - .child(Story::title_for::()) - .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::()) + // .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)), + // ), + // ) } } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 7c502ac5cb13a4322db626602feedcd34edccdce..0a8eb8d6be5bc6f6e806a5ad153ea038a23f2b44 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/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) diff --git a/crates/vcs_menu2/src/lib.rs b/crates/vcs_menu2/src/lib.rs index e867e04dcdb96229eb7513f891d60cc31ce1ec26..ca3b685aa60ad93f63cc2ba203fa52a184ff564b 100644 --- a/crates/vcs_menu2/src/lib.rs +++ b/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(()) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 11588212ef76e3891c57fc5a14778fd8e48a9e37..65e7f2a1063add17b8321f6594746871af418bdc 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1897,19 +1897,14 @@ impl Render for Pane { .on_drag_move::(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( diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index 966e2c6341a5c9fa7808424a07ea0571d99a5bf6..d35c138d5c8217b32786a2b95cb7c5e9c9009030 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/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, + child_start: Point, + container_size: Size, 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, + ) } }); });