Detailed changes
@@ -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",
@@ -165,7 +165,7 @@ struct ChannelMoveClipboard {
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
-use std::{iter::once, mem, sync::Arc};
+use std::{mem, sync::Arc};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
@@ -175,12 +175,12 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
- serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
- DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
- InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
- PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
- Subscription, Task, View, ViewContext, VisualContext, WeakView,
+ actions, canvas, div, fill, impl_actions, list, overlay, point, prelude::*, px, serde_json,
+ AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
+ EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement,
+ ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
+ Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext,
+ WeakView,
};
use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize};
@@ -188,7 +188,7 @@ use settings::{Settings, SettingsStore};
use ui::prelude::*;
use ui::{
h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
- Label, List, ListHeader, ListItem, Tooltip,
+ Label, ListHeader, ListItem, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@@ -303,6 +303,7 @@ pub struct CollabPanel {
channel_clipboard: Option<ChannelMoveClipboard>,
pending_serialization: Task<Option<()>>,
context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
+ list_state: ListState,
filter_editor: View<Editor>,
channel_name_editor: View<Editor>,
channel_editing_state: Option<ChannelEditingState>,
@@ -313,7 +314,6 @@ pub struct CollabPanel {
client: Arc<Client>,
project: Model<Project>,
match_candidates: Vec<StringMatchCandidate>,
- scroll_handle: ScrollHandle,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelId>,
@@ -398,7 +398,7 @@ enum ListEntry {
impl CollabPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
cx.build_view(|cx| {
- // let view_id = cx.view_id();
+ let view = cx.view().clone();
let filter_editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx);
@@ -445,136 +445,10 @@ impl CollabPanel {
})
.detach();
- // let list_state =
- // ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
- // let theme = theme::current(cx).clone();
- // let is_selected = this.selection == Some(ix);
- // let current_project_id = this.project.read(cx).remote_id();
-
- // match &this.entries[ix] {
- // ListEntry::Header(section) => {
- // let is_collapsed = this.collapsed_sections.contains(section);
- // this.render_header(*section, &theme, is_selected, is_collapsed, cx)
- // }
- // ListEntry::CallParticipant {
- // user,
- // peer_id,
- // is_pending,
- // } => Self::render_call_participant(
- // user,
- // *peer_id,
- // this.user_store.clone(),
- // *is_pending,
- // is_selected,
- // &theme,
- // cx,
- // ),
- // ListEntry::ParticipantProject {
- // project_id,
- // worktree_root_names,
- // host_user_id,
- // is_last,
- // } => Self::render_participant_project(
- // *project_id,
- // worktree_root_names,
- // *host_user_id,
- // Some(*project_id) == current_project_id,
- // *is_last,
- // is_selected,
- // &theme,
- // cx,
- // ),
- // ListEntry::ParticipantScreen { peer_id, is_last } => {
- // Self::render_participant_screen(
- // *peer_id,
- // *is_last,
- // is_selected,
- // &theme.collab_panel,
- // cx,
- // )
- // }
- // ListEntry::Channel {
- // channel,
- // depth,
- // has_children,
- // } => {
- // let channel_row = this.render_channel(
- // &*channel,
- // *depth,
- // &theme,
- // is_selected,
- // *has_children,
- // ix,
- // cx,
- // );
-
- // if is_selected && this.context_menu_on_selected {
- // Stack::new()
- // .with_child(channel_row)
- // .with_child(
- // ChildView::new(&this.context_menu, cx)
- // .aligned()
- // .bottom()
- // .right(),
- // )
- // .into_any()
- // } else {
- // return channel_row;
- // }
- // }
- // ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
- // *channel_id,
- // &theme.collab_panel,
- // is_selected,
- // ix,
- // cx,
- // ),
- // ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
- // *channel_id,
- // &theme.collab_panel,
- // is_selected,
- // ix,
- // cx,
- // ),
- // ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
- // channel.clone(),
- // this.channel_store.clone(),
- // &theme.collab_panel,
- // is_selected,
- // cx,
- // ),
- // ListEntry::IncomingRequest(user) => Self::render_contact_request(
- // user.clone(),
- // this.user_store.clone(),
- // &theme.collab_panel,
- // true,
- // is_selected,
- // cx,
- // ),
- // ListEntry::OutgoingRequest(user) => Self::render_contact_request(
- // user.clone(),
- // this.user_store.clone(),
- // &theme.collab_panel,
- // false,
- // is_selected,
- // cx,
- // ),
- // ListEntry::Contact { contact, calling } => Self::render_contact(
- // contact,
- // *calling,
- // &this.project,
- // &theme,
- // is_selected,
- // cx,
- // ),
- // ListEntry::ChannelEditor { depth } => {
- // this.render_channel_editor(&theme, *depth, cx)
- // }
- // ListEntry::ContactPlaceholder => {
- // this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
- // }
- // }
- // });
+ let list_state =
+ ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
+ view.update(cx, |view, cx| view.render_list_entry(ix, cx))
+ });
let mut this = Self {
width: None,
@@ -583,6 +457,7 @@ impl CollabPanel {
fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None),
context_menu: None,
+ list_state,
channel_name_editor,
filter_editor,
entries: Vec::default(),
@@ -593,7 +468,6 @@ impl CollabPanel {
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
- scroll_handle: ScrollHandle::new(),
collapsed_sections: vec![Section::Offline],
collapsed_channels: Vec::default(),
workspace: workspace.weak_handle(),
@@ -709,6 +583,10 @@ impl CollabPanel {
);
}
+ fn scroll_to_item(&mut self, ix: usize) {
+ self.list_state.scroll_to_reveal_item(ix)
+ }
+
fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.read(cx);
let user_store = self.user_store.read(cx);
@@ -1084,13 +962,15 @@ impl CollabPanel {
self.entries.push(ListEntry::ContactPlaceholder);
}
+ self.list_state.reset(self.entries.len());
+
if select_same_item {
if let Some(prev_selected_entry) = prev_selected_entry {
self.selection.take();
for (ix, entry) in self.entries.iter().enumerate() {
if *entry == prev_selected_entry {
self.selection = Some(ix);
- self.scroll_handle.scroll_to_item(ix);
+ self.scroll_to_item(ix);
break;
}
}
@@ -1101,16 +981,19 @@ impl CollabPanel {
None
} else {
let ix = prev_selection.min(self.entries.len() - 1);
- self.scroll_handle.scroll_to_item(ix);
+ self.scroll_to_item(ix);
Some(ix)
}
});
}
if scroll_to_top {
- self.scroll_handle.scroll_to_item(0)
+ self.scroll_to_item(0)
} else {
- let (old_index, old_offset) = self.scroll_handle.logical_scroll_top();
+ let ListOffset {
+ item_ix: old_index,
+ offset_in_item: old_offset,
+ } = self.list_state.logical_scroll_top();
// Attempt to maintain the same scroll position.
if let Some(old_top_entry) = old_entries.get(old_index) {
let (new_index, new_offset) = self
@@ -1136,8 +1019,10 @@ impl CollabPanel {
})
.unwrap_or_else(|| (old_index, old_offset));
- self.scroll_handle
- .set_logical_scroll_top(new_index, new_offset);
+ self.list_state.scroll_to(ListOffset {
+ item_ix: new_index,
+ offset_in_item: new_offset,
+ });
}
}
@@ -1628,7 +1513,7 @@ impl CollabPanel {
}
if let Some(ix) = self.selection {
- self.scroll_handle.scroll_to_item(ix)
+ self.scroll_to_item(ix)
}
cx.notify();
}
@@ -1640,7 +1525,7 @@ impl CollabPanel {
}
if let Some(ix) = self.selection {
- self.scroll_handle.scroll_to_item(ix)
+ self.scroll_to_item(ix)
}
cx.notify();
}
@@ -1965,7 +1850,7 @@ impl CollabPanel {
};
let Some(bounds) = self
.selection
- .and_then(|ix| self.scroll_handle.bounds_for_item(ix))
+ .and_then(|ix| self.list_state.bounds_for_item(ix))
else {
return;
};
@@ -2158,78 +2043,75 @@ impl CollabPanel {
)
}
+ fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
+ let entry = &self.entries[ix];
+
+ let is_selected = self.selection == Some(ix);
+ match entry {
+ ListEntry::Header(section) => {
+ let is_collapsed = self.collapsed_sections.contains(section);
+ self.render_header(*section, is_selected, is_collapsed, cx)
+ .into_any_element()
+ }
+ ListEntry::Contact { contact, calling } => self
+ .render_contact(contact, *calling, is_selected, cx)
+ .into_any_element(),
+ ListEntry::ContactPlaceholder => self
+ .render_contact_placeholder(is_selected, cx)
+ .into_any_element(),
+ ListEntry::IncomingRequest(user) => self
+ .render_contact_request(user, true, is_selected, cx)
+ .into_any_element(),
+ ListEntry::OutgoingRequest(user) => self
+ .render_contact_request(user, false, is_selected, cx)
+ .into_any_element(),
+ ListEntry::Channel {
+ channel,
+ depth,
+ has_children,
+ } => self
+ .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
+ .into_any_element(),
+ ListEntry::ChannelEditor { depth } => {
+ self.render_channel_editor(*depth, cx).into_any_element()
+ }
+ ListEntry::CallParticipant {
+ user,
+ peer_id,
+ is_pending,
+ } => self
+ .render_call_participant(user, *peer_id, *is_pending, cx)
+ .into_any_element(),
+ ListEntry::ParticipantProject {
+ project_id,
+ worktree_root_names,
+ host_user_id,
+ is_last,
+ } => self
+ .render_participant_project(
+ *project_id,
+ &worktree_root_names,
+ *host_user_id,
+ *is_last,
+ cx,
+ )
+ .into_any_element(),
+ ListEntry::ParticipantScreen { peer_id, is_last } => self
+ .render_participant_screen(*peer_id, *is_last, cx)
+ .into_any_element(),
+ ListEntry::ChannelNotes { channel_id } => self
+ .render_channel_notes(*channel_id, cx)
+ .into_any_element(),
+ ListEntry::ChannelChat { channel_id } => {
+ self.render_channel_chat(*channel_id, cx).into_any_element()
+ }
+ }
+ }
+
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
v_stack()
.size_full()
- .child(
- v_stack()
- .size_full()
- .id("scroll")
- .overflow_y_scroll()
- .track_scroll(&self.scroll_handle)
- .children(self.entries.iter().enumerate().map(|(ix, entry)| {
- let is_selected = self.selection == Some(ix);
- match entry {
- ListEntry::Header(section) => {
- let is_collapsed = self.collapsed_sections.contains(section);
- self.render_header(*section, is_selected, is_collapsed, cx)
- .into_any_element()
- }
- ListEntry::Contact { contact, calling } => self
- .render_contact(contact, *calling, is_selected, cx)
- .into_any_element(),
- ListEntry::ContactPlaceholder => self
- .render_contact_placeholder(is_selected, cx)
- .into_any_element(),
- ListEntry::IncomingRequest(user) => self
- .render_contact_request(user, true, is_selected, cx)
- .into_any_element(),
- ListEntry::OutgoingRequest(user) => self
- .render_contact_request(user, false, is_selected, cx)
- .into_any_element(),
- ListEntry::Channel {
- channel,
- depth,
- has_children,
- } => self
- .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
- .into_any_element(),
- ListEntry::ChannelEditor { depth } => {
- self.render_channel_editor(*depth, cx).into_any_element()
- }
- ListEntry::CallParticipant {
- user,
- peer_id,
- is_pending,
- } => self
- .render_call_participant(user, *peer_id, *is_pending, cx)
- .into_any_element(),
- ListEntry::ParticipantProject {
- project_id,
- worktree_root_names,
- host_user_id,
- is_last,
- } => self
- .render_participant_project(
- *project_id,
- &worktree_root_names,
- *host_user_id,
- *is_last,
- cx,
- )
- .into_any_element(),
- ListEntry::ParticipantScreen { peer_id, is_last } => self
- .render_participant_screen(*peer_id, *is_last, cx)
- .into_any_element(),
- ListEntry::ChannelNotes { channel_id } => self
- .render_channel_notes(*channel_id, cx)
- .into_any_element(),
- ListEntry::ChannelChat { channel_id } => {
- self.render_channel_chat(*channel_id, cx).into_any_element()
- }
- }
- })),
- )
+ .child(list(self.list_state.clone()).full())
.child(
div().p_2().child(
div()
@@ -2343,18 +2225,14 @@ impl CollabPanel {
.selected(is_selected),
)
.when(section == Section::Channels, |el| {
- el.drag_over::<DraggedChannelView>(|style| {
- style.bg(cx.theme().colors().ghost_element_hover)
- })
- .on_drop(cx.listener(
- move |this, view: &View<DraggedChannelView>, cx| {
+ el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+ .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
- channel_store.move_channel(view.read(cx).channel.id, None, cx)
+ channel_store.move_channel(dragged_channel.id, None, cx)
})
.detach_and_log_err(cx)
- },
- ))
+ }))
});
if section == Section::Offline {
@@ -2569,22 +2447,14 @@ impl CollabPanel {
width,
})
})
- .drag_over::<DraggedChannelView>(|style| {
- style.bg(cx.theme().colors().ghost_element_hover)
- })
- .on_drop(
- cx.listener(move |this, view: &View<DraggedChannelView>, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(
- view.read(cx).channel.id,
- Some(channel_id),
- cx,
- )
- })
- .detach_and_log_err(cx)
- }),
- )
+ .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+ .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
+ this.channel_store
+ .update(cx, |channel_store, cx| {
+ channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
+ })
+ .detach_and_log_err(cx)
+ }))
.child(
ListItem::new(channel_id as usize)
// Offset the indent depth by one to give us room to show the disclosure.
@@ -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(
@@ -101,6 +101,7 @@ pub struct CommandInterceptResult {
pub struct CommandPaletteDelegate {
command_palette: WeakView<CommandPalette>,
+ all_commands: Vec<Command>,
commands: Vec<Command>,
matches: Vec<StringMatch>,
selected_ix: usize,
@@ -135,6 +136,7 @@ impl CommandPaletteDelegate {
) -> Self {
Self {
command_palette,
+ all_commands: commands.clone(),
matches: vec![],
commands,
selected_ix: 0,
@@ -167,7 +169,7 @@ impl PickerDelegate for CommandPaletteDelegate {
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
- let mut commands = self.commands.clone();
+ let mut commands = self.all_commands.clone();
cx.spawn(move |picker, mut cx| async move {
cx.read_global::<HitCounts, _>(|hit_counts, _| {
@@ -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)),
),
)
@@ -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,
- )
- });
}
});
}
@@ -1,6 +1,7 @@
use crate::{
- px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
- Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext,
+ point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element,
+ IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
+ WindowContext,
};
use collections::VecDeque;
use refineable::Refineable as _;
@@ -23,7 +24,7 @@ pub struct List {
pub struct ListState(Rc<RefCell<StateInner>>);
struct StateInner {
- last_layout_width: Option<Pixels>,
+ last_layout_bounds: Option<Bounds<Pixels>>,
render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>,
@@ -83,7 +84,7 @@ impl ListState {
let mut items = SumTree::new();
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
Self(Rc::new(RefCell::new(StateInner {
- last_layout_width: None,
+ last_layout_bounds: None,
render_item: Box::new(render_item),
items,
logical_scroll_top: None,
@@ -152,6 +153,64 @@ impl ListState {
}
state.logical_scroll_top = Some(scroll_top);
}
+
+ pub fn scroll_to_reveal_item(&self, ix: usize) {
+ let state = &mut *self.0.borrow_mut();
+ let mut scroll_top = state.logical_scroll_top();
+ let height = state
+ .last_layout_bounds
+ .map_or(px(0.), |bounds| bounds.size.height);
+
+ if ix <= scroll_top.item_ix {
+ scroll_top.item_ix = ix;
+ scroll_top.offset_in_item = px(0.);
+ } else {
+ let mut cursor = state.items.cursor::<ListItemSummary>();
+ cursor.seek(&Count(ix + 1), Bias::Right, &());
+ let bottom = cursor.start().height;
+ let goal_top = px(0.).max(bottom - height);
+
+ cursor.seek(&Height(goal_top), Bias::Left, &());
+ let start_ix = cursor.start().count;
+ let start_item_top = cursor.start().height;
+
+ if start_ix >= scroll_top.item_ix {
+ scroll_top.item_ix = start_ix;
+ scroll_top.offset_in_item = goal_top - start_item_top;
+ }
+ }
+
+ state.logical_scroll_top = Some(scroll_top);
+ }
+
+ /// Get the bounds for the given item in window coordinates.
+ pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
+ let state = &*self.0.borrow();
+ let bounds = state.last_layout_bounds.unwrap_or_default();
+ let scroll_top = state.logical_scroll_top();
+
+ if ix < scroll_top.item_ix {
+ return None;
+ }
+
+ let mut cursor = state.items.cursor::<(Count, Height)>();
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+ let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
+
+ cursor.seek_forward(&Count(ix), Bias::Right, &());
+ if let Some(&ListItem::Rendered { height }) = cursor.item() {
+ let &(Count(count), Height(top)) = cursor.start();
+ if count == ix {
+ let top = bounds.top() + top - scroll_top;
+ return Some(Bounds::from_corners(
+ point(bounds.left(), top),
+ point(bounds.right(), top + height),
+ ));
+ }
+ }
+ None
+ }
}
impl StateInner {
@@ -265,7 +324,9 @@ impl Element for List {
let state = &mut *self.state.0.borrow_mut();
// If the width of the list has changed, invalidate all cached item heights
- if state.last_layout_width != Some(bounds.size.width) {
+ if state.last_layout_bounds.map_or(true, |last_bounds| {
+ last_bounds.size.width != bounds.size.width
+ }) {
state.items = SumTree::from_iter(
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
&(),
@@ -392,7 +453,7 @@ impl Element for List {
}
state.items = new_items;
- state.last_layout_width = Some(bounds.size.width);
+ state.last_layout_bounds = Some(bounds);
let list_state = self.state.clone();
let height = bounds.size.height;
@@ -187,8 +187,6 @@ impl MetalRenderer {
}
pub fn draw(&mut self, scene: &Scene) {
- let start = std::time::Instant::now();
-
let layer = self.layer.clone();
let viewport_size = layer.drawable_size();
let viewport_size: Size<DevicePixels> = size(
@@ -306,9 +304,6 @@ impl MetalRenderer {
command_buffer.commit();
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
- let duration_since_start = start.elapsed();
- println!("renderer draw: {:?}", duration_since_start);
-
command_buffer.wait_until_completed();
drawable.present();
}
@@ -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);
})
}
}
@@ -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
}
@@ -1389,7 +1389,9 @@ impl ProjectPanel {
entry_id: *entry_id,
})
})
- .drag_over::<ProjectEntryId>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+ .drag_over::<ProjectEntryId>(|style| {
+ style.bg(cx.theme().colors().drop_target_background)
+ })
.on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
this.move_entry(*dragged_id, entry_id, kind.is_file(), cx);
}))
@@ -1399,7 +1401,7 @@ impl ProjectPanel {
.indent_step_size(px(settings.indent_size))
.selected(is_selected)
.child(if let Some(icon) = &icon {
- div().child(IconElement::from_path(icon.to_string()))
+ div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
} else {
div()
})
@@ -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| {
@@ -338,7 +338,9 @@ impl BufferSearchBar {
pane.update(cx, |this, cx| {
this.toolbar().update(cx, |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
- search_bar.update(cx, |this, cx| this.toggle(deploy, cx));
+ search_bar.update(cx, |this, cx| {
+ this.deploy(deploy, cx);
+ });
return;
}
let view = cx.build_view(|cx| BufferSearchBar::new(cx));
@@ -1483,9 +1485,9 @@ mod tests {
search_bar.select_all_matches(&SelectAllMatches, cx);
});
assert!(
- editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
- "Should not switch focus to editor if SelectAllMatches does not find any matches"
- );
+ editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
+ "Should not switch focus to editor if SelectAllMatches does not find any matches"
+ );
search_bar.update(cx, |search_bar, cx| {
let all_selections =
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
@@ -1651,6 +1653,7 @@ mod tests {
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
}
+
#[gpui::test]
async fn test_replace_simple(cx: &mut TestAppContext) {
let (editor, search_bar, cx) = init_test(cx);
@@ -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);
+ })),
+ )
}),
),
);
@@ -8,3 +8,5 @@ publish = false
[dependencies]
gpui = { package = "gpui2", path = "../gpui2" }
+smallvec.workspace = true
+itertools = {package = "itertools", version = "0.10"}
@@ -1,22 +1,199 @@
-use gpui::prelude::*;
-use gpui::{div, hsla, Div, SharedString};
+use gpui::{
+ div, hsla, prelude::*, px, rems, AnyElement, Div, ElementId, Hsla, SharedString, Stateful,
+ WindowContext,
+};
+use itertools::Itertools;
+use smallvec::SmallVec;
+
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+static COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+pub fn reasonably_unique_id() -> String {
+ let now = SystemTime::now();
+ let timestamp = now.duration_since(UNIX_EPOCH).unwrap();
+
+ let cnt = COUNTER.fetch_add(1, Ordering::Relaxed);
+
+ let id = format!("{}_{}", timestamp.as_nanos(), cnt);
+
+ id
+}
+
+pub struct StoryColor {
+ pub primary: Hsla,
+ pub secondary: Hsla,
+ pub border: Hsla,
+ pub background: Hsla,
+ pub card_background: Hsla,
+ pub divider: Hsla,
+ pub link: Hsla,
+}
+
+impl StoryColor {
+ pub fn new() -> Self {
+ Self {
+ primary: hsla(216. / 360., 11. / 100., 0. / 100., 1.),
+ secondary: hsla(216. / 360., 11. / 100., 16. / 100., 1.),
+ border: hsla(216. / 360., 11. / 100., 91. / 100., 1.),
+ background: hsla(0. / 360., 0. / 100., 100. / 100., 1.),
+ card_background: hsla(0. / 360., 0. / 100., 96. / 100., 1.),
+ divider: hsla(216. / 360., 11. / 100., 86. / 100., 1.),
+ link: hsla(206. / 360., 100. / 100., 50. / 100., 1.),
+ }
+ }
+}
+
+pub fn story_color() -> StoryColor {
+ StoryColor::new()
+}
+
+#[derive(IntoElement)]
+pub struct StoryContainer {
+ title: SharedString,
+ relative_path: &'static str,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl StoryContainer {
+ pub fn new(title: impl Into<SharedString>, relative_path: &'static str) -> Self {
+ Self {
+ title: title.into(),
+ relative_path,
+ children: SmallVec::new(),
+ }
+ }
+}
+
+impl ParentElement for StoryContainer {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
+
+impl RenderOnce for StoryContainer {
+ type Rendered = Stateful<Div>;
+
+ fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+ div()
+ .size_full()
+ .flex()
+ .flex_col()
+ .id("story_container")
+ .bg(story_color().background)
+ .child(
+ div()
+ .flex()
+ .flex_none()
+ .w_full()
+ .justify_between()
+ .p_2()
+ .bg(story_color().background)
+ .border_b()
+ .border_color(story_color().border)
+ .child(Story::title(self.title))
+ .child(
+ div()
+ .text_xs()
+ .text_color(story_color().primary)
+ .child(Story::open_story_link(self.relative_path)),
+ ),
+ )
+ .child(
+ div()
+ .w_full()
+ .h_px()
+ .flex_1()
+ .id("story_body")
+ .overflow_hidden_x()
+ .overflow_y_scroll()
+ .flex()
+ .flex_col()
+ .pb_4()
+ .children(self.children),
+ )
+ }
+}
pub struct Story {}
impl Story {
pub fn container() -> Div {
- div().size_full().flex().flex_col().pt_2().px_4().bg(hsla(
- 0. / 360.,
- 0. / 100.,
- 100. / 100.,
- 1.,
- ))
+ div().size_full().overflow_hidden().child(
+ div()
+ .id("story_container")
+ .overflow_y_scroll()
+ .w_full()
+ .min_h_full()
+ .flex()
+ .flex_col()
+ .bg(story_color().background),
+ )
+ }
+
+ // TODO: Move all stories to container2, then rename
+ pub fn container2<T>(relative_path: &'static str) -> Div {
+ div().size_full().child(
+ div()
+ .size_full()
+ .id("story_container")
+ .overflow_y_scroll()
+ .flex()
+ .flex_col()
+ .flex_none()
+ .child(
+ div()
+ .flex()
+ .justify_between()
+ .p_2()
+ .border_b()
+ .border_color(story_color().border)
+ .child(Story::title_for::<T>())
+ .child(
+ div()
+ .text_xs()
+ .text_color(story_color().primary)
+ .child(Story::open_story_link(relative_path)),
+ ),
+ )
+ .child(
+ div()
+ .w_full()
+ .min_h_full()
+ .flex()
+ .flex_col()
+ .bg(story_color().background),
+ ),
+ )
+ }
+
+ pub fn open_story_link(relative_path: &'static str) -> impl Element {
+ let path = PathBuf::from_iter([relative_path]);
+
+ div()
+ .flex()
+ .gap_2()
+ .text_xs()
+ .text_color(story_color().primary)
+ .id(SharedString::from(format!("id_{}", relative_path)))
+ .on_click({
+ let path = path.clone();
+
+ move |_event, _cx| {
+ let path = format!("{}:0:0", path.to_string_lossy());
+
+ std::process::Command::new("zed").arg(path).spawn().ok();
+ }
+ })
+ .children(vec![div().child(Story::link("Open in Zed →"))])
}
pub fn title(title: impl Into<SharedString>) -> impl Element {
div()
- .text_xl()
- .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+ .text_xs()
+ .text_color(story_color().primary)
.child(title.into())
}
@@ -24,12 +201,185 @@ impl Story {
Self::title(std::any::type_name::<T>())
}
+ pub fn section() -> Div {
+ div()
+ .p_4()
+ .m_4()
+ .border()
+ .border_color(story_color().border)
+ }
+
+ pub fn section_title() -> Div {
+ div().text_lg().text_color(story_color().primary)
+ }
+
+ pub fn group() -> Div {
+ div().my_2().bg(story_color().background)
+ }
+
+ pub fn code_block(code: impl Into<SharedString>) -> Div {
+ div()
+ .size_full()
+ .p_2()
+ .max_w(rems(36.))
+ .bg(gpui::black())
+ .rounded_md()
+ .text_sm()
+ .text_color(gpui::white())
+ .overflow_hidden()
+ .child(code.into())
+ }
+
+ pub fn divider() -> Div {
+ div().my_2().h(px(1.)).bg(story_color().divider)
+ }
+
+ pub fn link(link: impl Into<SharedString>) -> impl Element {
+ div()
+ .id(ElementId::from(SharedString::from(reasonably_unique_id())))
+ .text_xs()
+ .text_color(story_color().link)
+ .cursor(gpui::CursorStyle::PointingHand)
+ .child(link.into())
+ }
+
+ pub fn description(description: impl Into<SharedString>) -> impl Element {
+ div()
+ .text_sm()
+ .text_color(story_color().secondary)
+ .min_w_96()
+ .child(description.into())
+ }
+
pub fn label(label: impl Into<SharedString>) -> impl Element {
div()
- .mt_4()
- .mb_2()
.text_xs()
- .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+ .text_color(story_color().primary)
.child(label.into())
}
+
+ /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate.
+ pub fn v_stack() -> Div {
+ div().flex().flex_col().gap_1()
+ }
+}
+
+#[derive(IntoElement)]
+pub struct StoryItem {
+ label: SharedString,
+ item: AnyElement,
+ description: Option<SharedString>,
+ usage: Option<SharedString>,
+}
+
+impl StoryItem {
+ pub fn new(label: impl Into<SharedString>, item: impl IntoElement) -> Self {
+ Self {
+ label: label.into(),
+ item: item.into_any_element(),
+ description: None,
+ usage: None,
+ }
+ }
+
+ pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+ self.description = Some(description.into());
+ self
+ }
+
+ pub fn usage(mut self, code: impl Into<SharedString>) -> Self {
+ self.usage = Some(code.into());
+ self
+ }
+}
+
+impl RenderOnce for StoryItem {
+ type Rendered = Div;
+
+ fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+ div()
+ .my_2()
+ .flex()
+ .gap_4()
+ .w_full()
+ .child(
+ Story::v_stack()
+ .px_2()
+ .w_1_2()
+ .min_h_px()
+ .child(Story::label(self.label))
+ .child(
+ div()
+ .rounded_md()
+ .bg(story_color().card_background)
+ .border()
+ .border_color(story_color().border)
+ .py_1()
+ .px_2()
+ .overflow_hidden()
+ .child(self.item),
+ )
+ .when_some(self.description, |this, description| {
+ this.child(Story::description(description))
+ }),
+ )
+ .child(
+ Story::v_stack()
+ .px_2()
+ .flex_none()
+ .w_1_2()
+ .min_h_px()
+ .when_some(self.usage, |this, usage| {
+ this.child(Story::label("Example Usage"))
+ .child(Story::code_block(usage))
+ }),
+ )
+ }
+}
+
+#[derive(IntoElement)]
+pub struct StorySection {
+ description: Option<SharedString>,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl StorySection {
+ pub fn new() -> Self {
+ Self {
+ description: None,
+ children: SmallVec::new(),
+ }
+ }
+
+ pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+ self.description = Some(description.into());
+ self
+ }
+}
+
+impl RenderOnce for StorySection {
+ type Rendered = Div;
+
+ fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+ let children: SmallVec<[AnyElement; 2]> = SmallVec::from_iter(Itertools::intersperse_with(
+ self.children.into_iter(),
+ || Story::divider().into_any_element(),
+ ));
+
+ Story::section()
+ // Section title
+ .py_2()
+ // Section description
+ .when_some(self.description.clone(), |section, description| {
+ section.child(Story::description(description))
+ })
+ .child(div().flex().flex_col().gap_2().children(children))
+ .child(Story::divider())
+ }
+}
+
+impl ParentElement for StorySection {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
}
@@ -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
@@ -1,8 +1,9 @@
use gpui::{
- blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render,
- Styled, StyledText, View, VisualContext, WindowContext,
+ div, green, red, Component, HighlightStyle, InteractiveText, IntoElement, ParentElement,
+ Render, Styled, StyledText, View, VisualContext, WindowContext,
};
-use ui::v_stack;
+use indoc::indoc;
+use story::*;
pub struct TextStory;
@@ -13,62 +14,164 @@ impl TextStory {
}
impl Render for TextStory {
- type Element = Div;
+ type Element = Component<StoryContainer>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
- v_stack()
- .bg(blue())
- .child(
- div()
- .flex()
- .child(div().max_w_96().bg(white()).child(concat!(
- "max-width: 96. The quick brown fox jumps over the lazy dog. ",
- "Meanwhile, the lazy dog decided it was time for a change. ",
- "He started daily workout routines, ate healthier and became the fastest dog in town.",
- ))),
- )
- .child(div().h_5())
- .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
- "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
- "Meanwhile, the lazy dog decided it was time for a change. ",
- "He started daily workout routines, ate healthier and became the fastest dog in town.",
- )))
- .child(div().h_5())
- .child(
- div()
- .flex()
- .child(div().min_w_96().bg(white()).child(concat!(
- "min-width: 96. The quick brown fox jumps over the lazy dog. ",
- "Meanwhile, the lazy dog decided it was time for a change. ",
- "He started daily workout routines, ate healthier and became the fastest dog in town.",
-))))
- .child(div().h_5())
- .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
- "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
- "Meanwhile, the lazy dog decided it was time for a change. ",
- "He started daily workout routines, ate healthier and became the fastest dog in town.",
- ))))
- // NOTE: When rendering text in a horizonal flex container,
- // Taffy will not pass width constraints down from the parent.
- // To fix this, render text in a praent with overflow: hidden, which
- .child(div().h_5())
- .child(div().flex().w_96().bg(red()).child(concat!(
- "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
- "Meanwhile, the lazy dog decided it was time for a change. ",
- "He started daily workout routines, ate healthier and became the fastest dog in town.",
- ))).child(
- InteractiveText::new(
- "interactive",
- StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
- (6..11, HighlightStyle {
- background_color: Some(green()),
- ..Default::default()
+ StoryContainer::new("Text Story", "crates/storybook2/src/stories/text.rs")
+ .children(
+ vec![
+
+ StorySection::new()
+ .child(
+ StoryItem::new("Default", div().bg(gpui::blue()).child("Hello World!"))
+ .usage(indoc! {r##"
+ div()
+ .child("Hello World!")
+ "##
}),
- ]),
)
- .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
- println!("Clicked range {range_ix}");
- })
- )
+ .child(
+ StoryItem::new("Wrapping Text",
+ div().max_w_96()
+ .child(
+ concat!(
+ "The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ )
+ )
+ )
+ .description("Set a width or max-width to enable text wrapping.")
+ .usage(indoc! {r##"
+ div()
+ .max_w_96()
+ .child("Some text that you want to wrap.")
+ "##
+ })
+ )
+ .child(
+ StoryItem::new("tbd",
+ div().flex().w_96().child(div().overflow_hidden().child(concat!(
+ "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ )))
+ )
+ )
+ .child(
+ StoryItem::new("Text in Horizontal Flex",
+ div().flex().w_96().bg(red()).child(concat!(
+ "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+ "Meanwhile, the lazy dog decided it was time for a change. ",
+ "He started daily workout routines, ate healthier and became the fastest dog in town.",
+ ))
+ )
+ .usage(indoc! {r##"
+ // NOTE: When rendering text in a horizonal flex container,
+ // Taffy will not pass width constraints down from the parent.
+ // To fix this, render text in a parent with overflow: hidden
+
+ div()
+ .max_w_96()
+ .child("Some text that you want to wrap.")
+ "##
+ })
+ )
+ .child(
+ StoryItem::new("Interactive Text",
+ InteractiveText::new(
+ "interactive",
+ StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+ (6..11, HighlightStyle {
+ background_color: Some(green()),
+ ..Default::default()
+ }),
+ ]),
+ )
+ .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+ println!("Clicked range {range_ix}");
+ })
+ )
+ .usage(indoc! {r##"
+ InteractiveText::new(
+ "interactive",
+ StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+ (6..11, HighlightStyle {
+ background_color: Some(green()),
+ ..Default::default()
+ }),
+ ]),
+ )
+ .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+ println!("Clicked range {range_ix}");
+ })
+ "##
+ })
+ )
+ ]
+ ).into_element()
}
}
+
+// TODO: Check all were updated to new style and remove
+
+// impl Render for TextStory {
+// type Element = Div;
+
+// fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+// v_stack()
+// .bg(blue())
+// .child(
+// div()
+// .flex()
+// .child(div().max_w_96().bg(white()).child(concat!(
+// "max-width: 96. The quick brown fox jumps over the lazy dog. ",
+// "Meanwhile, the lazy dog decided it was time for a change. ",
+// "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// ))),
+// )
+// .child(div().h_5())
+// .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
+// "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
+// "Meanwhile, the lazy dog decided it was time for a change. ",
+// "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// )))
+// .child(div().h_5())
+// .child(
+// div()
+// .flex()
+// .child(div().min_w_96().bg(white()).child(concat!(
+// "min-width: 96. The quick brown fox jumps over the lazy dog. ",
+// "Meanwhile, the lazy dog decided it was time for a change. ",
+// "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// ))))
+// .child(div().h_5())
+// .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
+// "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+// "Meanwhile, the lazy dog decided it was time for a change. ",
+// "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// ))))
+// // NOTE: When rendering text in a horizonal flex container,
+// // Taffy will not pass width constraints down from the parent.
+// // To fix this, render text in a parent with overflow: hidden
+// .child(div().h_5())
+// .child(div().flex().w_96().bg(red()).child(concat!(
+// "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+// "Meanwhile, the lazy dog decided it was time for a change. ",
+// "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// ))).child(
+// InteractiveText::new(
+// "interactive",
+// StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+// (6..11, HighlightStyle {
+// background_color: Some(green()),
+// ..Default::default()
+// }),
+// ]),
+// )
+// .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+// println!("Clicked range {range_ix}");
+// })
+// )
+// }
+// }
@@ -19,6 +19,7 @@ use ui::prelude::*;
use crate::assets::Assets;
use crate::story_selector::{ComponentStory, StorySelector};
+pub use indoc::indoc;
// gpui::actions! {
// storybook,
@@ -1,5 +1,5 @@
-use gpui::{Div, Render};
-use story::Story;
+use gpui::{Component, Render};
+use story::{StoryContainer, StoryItem, StorySection};
use crate::{prelude::*, Tooltip};
use crate::{Icon, IconButton};
@@ -7,57 +7,167 @@ use crate::{Icon, IconButton};
pub struct IconButtonStory;
impl Render for IconButtonStory {
- type Element = Div;
+ type Element = Component<StoryContainer>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container()
- .child(Story::title_for::<IconButton>())
- .child(Story::label("Default"))
- .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
- .child(Story::label("Selected"))
- .child(
- div()
- .w_8()
- .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
- )
- .child(Story::label("Selected with `selected_icon`"))
- .child(
- div().w_8().child(
- IconButton::new("icon_a", Icon::AudioOn)
- .selected(true)
- .selected_icon(Icon::AudioOff),
- ),
- )
- .child(Story::label("Disabled"))
- .child(
- div()
- .w_8()
- .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
- )
- .child(Story::label("With `on_click`"))
- .child(
- div()
- .w_8()
- .child(
- IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
- println!("Clicked!");
- }),
- ),
- )
- .child(Story::label("With `tooltip`"))
- .child(
- div().w_8().child(
- IconButton::new("with_tooltip", Icon::MessageBubbles)
- .tooltip(|cx| Tooltip::text("Open messages", cx)),
- ),
- )
- .child(Story::label("Selected with `tooltip`"))
- .child(
- div().w_8().child(
- IconButton::new("selected_with_tooltip", Icon::InlayHint)
- .selected(true)
- .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
- ),
- )
+ let default_button = StoryItem::new(
+ "Default",
+ IconButton::new("default_icon_button", Icon::Hash),
+ )
+ .description("Displays an icon button.")
+ .usage(
+ r#"
+ IconButton::new("default_icon_button", Icon::Hash)
+ "#,
+ );
+
+ let selected_button = StoryItem::new(
+ "Selected",
+ IconButton::new("selected_icon_button", Icon::Hash).selected(true),
+ )
+ .description("Displays an icon button that is selected.")
+ .usage(
+ r#"
+ IconButton::new("selected_icon_button", Icon::Hash).selected(true)
+ "#,
+ );
+
+ let selected_with_selected_icon = StoryItem::new(
+ "Selected with `selected_icon`",
+ IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+ .selected(true)
+ .selected_icon(Icon::AudioOff),
+ )
+ .description(
+ "Displays an icon button that is selected and shows a different icon when selected.",
+ )
+ .usage(
+ r#"
+ IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+ .selected(true)
+ .selected_icon(Icon::AudioOff)
+ "#,
+ );
+
+ let disabled_button = StoryItem::new(
+ "Disabled",
+ IconButton::new("disabled_icon_button", Icon::Hash).disabled(true),
+ )
+ .description("Displays an icon button that is disabled.")
+ .usage(
+ r#"
+ IconButton::new("disabled_icon_button", Icon::Hash).disabled(true)
+ "#,
+ );
+
+ let with_on_click_button = StoryItem::new(
+ "With `on_click`",
+ IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+ println!("Clicked!");
+ }),
+ )
+ .description("Displays an icon button which triggers an event on click.")
+ .usage(
+ r#"
+ IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+ println!("Clicked!");
+ })
+ "#,
+ );
+
+ let with_tooltip_button = StoryItem::new(
+ "With `tooltip`",
+ IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+ .tooltip(|cx| Tooltip::text("Open messages", cx)),
+ )
+ .description("Displays an icon button that has a tooltip when hovered.")
+ .usage(
+ r#"
+ IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+ .tooltip(|cx| Tooltip::text("Open messages", cx))
+ "#,
+ );
+
+ let selected_with_tooltip_button = StoryItem::new(
+ "Selected with `tooltip`",
+ IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+ .selected(true)
+ .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
+ )
+ .description("Displays a selected icon button with tooltip.")
+ .usage(
+ r#"
+ IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+ .selected(true)
+ .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx))
+ "#,
+ );
+
+ let buttons = vec![
+ default_button,
+ selected_button,
+ selected_with_selected_icon,
+ disabled_button,
+ with_on_click_button,
+ with_tooltip_button,
+ selected_with_tooltip_button,
+ ];
+
+ StoryContainer::new(
+ "Icon Button",
+ "crates/ui2/src/components/stories/icon_button.rs",
+ )
+ .children(vec![StorySection::new().children(buttons)])
+ .into_element()
+
+ // Story::container()
+ // .child(Story::title_for::<IconButton>())
+ // .child(Story::label("Default"))
+ // .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
+ // .child(Story::label("Selected"))
+ // .child(
+ // div()
+ // .w_8()
+ // .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
+ // )
+ // .child(Story::label("Selected with `selected_icon`"))
+ // .child(
+ // div().w_8().child(
+ // IconButton::new("icon_a", Icon::AudioOn)
+ // .selected(true)
+ // .selected_icon(Icon::AudioOff),
+ // ),
+ // )
+ // .child(Story::label("Disabled"))
+ // .child(
+ // div()
+ // .w_8()
+ // .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
+ // )
+ // .child(Story::label("With `on_click`"))
+ // .child(
+ // div()
+ // .w_8()
+ // .child(
+ // IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
+ // println!("Clicked!");
+ // }),
+ // ),
+ // )
+ // .child(Story::label("With `tooltip`"))
+ // .child(
+ // div().w_8().child(
+ // IconButton::new("with_tooltip", Icon::MessageBubbles)
+ // .tooltip(|cx| Tooltip::text("Open messages", cx)),
+ // ),
+ // )
+ // .child(Story::label("Selected with `tooltip`"))
+ // .child(
+ // div().w_8().child(
+ // IconButton::new("selected_with_tooltip", Icon::InlayHint)
+ // .selected(true)
+ // .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
+ // ),
+ // )
}
}
@@ -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)
@@ -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(())
@@ -1897,19 +1897,14 @@ impl Render for Pane {
.on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
.map(|div| {
if let Some(item) = self.active_item() {
- div.flex_col()
+ div.v_flex()
.child(self.toolbar.clone())
.child(item.to_any())
} else {
- div.flex()
- .flex_row()
- .items_center()
- .size_full()
- .justify_center()
- .child(
- Label::new("Open a file or project to get started.")
- .color(Color::Muted),
- )
+ div.h_flex().size_full().justify_center().child(
+ Label::new("Open a file or project to get started.")
+ .color(Color::Muted),
+ )
}
})
.child(
@@ -693,7 +693,8 @@ mod element {
use gpui::{
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
- MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Style, WindowContext,
+ MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style,
+ WindowContext,
};
use parking_lot::Mutex;
use smallvec::SmallVec;
@@ -736,7 +737,8 @@ mod element {
e: &MouseMoveEvent,
ix: usize,
axis: Axis,
- axis_bounds: Bounds<Pixels>,
+ child_start: Point<Pixels>,
+ container_size: Size<Pixels>,
cx: &mut WindowContext,
) {
let min_size = match axis {
@@ -747,7 +749,7 @@ mod element {
debug_assert!(flex_values_in_bounds(flexes.as_slice()));
let size = move |ix, flexes: &[f32]| {
- axis_bounds.size.along(axis) * (flexes[ix] / flexes.len() as f32)
+ container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
};
// Don't allow resizing to less than the minimum size, if elements are already too small
@@ -756,10 +758,10 @@ mod element {
}
let mut proposed_current_pixel_change =
- (e.position - axis_bounds.origin).along(axis) - size(ix, flexes.as_slice());
+ (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
- let flex_change = pixel_dx / axis_bounds.size.along(axis);
+ let flex_change = pixel_dx / container_size.along(axis);
let current_target_flex = flexes[target_ix] + flex_change;
let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
(current_target_flex, next_target_flex)
@@ -854,7 +856,15 @@ mod element {
cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
let dragged_handle = dragged_handle.borrow();
if phase.bubble() && *dragged_handle == Some(ix) {
- Self::compute_resize(&flexes, e, ix, axis, axis_bounds, cx)
+ Self::compute_resize(
+ &flexes,
+ e,
+ ix,
+ axis,
+ pane_bounds.origin,
+ axis_bounds.size,
+ cx,
+ )
}
});
});