From 3f7dc5951292d2fd717dde46891e7695a07324e5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 6 Nov 2023 12:33:20 -0800 Subject: [PATCH 01/14] Snapshot for kirill --- Cargo.lock | 17 ++ Cargo.toml | 1 + crates/gpui2/src/elements.rs | 1 + crates/gpui2/src/elements/img.rs | 2 + crates/gpui2/src/elements/list.rs | 91 +++++++ crates/picker2/Cargo.toml | 28 +++ crates/picker2/src/picker2.rs | 400 ++++++++++++++++++++++++++++++ 7 files changed, 540 insertions(+) create mode 100644 crates/gpui2/src/elements/list.rs create mode 100644 crates/picker2/Cargo.toml create mode 100644 crates/picker2/src/picker2.rs diff --git a/Cargo.lock b/Cargo.lock index 6d5fdc67d74a9a23f28aadf0497e760ad143b38d..3a888cfd23b2a8220d5520ead920de9ccfaa94e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6006,6 +6006,23 @@ dependencies = [ "workspace", ] +[[package]] +name = "picker2" +version = "0.1.0" +dependencies = [ + "ctor", + "editor2", + "env_logger 0.9.3", + "gpui2", + "menu2", + "parking_lot 0.11.2", + "serde_json", + "settings2", + "theme2", + "util", + "workspace2", +] + [[package]] name = "pico-args" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 6245889530bcefc9ea01a42efb49c1365859eb1c..853053f2c848a38ce3ef3957e31d7ba843d0d0df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ members = [ "crates/notifications", "crates/outline", "crates/picker", + "crates/picker2", "crates/plugin", "crates/plugin_macros", "crates/plugin_runtime", diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements.rs index 83c27b8a3b1a88e6dee517c485f5f99ea1df7a93..dc8baf4ca59f4cf7f3d28ade11490594038b8786 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements.rs @@ -1,5 +1,6 @@ mod div; mod img; +mod list; mod svg; mod text; diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index a35436d74ec9f668e4081eb9a9c0f1ea4168a914..637bcd78e626140a17bd39766303eb97e0ff9d32 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::{ div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId, ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..647d59586e6f59187053b4f0306620769ee5774e --- /dev/null +++ b/crates/gpui2/src/elements/list.rs @@ -0,0 +1,91 @@ +use std::ops::Range; + +use smallvec::SmallVec; + +use crate::{AnyElement, Component, Element, ElementId, StyleRefinement, ViewContext}; + +// We want to support uniform and non-uniform height +// We need to make the ID mandatory, to replace the 'state' field +// Previous implementation measured the first element as early as possible + +fn list<'a, Id, V, Iter, C>( + id: Id, + f: impl 'static + FnOnce(&'a mut V, Range, &'a mut ViewContext) -> Iter, +) -> List +where + Id: Into, + V: 'static, + Iter: 'a + Iterator, + C: Component, +{ + List { + id: id.into(), + render_items: Box::new(|view, visible_range, cx| { + f(view, visible_range, cx) + .map(|element| element.render()) + .collect() + }), + } +} + +struct List { + id: ElementId, + render_items: Box< + dyn for<'a> FnOnce( + &'a mut V, + Range, + &'a mut ViewContext, + ) -> SmallVec<[AnyElement; 64]>, + >, +} + +impl List {} + +// #[derive(Debug)] +// pub enum ScrollTarget { +// Show(usize), +// Center(usize), +// } + +#[derive(Default)] +struct ListState { + scroll_top: f32, + style: StyleRefinement, + // todo + // scroll_to: Option, +} +impl Element for List { + type ElementState = ListState; + + fn id(&self) -> Option { + Some(self.id) + } + + fn initialize( + &mut self, + _: &mut V, + element_state: Option, + _: &mut crate::ViewContext, + ) -> Self::ElementState { + let element_state = element_state.unwrap_or_default(); + element_state + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) -> crate::LayoutId { + todo!() + } + + fn paint( + &mut self, + bounds: crate::Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + } +} diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8d88c25366fa52bcaa20018503608d3dd86ebbf2 --- /dev/null +++ b/crates/picker2/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "picker2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/picker2.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +menu = { package = "menu2", path = "../menu2" } +settings = { package = "settings2", path = "../settings2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } + +parking_lot.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +serde_json.workspace = true +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs new file mode 100644 index 0000000000000000000000000000000000000000..c6c5ee3e41e78b18fb84143d0ff448873d31cc5b --- /dev/null +++ b/crates/picker2/src/picker2.rs @@ -0,0 +1,400 @@ +// use editor::Editor; +// use gpui::{ +// elements::*, +// geometry::vector::{vec2f, Vector2F}, +// keymap_matcher::KeymapContext, +// platform::{CursorStyle, MouseButton}, +// AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext, +// ViewHandle, +// }; +// use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; +// use parking_lot::Mutex; +// use std::{cmp, sync::Arc}; +// use util::ResultExt; +// use workspace::Modal; + +// #[derive(Clone, Copy)] +// pub enum PickerEvent { +// Dismiss, +// } + +use std::ops::Range; + +use gpui::{div, AppContext, Component, Div, Element, ParentElement, Render, ViewContext}; + +pub struct Picker { + delegate: D, + // query_editor: ViewHandle, + // list_state: UniformListState, + // max_size: Vector2F, + // theme: Arc theme::Picker>>>, + // confirmed: bool, + // pending_update_matches: Option>>, + // confirm_on_update: Option, + // has_focus: bool, +} + +pub trait PickerDelegate: Sized + 'static { + type ListItem: Element>; + // fn placeholder_text(&self) -> Arc; + // fn match_count(&self) -> usize; + // fn selected_index(&self) -> usize; + // fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); + // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; + // fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); + // fn dismissed(&mut self, cx: &mut ViewContext>); + + // todo!("rename to render_candidate?") + fn render_match( + &self, + ix: usize, + active: bool, + hovered: bool, + selected: bool, + cx: &mut ViewContext>, + ) -> Self::ListItem; + + // fn center_selection_after_match_updates(&self) -> bool { + // false + // } + // fn render_header( + // &self, + // _cx: &mut ViewContext>, + // ) -> Option>> { + // None + // } + // fn render_footer( + // &self, + // _cx: &mut ViewContext>, + // ) -> Option>> { + // None + // } +} + +// impl Entity for Picker { +// type Event = PickerEvent; +// } + +impl Render for Picker { + type Element = Div; + + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + div().child(list( + "candidates", + |this: &mut Picker, visible_range, cx| { + visible_range + .into_iter() + .map(|ix| this.delegate.render_match(ix, false, false, false, cx)) + }, + )) + } +} + +fn list<'a, D: PickerDelegate, F, I>(id: &'static str, f: F) -> Div> +where + F: FnOnce(&'a mut Picker, Range, &'a mut ViewContext>) -> I, + I: 'a + Iterator, +{ + todo!(); +} + +// impl View for Picker { +// fn ui_name() -> &'static str { +// "Picker" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let theme = (self.theme.lock())(theme::current(cx).as_ref()); +// let query = self.query(cx); +// let match_count = self.delegate.match_count(); + +// let container_style; +// let editor_style; +// if query.is_empty() && match_count == 0 { +// container_style = theme.empty_container; +// editor_style = theme.empty_input_editor.container; +// } else { +// container_style = theme.container; +// editor_style = theme.input_editor.container; +// }; + +// Flex::new(Axis::Vertical) +// .with_child( +// ChildView::new(&self.query_editor, cx) +// .contained() +// .with_style(editor_style), +// ) +// .with_children(self.delegate.render_header(cx)) +// .with_children(if match_count == 0 { +// if query.is_empty() { +// None +// } else { +// Some( +// Label::new("No matches", theme.no_matches.label.clone()) +// .contained() +// .with_style(theme.no_matches.container) +// .into_any(), +// ) +// } +// } else { +// Some( +// UniformList::new( +// self.list_state.clone(), +// match_count, +// cx, +// move |this, mut range, items, cx| { +// let selected_ix = this.delegate.selected_index(); +// range.end = cmp::min(range.end, this.delegate.match_count()); +// items.extend(range.map(move |ix| { +// MouseEventHandler::new::(ix, cx, |state, cx| { +// this.delegate.render_match(ix, state, ix == selected_ix, cx) +// }) +// // Capture mouse events +// .on_down(MouseButton::Left, |_, _, _| {}) +// .on_up(MouseButton::Left, |_, _, _| {}) +// .on_click(MouseButton::Left, move |click, picker, cx| { +// picker.select_index(ix, click.cmd, cx); +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .into_any() +// })); +// }, +// ) +// .contained() +// .with_margin_top(6.0) +// .flex(1., false) +// .into_any(), +// ) +// }) +// .with_children(self.delegate.render_footer(cx)) +// .contained() +// .with_style(container_style) +// .constrained() +// .with_max_width(self.max_size.x()) +// .with_max_height(self.max_size.y()) +// .into_any_named("picker") +// } + +// fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { +// Self::reset_to_default_keymap_context(keymap); +// keymap.add_identifier("menu"); +// } + +// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { +// self.has_focus = true; +// if cx.is_self_focused() { +// cx.focus(&self.query_editor); +// } +// } + +// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { +// self.has_focus = false; +// } +// } + +// impl Modal for Picker { +// fn has_focus(&self) -> bool { +// self.has_focus +// } + +// fn dismiss_on_event(event: &Self::Event) -> bool { +// matches!(event, PickerEvent::Dismiss) +// } +// } + +// impl Picker { +// pub fn init(cx: &mut AppContext) { +// cx.add_action(Self::select_first); +// cx.add_action(Self::select_last); +// cx.add_action(Self::select_next); +// cx.add_action(Self::select_prev); +// cx.add_action(Self::confirm); +// cx.add_action(Self::secondary_confirm); +// cx.add_action(Self::cancel); +// } + +// pub fn new(delegate: D, cx: &mut ViewContext) -> Self { +// let theme = Arc::new(Mutex::new( +// Box::new(|theme: &theme::Theme| theme.picker.clone()) +// as Box theme::Picker>, +// )); +// let placeholder_text = delegate.placeholder_text(); +// let query_editor = cx.add_view({ +// let picker_theme = theme.clone(); +// |cx| { +// let mut editor = Editor::single_line( +// Some(Arc::new(move |theme| { +// (picker_theme.lock())(theme).input_editor.clone() +// })), +// cx, +// ); +// editor.set_placeholder_text(placeholder_text, cx); +// editor +// } +// }); +// cx.subscribe(&query_editor, Self::on_query_editor_event) +// .detach(); +// let mut this = Self { +// query_editor, +// list_state: Default::default(), +// delegate, +// max_size: vec2f(540., 420.), +// theme, +// confirmed: false, +// pending_update_matches: None, +// confirm_on_update: None, +// has_focus: false, +// }; +// this.update_matches(String::new(), cx); +// this +// } + +// pub fn with_max_size(mut self, width: f32, height: f32) -> Self { +// self.max_size = vec2f(width, height); +// self +// } + +// pub fn with_theme(self, theme: F) -> Self +// where +// F: 'static + Fn(&theme::Theme) -> theme::Picker, +// { +// *self.theme.lock() = Box::new(theme); +// self +// } + +// pub fn delegate(&self) -> &D { +// &self.delegate +// } + +// pub fn delegate_mut(&mut self) -> &mut D { +// &mut self.delegate +// } + +// pub fn query(&self, cx: &AppContext) -> String { +// self.query_editor.read(cx).text(cx) +// } + +// pub fn set_query(&self, query: impl Into>, cx: &mut ViewContext) { +// self.query_editor +// .update(cx, |editor, cx| editor.set_text(query, cx)); +// } + +// fn on_query_editor_event( +// &mut self, +// _: ViewHandle, +// event: &editor::Event, +// cx: &mut ViewContext, +// ) { +// match event { +// editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx), +// editor::Event::Blurred if !self.confirmed => { +// self.dismiss(cx); +// } +// _ => {} +// } +// } + +// pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { +// let update = self.delegate.update_matches(query, cx); +// self.matches_updated(cx); +// self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move { +// update.await; +// this.update(&mut cx, |this, cx| { +// this.matches_updated(cx); +// }) +// .log_err() +// })); +// } + +// fn matches_updated(&mut self, cx: &mut ViewContext) { +// let index = self.delegate.selected_index(); +// let target = if self.delegate.center_selection_after_match_updates() { +// ScrollTarget::Center(index) +// } else { +// ScrollTarget::Show(index) +// }; +// self.list_state.scroll_to(target); +// self.pending_update_matches = None; +// if let Some(secondary) = self.confirm_on_update.take() { +// self.confirmed = true; +// self.delegate.confirm(secondary, cx) +// } +// cx.notify(); +// } + +// pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { +// if self.delegate.match_count() > 0 { +// self.delegate.set_selected_index(0, cx); +// self.list_state.scroll_to(ScrollTarget::Show(0)); +// } + +// cx.notify(); +// } + +// pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext) { +// if self.delegate.match_count() > 0 { +// self.confirmed = true; +// self.delegate.set_selected_index(index, cx); +// self.delegate.confirm(cmd, cx); +// } +// } + +// pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { +// let match_count = self.delegate.match_count(); +// if match_count > 0 { +// let index = match_count - 1; +// self.delegate.set_selected_index(index, cx); +// self.list_state.scroll_to(ScrollTarget::Show(index)); +// } +// cx.notify(); +// } + +// pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { +// let next_index = self.delegate.selected_index() + 1; +// if next_index < self.delegate.match_count() { +// self.delegate.set_selected_index(next_index, cx); +// self.list_state.scroll_to(ScrollTarget::Show(next_index)); +// } + +// cx.notify(); +// } + +// pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { +// let mut selected_index = self.delegate.selected_index(); +// if selected_index > 0 { +// selected_index -= 1; +// self.delegate.set_selected_index(selected_index, cx); +// self.list_state +// .scroll_to(ScrollTarget::Show(selected_index)); +// } + +// cx.notify(); +// } + +// pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { +// if self.pending_update_matches.is_some() { +// self.confirm_on_update = Some(false) +// } else { +// self.confirmed = true; +// self.delegate.confirm(false, cx); +// } +// } + +// pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext) { +// if self.pending_update_matches.is_some() { +// self.confirm_on_update = Some(true) +// } else { +// self.confirmed = true; +// self.delegate.confirm(true, cx); +// } +// } + +// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { +// self.dismiss(cx); +// } + +// fn dismiss(&mut self, cx: &mut ViewContext) { +// cx.emit(PickerEvent::Dismiss); +// self.delegate.dismissed(cx); +// } +// } From ea6755b1cabd70098b308d632ba7a64a630f84fb Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 6 Nov 2023 14:26:10 -0800 Subject: [PATCH 02/14] Checkpoint --- Cargo.lock | 1 + crates/editor2/src/element.rs | 2 +- crates/gpui2/src/app/entity_map.rs | 6 +- crates/gpui2/src/elements.rs | 1 + crates/gpui2/src/elements/list.rs | 107 +++++++++++++++++++----- crates/gpui2/src/geometry.rs | 41 ++++++--- crates/gpui2/src/styled.rs | 7 +- crates/gpui2/src/window.rs | 6 ++ crates/picker2/src/picker2.rs | 78 +++++++++-------- crates/storybook2/Cargo.toml | 1 + crates/storybook2/src/stories.rs | 2 + crates/storybook2/src/stories/picker.rs | 40 +++++++++ crates/storybook2/src/story_selector.rs | 2 + crates/terminal2/src/mappings/mouse.rs | 4 +- crates/terminal2/src/terminal2.rs | 11 ++- 15 files changed, 232 insertions(+), 77 deletions(-) create mode 100644 crates/storybook2/src/stories/picker.rs diff --git a/Cargo.lock b/Cargo.lock index 3a888cfd23b2a8220d5520ead920de9ccfaa94e4..98feaf9f61053e7d7ae701c3b72f7e3411b45111 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8542,6 +8542,7 @@ dependencies = [ "gpui2", "itertools 0.11.0", "log", + "picker2", "rust-embed", "serde", "settings2", diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 6420d1e6cd6482ac99e79f3de0dfbd3c2963359c..4e42124c02dbfbd07e5c7a8d6792e0d2e91e5c80 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2834,7 +2834,7 @@ impl PositionMap { let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); - let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance).into(); + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; *exact_unclipped.column_mut() += column_overshoot_after_line_end; PointForPosition { previous_valid, diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index e626f8c409d2a80ac9f7ac2116741855f7e5beb3..588091c7a0bb1e3027b3a81cc0be2fa55f98e0f3 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -13,6 +13,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, Weak, }, + thread::panicking, }; slotmap::new_key_type! { pub struct EntityId; } @@ -140,9 +141,8 @@ impl<'a, T: 'static> core::ops::DerefMut for Lease<'a, T> { impl<'a, T> Drop for Lease<'a, T> { fn drop(&mut self) { - if self.entity.is_some() { - // We don't panic here, because other panics can cause us to drop the lease without ending it cleanly. - log::error!("Leases must be ended with EntityMap::end_lease") + if self.entity.is_some() && !panicking() { + panic!("Leases must be ended with EntityMap::end_lease") } } } diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements.rs index dc8baf4ca59f4cf7f3d28ade11490594038b8786..3ebe5bf5d1fbb4196c91885c230d23fd0d5aa170 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements.rs @@ -6,5 +6,6 @@ mod text; pub use div::*; pub use img::*; +pub use list::*; pub use svg::*; pub use text::*; diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 647d59586e6f59187053b4f0306620769ee5774e..01a64f7bd12f8559d30e00a0d51736b00e3982a3 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -2,36 +2,44 @@ use std::ops::Range; use smallvec::SmallVec; -use crate::{AnyElement, Component, Element, ElementId, StyleRefinement, ViewContext}; +use crate::{ + point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId, + LayoutId, Pixels, Size, StyleRefinement, Styled, ViewContext, +}; // We want to support uniform and non-uniform height // We need to make the ID mandatory, to replace the 'state' field // Previous implementation measured the first element as early as possible -fn list<'a, Id, V, Iter, C>( +pub fn list( id: Id, - f: impl 'static + FnOnce(&'a mut V, Range, &'a mut ViewContext) -> Iter, + item_count: usize, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> SmallVec<[C; 64]>, ) -> List where Id: Into, V: 'static, - Iter: 'a + Iterator, C: Component, { List { id: id.into(), - render_items: Box::new(|view, visible_range, cx| { + style: Default::default(), + item_count, + render_items: Box::new(move |view, visible_range, cx| { f(view, visible_range, cx) - .map(|element| element.render()) + .into_iter() + .map(|component| component.render()) .collect() }), } } -struct List { +pub struct List { id: ElementId, + style: StyleRefinement, + item_count: usize, render_items: Box< - dyn for<'a> FnOnce( + dyn for<'a> Fn( &'a mut V, Range, &'a mut ViewContext, @@ -39,8 +47,6 @@ struct List { >, } -impl List {} - // #[derive(Debug)] // pub enum ScrollTarget { // Show(usize), @@ -48,24 +54,30 @@ impl List {} // } #[derive(Default)] -struct ListState { +pub struct ListState { scroll_top: f32, - style: StyleRefinement, // todo // scroll_to: Option, } + +impl Styled for List { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + impl Element for List { type ElementState = ListState; fn id(&self) -> Option { - Some(self.id) + Some(self.id.clone()) } fn initialize( &mut self, _: &mut V, element_state: Option, - _: &mut crate::ViewContext, + _: &mut ViewContext, ) -> Self::ElementState { let element_state = element_state.unwrap_or_default(); element_state @@ -73,11 +85,11 @@ impl Element for List { fn layout( &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, - ) -> crate::LayoutId { - todo!() + _view_state: &mut V, + _element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + cx.request_layout(&self.computed_style(), None) } fn paint( @@ -85,7 +97,62 @@ impl Element for List { bounds: crate::Bounds, view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, + cx: &mut ViewContext, ) { + let style = self.computed_style(); + style.paint(bounds, cx); + + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.lower_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + if self.item_count > 0 { + let item_height = self.measure_item_height(view_state, padded_bounds, cx); + let visible_item_count = (padded_bounds.size.height / item_height) as usize; + let visible_range = 0..visible_item_count; + + let mut items = (self.render_items)(view_state, visible_range, cx); + + for (ix, item) in items.iter_mut().enumerate() { + item.initialize(view_state, cx); + item.layout(view_state, cx); + let offset = padded_bounds.origin + point(px(0.), item_height * ix); + cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx)) + } + } + } +} + +impl List { + fn measure_item_height( + &self, + view_state: &mut V, + list_bounds: Bounds, + cx: &mut ViewContext, + ) -> Pixels { + let mut items = (self.render_items)(view_state, 0..1, cx); + debug_assert!(items.len() == 1); + let mut item_to_measure = items.pop().unwrap(); + item_to_measure.initialize(view_state, cx); + let layout_id = item_to_measure.layout(view_state, cx); + cx.compute_layout( + layout_id, + Size { + width: AvailableSpace::Definite(list_bounds.size.width), + height: AvailableSpace::MinContent, + }, + ); + cx.layout_bounds(layout_id).size.height + } +} + +impl Component for List { + fn render(self) -> AnyElement { + AnyElement::new(self) } } diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index d6755a53973f00d5fee4fd11912f6e73da26cf71..a9df7569325e92d941d8d496fceb018efae7c2b7 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -259,6 +259,24 @@ impl From> for Size { } } +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + impl Size { pub fn full() -> Self { Self { @@ -541,6 +559,15 @@ impl Edges { left: px(0.).into(), } } + + pub fn to_pixels(&self, parent_size: Size, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(parent_size.height, rem_size), + right: self.right.to_pixels(parent_size.width, rem_size), + bottom: self.bottom.to_pixels(parent_size.height, rem_size), + left: self.left.to_pixels(parent_size.width, rem_size), + } + } } impl Edges { @@ -672,16 +699,16 @@ impl Copy for Corners where T: Copy + Clone + Default + Debug {} pub struct Pixels(pub(crate) f32); impl std::ops::Div for Pixels { - type Output = Self; + type Output = f32; fn div(self, rhs: Self) -> Self::Output { - Self(self.0 / rhs.0) + self.0 / rhs.0 } } impl std::ops::DivAssign for Pixels { fn div_assign(&mut self, rhs: Self) { - self.0 /= rhs.0; + *self = Self(self.0 / rhs.0); } } @@ -732,14 +759,6 @@ impl MulAssign for Pixels { impl Pixels { pub const MAX: Pixels = Pixels(f32::MAX); - pub fn as_usize(&self) -> usize { - self.0 as usize - } - - pub fn as_isize(&self) -> isize { - self.0 as isize - } - pub fn floor(&self) -> Self { Self(self.0.floor()) } diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index a272ab95b14d7416e91d0bf166f2683e7941f02e..f45e6a219e1a9206a94415a25cf073d329839801 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -1,14 +1,19 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AlignItems, CursorStyle, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, - StyleRefinement, Visibility, + Style, StyleRefinement, Visibility, }; use crate::{BoxShadow, TextStyleRefinement}; +use refineable::Refineable; use smallvec::smallvec; pub trait Styled { fn style(&mut self) -> &mut StyleRefinement; + fn computed_style(&mut self) -> Style { + Style::default().refined(self.style().clone()) + } + gpui2_macros::style_helpers!(); /// Sets the size of the element to the full width and height. diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 9cab40082b02912eced41363a2afa6506fb3d9c0..880fc6c6cb22f423363940053b38fe198f31e612 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -559,6 +559,12 @@ impl<'a> WindowContext<'a> { .request_measured_layout(style, rem_size, measure) } + pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { + self.window + .layout_engine + .compute_layout(layout_id, available_space) + } + /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not /// be invoked until the paint phase begins, and will usually be invoked by GPUI itself automatically /// in order to pass your element its `Bounds` automatically. diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index c6c5ee3e41e78b18fb84143d0ff448873d31cc5b..9b4f8778e2a5aca7befa4a2becdd6665edbf2f18 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -20,24 +20,28 @@ use std::ops::Range; -use gpui::{div, AppContext, Component, Div, Element, ParentElement, Render, ViewContext}; - -pub struct Picker { - delegate: D, - // query_editor: ViewHandle, - // list_state: UniformListState, - // max_size: Vector2F, - // theme: Arc theme::Picker>>>, - // confirmed: bool, - // pending_update_matches: Option>>, - // confirm_on_update: Option, - // has_focus: bool, -} +use gpui::{ + div, list, AppContext, Component, Div, Element, ElementId, ParentElement, Render, ViewContext, +}; + +// pub struct Picker { +// delegate: D, +// query_editor: ViewHandle, +// list_state: UniformListState, +// max_size: Vector2F, +// theme: Arc theme::Picker>>>, +// confirmed: bool, +// pending_update_matches: Option>>, +// confirm_on_update: Option, +// has_focus: bool, +// } pub trait PickerDelegate: Sized + 'static { - type ListItem: Element>; + type ListItem: Component; // fn placeholder_text(&self) -> Arc; - // fn match_count(&self) -> usize; + + fn match_count(&self, picker_id: ElementId) -> usize; + // fn selected_index(&self) -> usize; // fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; @@ -51,7 +55,8 @@ pub trait PickerDelegate: Sized + 'static { active: bool, hovered: bool, selected: bool, - cx: &mut ViewContext>, + picker_id: ElementId, + cx: &mut ViewContext, ) -> Self::ListItem; // fn center_selection_after_match_updates(&self) -> bool { @@ -75,29 +80,35 @@ pub trait PickerDelegate: Sized + 'static { // type Event = PickerEvent; // } -impl Render for Picker { - type Element = Div; +#[derive(Component)] +pub struct Picker { + id: ElementId, + phantom: std::marker::PhantomData, +} + +impl Picker { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + phantom: std::marker::PhantomData, + } + } +} - fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - div().child(list( +impl Picker { + pub fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { + div().id(self.id.clone()).child(list( "candidates", - |this: &mut Picker, visible_range, cx| { + view.match_count(self.id.clone()), + move |this: &mut V, visible_range, cx| { visible_range - .into_iter() - .map(|ix| this.delegate.render_match(ix, false, false, false, cx)) + .map(|ix| this.render_match(ix, false, false, false, self.id.clone(), cx)) + .collect() }, )) } } -fn list<'a, D: PickerDelegate, F, I>(id: &'static str, f: F) -> Div> -where - F: FnOnce(&'a mut Picker, Range, &'a mut ViewContext>) -> I, - I: 'a + Iterator, -{ - todo!(); -} - // impl View for Picker { // fn ui_name() -> &'static str { // "Picker" @@ -213,7 +224,7 @@ where // cx.add_action(Self::cancel); // } -// pub fn new(delegate: D, cx: &mut ViewContext) -> Self { +// pub fn new(delegate: D, cx: &mut ViewContext) -> Self { // let theme = Arc::new(Mutex::new( // Box::new(|theme: &theme::Theme| theme.picker.clone()) // as Box theme::Picker>, @@ -247,7 +258,8 @@ where // }; // this.update_matches(String::new(), cx); // this -// } +// Self { delegate } +// } // pub fn with_max_size(mut self, width: f32, height: f32) -> Self { // self.max_size = vec2f(width, height); diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 1f3a0b33cc2254671069d8e8c1f0fb0e453db9ae..0e35a7a66f3f892aec1df787456ce9e9d00acfb1 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -27,6 +27,7 @@ theme = { path = "../theme" } theme2 = { path = "../theme2" } ui = { package = "ui2", path = "../ui2", features = ["stories"] } util = { path = "../util" } +picker = { package = "picker2", path = "../picker2" } [dev-dependencies] gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/storybook2/src/stories.rs b/crates/storybook2/src/stories.rs index 3d8a332fb93b16ae8480acd32e97bbd4a0c5ab25..2620e68d6c102600637f41551e5d6f65b96451e1 100644 --- a/crates/storybook2/src/stories.rs +++ b/crates/storybook2/src/stories.rs @@ -1,6 +1,7 @@ mod colors; mod focus; mod kitchen_sink; +mod picker; mod scroll; mod text; mod z_index; @@ -8,6 +9,7 @@ mod z_index; pub use colors::*; pub use focus::*; pub use kitchen_sink::*; +pub use picker::*; pub use scroll::*; pub use text::*; pub use z_index::*; diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..de983e0326890aeff3a4f9ec8126648adaf164a2 --- /dev/null +++ b/crates/storybook2/src/stories/picker.rs @@ -0,0 +1,40 @@ +use gpui::{div, Div, ParentElement, Render, View, VisualContext, WindowContext}; +use picker::{Picker, PickerDelegate}; + +pub struct PickerStory { + // picker: View>, +} + +impl PickerDelegate for PickerStory { + type ListItem = Div; + + fn match_count(&self, picker_id: gpui::ElementId) -> usize { + 0 + } + + fn render_match( + &self, + ix: usize, + active: bool, + hovered: bool, + selected: bool, + picker_id: gpui::ElementId, + cx: &mut gpui::ViewContext, + ) -> Self::ListItem { + todo!() + } +} + +impl PickerStory { + pub fn new(cx: &mut WindowContext) -> View { + cx.build_view(|cx| PickerStory {}) + } +} + +impl Render for PickerStory { + type Element = Div; + + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + div().child(Picker::new("picker_story")) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index f59208ccb8794948dd93afa99155d707ea91ad0d..766c0dd51a3d60809161d16bfd64780adad98026 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -51,6 +51,7 @@ pub enum ComponentStory { TrafficLights, Workspace, ZIndex, + Picker, } impl ComponentStory { @@ -94,6 +95,7 @@ impl ComponentStory { Self::TrafficLights => cx.build_view(|_| ui::TrafficLightsStory).into(), Self::Workspace => ui::WorkspaceStory::view(cx).into(), Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), + Self::Picker => PickerStory::new(cx).into(), } } } diff --git a/crates/terminal2/src/mappings/mouse.rs b/crates/terminal2/src/mappings/mouse.rs index eac6ad17ff257384396c4c639dd6f7ea39acea5f..edced3156faf90cdf9dc227ee646f0e4cccb68e3 100644 --- a/crates/terminal2/src/mappings/mouse.rs +++ b/crates/terminal2/src/mappings/mouse.rs @@ -186,9 +186,9 @@ pub fn mouse_side( } pub fn grid_point(pos: Point, cur_size: TerminalSize, display_offset: usize) -> AlacPoint { - let col = GridCol((pos.x / cur_size.cell_width).as_usize()); + let col = GridCol((cur_size.cell_width / pos.x) as usize); let col = min(col, cur_size.last_column()); - let line = (pos.y / cur_size.line_height).as_isize() as i32; + let line = (cur_size.line_height / pos.y) as i32; let line = min(line, cur_size.bottommost_line().0); AlacPoint::new(GridLine(line - display_offset as i32), col) } diff --git a/crates/terminal2/src/terminal2.rs b/crates/terminal2/src/terminal2.rs index ba5c4815f29253152c355c1ba4d998e54c2fc9e5..3d06b488123016c19102e8d911d6b24568124ab1 100644 --- a/crates/terminal2/src/terminal2.rs +++ b/crates/terminal2/src/terminal2.rs @@ -1121,8 +1121,7 @@ impl Terminal { None => return, }; - let scroll_lines = - (scroll_delta / self.last_content.size.line_height).as_isize() as i32; + let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32; self.events .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); @@ -1280,11 +1279,11 @@ impl Terminal { } /* Calculate the appropriate scroll lines */ TouchPhase::Moved => { - let old_offset = (self.scroll_px / line_height).as_isize() as i32; + let old_offset = (self.scroll_px / line_height) as i32; self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier; - let new_offset = (self.scroll_px / line_height).as_isize() as i32; + let new_offset = (self.scroll_px / line_height) as i32; // Whenever we hit the edges, reset our stored scroll to 0 // so we can respond to changes in direction quickly @@ -1396,9 +1395,9 @@ fn all_search_matches<'a, T>( } fn content_index_for_mouse(pos: Point, size: &TerminalSize) -> usize { - let col = (pos.x / size.cell_width()).round().as_usize(); + let col = (pos.x / size.cell_width()).round() as usize; let clamped_col = min(col, size.columns() - 1); - let row = (pos.y / size.line_height()).round().as_usize(); + let row = (pos.y / size.line_height()).round() as usize; let clamped_row = min(row, size.screen_lines() - 1); clamped_row * size.columns() + clamped_col } From 3c93b585ab183153f6f3c1dd96ecf4ce9e00d90d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 6 Nov 2023 15:11:22 -0800 Subject: [PATCH 03/14] Checkpoint --- crates/gpui2/src/app.rs | 8 ++-- crates/gpui2/src/elements/list.rs | 18 ++++++-- crates/gpui2/src/elements/text.rs | 1 + crates/gpui2/src/text_system/line.rs | 7 +-- crates/gpui2/src/window.rs | 2 + crates/picker2/src/picker2.rs | 24 +++++----- crates/storybook2/src/stories/picker.rs | 58 ++++++++++++++++++++----- 7 files changed, 85 insertions(+), 33 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 9afffeb685a8c38ecd8a0aaf075ce6a2c4fbdf5e..801047de5fc857fd3a1bab0a89535febc5126c25 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -48,15 +48,15 @@ pub struct AppCell { impl AppCell { #[track_caller] pub fn borrow(&self) -> AppRef { - let thread_id = std::thread::current().id(); - eprintln!("borrowed {thread_id:?}"); + // let thread_id = std::thread::current().id(); + // eprintln!("borrowed {thread_id:?}"); AppRef(self.app.borrow()) } #[track_caller] pub fn borrow_mut(&self) -> AppRefMut { - let thread_id = std::thread::current().id(); - eprintln!("borrowed {thread_id:?}"); + // let thread_id = std::thread::current().id(); + // eprintln!("borrowed {thread_id:?}"); AppRefMut(self.app.borrow_mut()) } } diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 01a64f7bd12f8559d30e00a0d51736b00e3982a3..7f15af66c4289afa197911e8730016b851465ee9 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{cmp, ops::Range}; use smallvec::SmallVec; @@ -113,14 +113,24 @@ impl Element for List { if self.item_count > 0 { let item_height = self.measure_item_height(view_state, padded_bounds, cx); - let visible_item_count = (padded_bounds.size.height / item_height) as usize; - let visible_range = 0..visible_item_count; + let visible_item_count = (padded_bounds.size.height / item_height).ceil() as usize; + let visible_range = 0..cmp::min(visible_item_count, self.item_count); let mut items = (self.render_items)(view_state, visible_range, cx); + dbg!(items.len(), self.item_count, visible_item_count); + for (ix, item) in items.iter_mut().enumerate() { item.initialize(view_state, cx); - item.layout(view_state, cx); + + let layout_id = item.layout(view_state, cx); + cx.compute_layout( + layout_id, + Size { + width: AvailableSpace::Definite(bounds.size.width), + height: AvailableSpace::Definite(item_height), + }, + ); let offset = padded_bounds.origin + point(px(0.), item_height * ix); cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx)) } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 4bc37054902653a85f931d82354183edc9142fe3..7e771962cd5619db7c5e724b7cf703860287115b 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -127,6 +127,7 @@ impl Element for Text { let element_state = element_state .as_ref() .expect("measurement has not been performed"); + let line_height = element_state.line_height; let mut line_origin = bounds.origin; for line in &element_state.lines { diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 63aac96faff9be6f3a452ecf034f501952a4285f..21a7dcea6fcc9ec1f20f1b6d066bb24ecf9ae8da 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -78,7 +78,6 @@ impl Line { glyph_origin.y += line_height; } prev_glyph_position = glyph.position; - let glyph_origin = glyph_origin + baseline_offset; let mut finished_underline: Option<(Point, UnderlineStyle)> = None; if glyph.index >= run_end { @@ -129,20 +128,22 @@ impl Line { if max_glyph_bounds.intersects(&content_mask.bounds) { if glyph.is_emoji { cx.paint_emoji( - glyph_origin, + glyph_origin + baseline_offset, run.font_id, glyph.id, self.layout.layout.font_size, )?; } else { cx.paint_glyph( - glyph_origin, + glyph_origin + baseline_offset, run.font_id, glyph.id, self.layout.layout.font_size, color, )?; } + } else { + dbg!(content_mask.bounds, max_glyph_bounds); } } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 880fc6c6cb22f423363940053b38fe198f31e612..7c05018aa1aaca1fb64461cab20989827ac81161 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -792,6 +792,7 @@ impl<'a> WindowContext<'a> { } /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. + /// The y component of the origin is the baseline of the glyph. pub fn paint_glyph( &mut self, origin: Point, @@ -845,6 +846,7 @@ impl<'a> WindowContext<'a> { } /// Paint an emoji glyph into the scene for the current frame at the current z-index. + /// The y component of the origin is the baseline of the glyph. pub fn paint_emoji( &mut self, origin: Point, diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 9b4f8778e2a5aca7befa4a2becdd6665edbf2f18..3bcf5b92037a48e3e99fa372c442820bf084d000 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -21,7 +21,8 @@ use std::ops::Range; use gpui::{ - div, list, AppContext, Component, Div, Element, ElementId, ParentElement, Render, ViewContext, + div, list, red, AppContext, Component, Div, Element, ElementId, ParentElement, Render, Styled, + ViewContext, }; // pub struct Picker { @@ -97,15 +98,18 @@ impl Picker { impl Picker { pub fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { - div().id(self.id.clone()).child(list( - "candidates", - view.match_count(self.id.clone()), - move |this: &mut V, visible_range, cx| { - visible_range - .map(|ix| this.render_match(ix, false, false, false, self.id.clone(), cx)) - .collect() - }, - )) + div().size_full().id(self.id.clone()).child( + list( + "candidates", + view.match_count(self.id.clone()), + move |this: &mut V, visible_range, cx| { + visible_range + .map(|ix| this.render_match(ix, false, false, false, self.id.clone(), cx)) + .collect() + }, + ) + .size_full(), + ) } } diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index de983e0326890aeff3a4f9ec8126648adaf164a2..23d13b83fc80a53b8789bfd0fe73dd3af9d9e07f 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,33 +1,64 @@ -use gpui::{div, Div, ParentElement, Render, View, VisualContext, WindowContext}; +use gpui::{ + black, div, red, Div, Fill, ParentElement, Render, SharedString, Styled, View, VisualContext, + WindowContext, +}; use picker::{Picker, PickerDelegate}; pub struct PickerStory { - // picker: View>, + candidates: Vec, } impl PickerDelegate for PickerStory { - type ListItem = Div; + type ListItem = SharedString; - fn match_count(&self, picker_id: gpui::ElementId) -> usize { - 0 + fn match_count(&self, _picker_id: gpui::ElementId) -> usize { + self.candidates.len() } fn render_match( &self, ix: usize, - active: bool, - hovered: bool, - selected: bool, - picker_id: gpui::ElementId, + _active: bool, + _hovered: bool, + _selected: bool, + _picker_id: gpui::ElementId, cx: &mut gpui::ViewContext, ) -> Self::ListItem { - todo!() + self.candidates[ix].clone() } } impl PickerStory { pub fn new(cx: &mut WindowContext) -> View { - cx.build_view(|cx| PickerStory {}) + cx.build_view(|cx| PickerStory { + candidates: vec![ + "Pizza (Italy)".into(), + "Sushi (Japan)".into(), + "Paella (Spain)".into(), + "Tacos (Mexico)".into(), + "Peking Duck (China)".into(), + "Fish and Chips (UK)".into(), + "Croissant (France)".into(), + "Bratwurst (Germany)".into(), + "Poutine (Canada)".into(), + "Chicken Tikka Masala (India)".into(), + "Feijoada (Brazil)".into(), + "Kimchi (Korea)".into(), + "Borscht (Ukraine)".into(), + "Falafel (Middle East)".into(), + "Baklava (Turkey)".into(), + "Shepherd's Pie (Ireland)".into(), + "Rendang (Indonesia)".into(), + "Kebab (Middle East)".into(), + "Ceviche (Peru)".into(), + "Pierogi (Poland)".into(), + "Churrasco (Brazil)".into(), + "Moussaka (Greece)".into(), + "Lasagna (Italy)".into(), + "Pad Thai (Thailand)".into(), + "Pho (Vietnam)".into(), + ], + }) } } @@ -35,6 +66,9 @@ impl Render for PickerStory { type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - div().child(Picker::new("picker_story")) + div() + .text_color(red()) + .size_full() + .child(Picker::new("picker_story")) } } From 85000eba819e0fd375fed0e7cf6dfcd3c2ba888a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 6 Nov 2023 17:09:38 -0800 Subject: [PATCH 04/14] wip: picker co-authored-by: nathan co-authored-by: max --- Cargo.lock | 4 +- crates/gpui2/src/color.rs | 9 ++ crates/gpui2/src/elements/div.rs | 80 +++++----- crates/gpui2/src/elements/img.rs | 42 +++-- crates/gpui2/src/elements/list.rs | 122 +++++++++----- crates/gpui2/src/elements/svg.rs | 36 ++--- crates/gpui2/src/interactive.rs | 129 +++++++++------ crates/gpui2/src/style.rs | 4 +- crates/gpui2/src/text_system/line.rs | 2 - crates/gpui2/src/window.rs | 4 +- crates/menu2/Cargo.toml | 3 +- crates/menu2/src/menu2.rs | 18 +-- crates/picker2/src/picker2.rs | 79 ++++++--- crates/storybook2/Cargo.toml | 1 + crates/storybook2/src/stories/focus.rs | 42 ++--- crates/storybook2/src/stories/kitchen_sink.rs | 4 +- crates/storybook2/src/stories/picker.rs | 151 +++++++++++++----- crates/storybook2/src/stories/scroll.rs | 4 +- crates/ui2/src/components/checkbox.rs | 4 +- .../zed/src/languages/racket/highlights.scm | 1 - .../zed2/src/languages/racket/highlights.scm | 1 - 21 files changed, 461 insertions(+), 279 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98feaf9f61053e7d7ae701c3b72f7e3411b45111..50475da14372794944c77f4eabaae154d6596ddb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4970,7 +4970,8 @@ dependencies = [ name = "menu2" version = "0.1.0" dependencies = [ - "gpui2", + "serde", + "serde_derive", ] [[package]] @@ -8542,6 +8543,7 @@ dependencies = [ "gpui2", "itertools 0.11.0", "log", + "menu2", "picker2", "rust-embed", "serde", diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index db072594760f160a303020af20021e05d74db300..d5ff1603214af5007a11dbf05e6e3c49d68f5880 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -194,6 +194,15 @@ pub fn red() -> Hsla { } } +pub fn blue() -> Hsla { + Hsla { + h: 0.6, + s: 1., + l: 0.5, + a: 1., + } +} + impl Hsla { /// Returns true if the HSLA color is fully transparent, false otherwise. pub fn is_transparent(&self) -> bool { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index e011041bae2259f8a290ef098b061740721f4307..77d066729e59fed3930f759e502a2bb2d42a4021 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,28 +1,28 @@ use crate::{ point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId, - ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, + ElementInteractivity, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement, - Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, - StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, Visibility, + Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, + StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, Visibility, }; use refineable::Refineable; use smallvec::SmallVec; pub struct Div< V: 'static, - I: ElementInteraction = StatelessInteraction, + I: ElementInteractivity = StatelessInteractivity, F: ElementFocus = FocusDisabled, > { - interaction: I, + interactivity: I, focus: F, children: SmallVec<[AnyElement; 2]>, group: Option, base_style: StyleRefinement, } -pub fn div() -> Div, FocusDisabled> { +pub fn div() -> Div, FocusDisabled> { Div { - interaction: StatelessInteraction::default(), + interactivity: StatelessInteractivity::default(), focus: FocusDisabled, children: SmallVec::new(), group: None, @@ -30,14 +30,14 @@ pub fn div() -> Div, FocusDisabled> { } } -impl Div, F> +impl Div, F> where V: 'static, F: ElementFocus, { - pub fn id(self, id: impl Into) -> Div, F> { + pub fn id(self, id: impl Into) -> Div, F> { Div { - interaction: id.into().into(), + interactivity: id.into().into(), focus: self.focus, children: self.children, group: self.group, @@ -48,7 +48,7 @@ where impl Div where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { pub fn group(mut self, group: impl Into) -> Self { @@ -98,16 +98,20 @@ where let mut computed_style = Style::default(); computed_style.refine(&self.base_style); self.focus.refine_style(&mut computed_style, cx); - self.interaction - .refine_style(&mut computed_style, bounds, &element_state.interactive, cx); + self.interactivity.refine_style( + &mut computed_style, + bounds, + &element_state.interactive, + cx, + ); computed_style } } -impl Div, FocusDisabled> { - pub fn focusable(self) -> Div, FocusEnabled> { +impl Div, FocusDisabled> { + pub fn focusable(self) -> Div, FocusEnabled> { Div { - interaction: self.interaction, + interactivity: self.interactivity, focus: FocusEnabled::new(), children: self.children, group: self.group, @@ -118,9 +122,9 @@ impl Div, FocusDisabled> { pub fn track_focus( self, handle: &FocusHandle, - ) -> Div, FocusEnabled> { + ) -> Div, FocusEnabled> { Div { - interaction: self.interaction, + interactivity: self.interactivity, focus: FocusEnabled::tracked(handle), children: self.children, group: self.group, @@ -145,13 +149,13 @@ impl Div, FocusDisabled> { } } -impl Div, FocusDisabled> { +impl Div, FocusDisabled> { pub fn track_focus( self, handle: &FocusHandle, - ) -> Div, FocusEnabled> { + ) -> Div, FocusEnabled> { Div { - interaction: self.interaction.into_stateful(handle), + interactivity: self.interactivity.into_stateful(handle), focus: handle.clone().into(), children: self.children, group: self.group, @@ -163,7 +167,7 @@ impl Div, FocusDisabled> { impl Focusable for Div> where V: 'static, - I: ElementInteraction, + I: ElementInteractivity, { fn focus_listeners(&mut self) -> &mut FocusListeners { &mut self.focus.focus_listeners @@ -191,13 +195,13 @@ pub struct DivState { impl Element for Div where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { type ElementState = DivState; fn id(&self) -> Option { - self.interaction + self.interactivity .as_stateful() .map(|identified| identified.id.clone()) } @@ -212,7 +216,7 @@ where self.focus .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| { element_state.focus_handle = focus_handle; - self.interaction.initialize(cx, |cx| { + self.interactivity.initialize(cx, |cx| { for child in &mut self.children { child.initialize(view_state, cx); } @@ -281,11 +285,11 @@ where (child_max - child_min).into() }; - cx.stack(z_index, |cx| { - cx.stack(0, |cx| { + cx.with_z_index(z_index, |cx| { + cx.with_z_index(0, |cx| { style.paint(bounds, cx); this.focus.paint(bounds, cx); - this.interaction.paint( + this.interactivity.paint( bounds, content_size, style.overflow, @@ -293,7 +297,7 @@ where cx, ); }); - cx.stack(1, |cx| { + cx.with_z_index(1, |cx| { style.apply_text_style(cx, |cx| { style.apply_overflow(bounds, cx, |cx| { let scroll_offset = element_state.interactive.scroll_offset(); @@ -316,7 +320,7 @@ where impl Component for Div where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn render(self) -> AnyElement { @@ -326,7 +330,7 @@ where impl ParentElement for Div where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { @@ -336,7 +340,7 @@ where impl Styled for Div where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn style(&mut self) -> &mut StyleRefinement { @@ -346,19 +350,19 @@ where impl StatelessInteractive for Div where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { - fn stateless_interaction(&mut self) -> &mut StatelessInteraction { - self.interaction.as_stateless_mut() + fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { + self.interactivity.as_stateless_mut() } } -impl StatefulInteractive for Div, F> +impl StatefulInteractive for Div, F> where F: ElementFocus, { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction { - &mut self.interaction + fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { + &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 637bcd78e626140a17bd39766303eb97e0ff9d32..638665d4141d4a19362b2812932048a8ad46c348 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,17 +1,15 @@ -use std::sync::Arc; - use crate::{ div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus, - ElementId, ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, - LayoutId, Pixels, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, - StatelessInteractive, StyleRefinement, Styled, ViewContext, + ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, + LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; use futures::FutureExt; use util::ResultExt; pub struct Img< V: 'static, - I: ElementInteraction = StatelessInteraction, + I: ElementInteractivity = StatelessInteractivity, F: ElementFocus = FocusDisabled, > { base: Div, @@ -19,7 +17,7 @@ pub struct Img< grayscale: bool, } -pub fn img() -> Img, FocusDisabled> { +pub fn img() -> Img, FocusDisabled> { Img { base: div(), uri: None, @@ -30,7 +28,7 @@ pub fn img() -> Img, FocusDisabled> { impl Img where V: 'static, - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { pub fn uri(mut self, uri: impl Into) -> Self { @@ -44,11 +42,11 @@ where } } -impl Img, F> +impl Img, F> where F: ElementFocus, { - pub fn id(self, id: impl Into) -> Img, F> { + pub fn id(self, id: impl Into) -> Img, F> { Img { base: self.base.id(id), uri: self.uri, @@ -59,7 +57,7 @@ where impl Component for Img where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn render(self) -> AnyElement { @@ -69,7 +67,7 @@ where impl Element for Img where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { type ElementState = DivState; @@ -103,7 +101,7 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - cx.stack(0, |cx| { + cx.with_z_index(0, |cx| { self.base.paint(bounds, view, element_state, cx); }); @@ -120,7 +118,7 @@ where .and_then(ResultExt::log_err) { let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.stack(1, |cx| { + cx.with_z_index(1, |cx| { cx.paint_image(bounds, corner_radii, data, self.grayscale) .log_err() }); @@ -138,7 +136,7 @@ where impl Styled for Img where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn style(&mut self) -> &mut StyleRefinement { @@ -148,27 +146,27 @@ where impl StatelessInteractive for Img where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { - fn stateless_interaction(&mut self) -> &mut StatelessInteraction { - self.base.stateless_interaction() + fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { + self.base.stateless_interactivity() } } -impl StatefulInteractive for Img, F> +impl StatefulInteractive for Img, F> where F: ElementFocus, { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction { - self.base.stateful_interaction() + fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { + self.base.stateful_interactivity() } } impl Focusable for Img> where V: 'static, - I: ElementInteraction, + I: ElementInteractivity, { fn focus_listeners(&mut self) -> &mut FocusListeners { self.base.focus_listeners() diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 7f15af66c4289afa197911e8730016b851465ee9..0719f2a92ee318f74ee25748e793c18aa2d6b5f2 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -1,15 +1,12 @@ -use std::{cmp, ops::Range}; - -use smallvec::SmallVec; - use crate::{ point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId, - LayoutId, Pixels, Size, StyleRefinement, Styled, ViewContext, + ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Size, StatefulInteractive, + StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, + ViewContext, }; - -// We want to support uniform and non-uniform height -// We need to make the ID mandatory, to replace the 'state' field -// Previous implementation measured the first element as early as possible +use smallvec::SmallVec; +use std::{cmp, ops::Range}; +use taffy::style::Overflow; pub fn list( id: Id, @@ -21,8 +18,9 @@ where V: 'static, C: Component, { + let id = id.into(); List { - id: id.into(), + id: id.clone(), style: Default::default(), item_count, render_items: Box::new(move |view, visible_range, cx| { @@ -31,10 +29,11 @@ where .map(|component| component.render()) .collect() }), + interactivity: id.into(), } } -pub struct List { +pub struct List { id: ElementId, style: StyleRefinement, item_count: usize, @@ -45,19 +44,12 @@ pub struct List { &'a mut ViewContext, ) -> SmallVec<[AnyElement; 64]>, >, + interactivity: StatefulInteractivity, } -// #[derive(Debug)] -// pub enum ScrollTarget { -// Show(usize), -// Center(usize), -// } - #[derive(Default)] pub struct ListState { - scroll_top: f32, - // todo - // scroll_to: Option, + interactive: InteractiveElementState, } impl Styled for List { @@ -111,30 +103,66 @@ impl Element for List { - point(border.right + padding.right, border.bottom + padding.bottom), ); - if self.item_count > 0 { - let item_height = self.measure_item_height(view_state, padded_bounds, cx); - let visible_item_count = (padded_bounds.size.height / item_height).ceil() as usize; - let visible_range = 0..cmp::min(visible_item_count, self.item_count); - - let mut items = (self.render_items)(view_state, visible_range, cx); - - dbg!(items.len(), self.item_count, visible_item_count); + cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + let content_size; + if self.item_count > 0 { + let item_height = self.measure_item_height(view_state, padded_bounds, cx); + let visible_item_count = + (padded_bounds.size.height / item_height).ceil() as usize + 1; + let scroll_offset = element_state + .interactive + .scroll_offset() + .map_or((0.0).into(), |offset| offset.y); + let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; + let visible_range = first_visible_element_ix + ..cmp::min( + first_visible_element_ix + visible_item_count, + self.item_count, + ); + + let mut items = (self.render_items)(view_state, visible_range.clone(), cx); + + content_size = Size { + width: padded_bounds.size.width, + height: item_height * self.item_count, + }; + + cx.with_z_index(1, |cx| { + for (item, ix) in items.iter_mut().zip(visible_range) { + item.initialize(view_state, cx); + + let layout_id = item.layout(view_state, cx); + cx.compute_layout( + layout_id, + Size { + width: AvailableSpace::Definite(bounds.size.width), + height: AvailableSpace::Definite(item_height), + }, + ); + let offset = + padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset); + cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx)) + } + }); + } else { + content_size = Size { + width: bounds.size.width, + height: px(0.), + }; + } - for (ix, item) in items.iter_mut().enumerate() { - item.initialize(view_state, cx); + let overflow = point(style.overflow.x, Overflow::Scroll); - let layout_id = item.layout(view_state, cx); - cx.compute_layout( - layout_id, - Size { - width: AvailableSpace::Definite(bounds.size.width), - height: AvailableSpace::Definite(item_height), - }, + cx.with_z_index(0, |cx| { + self.interactivity.paint( + bounds, + content_size, + overflow, + &mut element_state.interactive, + cx, ); - let offset = padded_bounds.origin + point(px(0.), item_height * ix); - cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx)) - } - } + }); + }) } } @@ -161,6 +189,18 @@ impl List { } } +impl StatelessInteractive for List { + fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { + self.interactivity.as_stateless_mut() + } +} + +impl StatefulInteractive for List { + fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { + &mut self.interactivity + } +} + impl Component for List { fn render(self) -> AnyElement { AnyElement::new(self) diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 7db4c5cf6db86d96bf154c9a4f243c630b0a3fe5..8e2ba9d8a1ba944f7cd82ca583e30e53de147c23 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,21 +1,21 @@ use crate::{ div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId, - ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, - SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction, - StatelessInteractive, StyleRefinement, Styled, ViewContext, + ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, + SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, + StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; use util::ResultExt; pub struct Svg< V: 'static, - I: ElementInteraction = StatelessInteraction, + I: ElementInteractivity = StatelessInteractivity, F: ElementFocus = FocusDisabled, > { base: Div, path: Option, } -pub fn svg() -> Svg, FocusDisabled> { +pub fn svg() -> Svg, FocusDisabled> { Svg { base: div(), path: None, @@ -24,7 +24,7 @@ pub fn svg() -> Svg, FocusDisabled> { impl Svg where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { pub fn path(mut self, path: impl Into) -> Self { @@ -33,11 +33,11 @@ where } } -impl Svg, F> +impl Svg, F> where F: ElementFocus, { - pub fn id(self, id: impl Into) -> Svg, F> { + pub fn id(self, id: impl Into) -> Svg, F> { Svg { base: self.base.id(id), path: self.path, @@ -47,7 +47,7 @@ where impl Component for Svg where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn render(self) -> AnyElement { @@ -57,7 +57,7 @@ where impl Element for Svg where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { type ElementState = DivState; @@ -107,7 +107,7 @@ where impl Styled for Svg where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { fn style(&mut self) -> &mut StyleRefinement { @@ -117,27 +117,27 @@ where impl StatelessInteractive for Svg where - I: ElementInteraction, + I: ElementInteractivity, F: ElementFocus, { - fn stateless_interaction(&mut self) -> &mut StatelessInteraction { - self.base.stateless_interaction() + fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { + self.base.stateless_interactivity() } } -impl StatefulInteractive for Svg, F> +impl StatefulInteractive for Svg, F> where V: 'static, F: ElementFocus, { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction { - self.base.stateful_interaction() + fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { + self.base.stateful_interactivity() } } impl Focusable for Svg> where - I: ElementInteraction, + I: ElementInteractivity, { fn focus_listeners(&mut self) -> &mut FocusListeners { self.base.focus_listeners() diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index da208b38131f8ddd0e297c47bc504065af232d15..9d0b896673f27a07531121ca029fdc9a451bc14d 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -25,13 +25,13 @@ const TOOLTIP_DELAY: Duration = Duration::from_millis(500); const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); pub trait StatelessInteractive: Element { - fn stateless_interaction(&mut self) -> &mut StatelessInteraction; + fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity; fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, { - self.stateless_interaction().hover_style = f(StyleRefinement::default()); + self.stateless_interactivity().hover_style = f(StyleRefinement::default()); self } @@ -43,7 +43,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction().group_hover_style = Some(GroupStyle { + self.stateless_interactivity().group_hover_style = Some(GroupStyle { group: group_name.into(), style: f(StyleRefinement::default()), }); @@ -58,7 +58,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .mouse_down_listeners .push(Box::new(move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble @@ -79,7 +79,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .mouse_up_listeners .push(Box::new(move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble @@ -100,7 +100,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .mouse_down_listeners .push(Box::new(move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Capture @@ -121,7 +121,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .mouse_up_listeners .push(Box::new(move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Capture @@ -141,7 +141,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .mouse_move_listeners .push(Box::new(move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { @@ -158,7 +158,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .scroll_wheel_listeners .push(Box::new(move |view, event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { @@ -174,23 +174,48 @@ pub trait StatelessInteractive: Element { C: TryInto, C::Error: Debug, { - self.stateless_interaction().dispatch_context = + self.stateless_interactivity().dispatch_context = context.try_into().expect("invalid dispatch context"); self } + /// Capture the given action, fires during the capture phase + fn capture_action( + mut self, + listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.stateless_interactivity().key_listeners.push(( + TypeId::of::(), + Box::new(move |view, event, _, phase, cx| { + let event = event.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + listener(view, event, cx) + } + None + }), + )); + self + } + + /// Add a listener for the given action, fires during the bubble event phase fn on_action( mut self, - listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext) + 'static, + listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self where Self: Sized, { - self.stateless_interaction().key_listeners.push(( + self.stateless_interactivity().key_listeners.push(( TypeId::of::(), Box::new(move |view, event, _, phase, cx| { let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx); + if phase == DispatchPhase::Bubble { + listener(view, event, cx) + } + None }), )); @@ -204,7 +229,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction().key_listeners.push(( + self.stateless_interactivity().key_listeners.push(( TypeId::of::(), Box::new(move |view, event, _, phase, cx| { let event = event.downcast_ref().unwrap(); @@ -222,7 +247,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction().key_listeners.push(( + self.stateless_interactivity().key_listeners.push(( TypeId::of::(), Box::new(move |view, event, _, phase, cx| { let event = event.downcast_ref().unwrap(); @@ -237,7 +262,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction() + self.stateless_interactivity() .drag_over_styles .push((TypeId::of::(), f(StyleRefinement::default()))); self @@ -251,7 +276,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction().group_drag_over_styles.push(( + self.stateless_interactivity().group_drag_over_styles.push(( TypeId::of::(), GroupStyle { group: group_name.into(), @@ -268,7 +293,7 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interaction().drop_listeners.push(( + self.stateless_interactivity().drop_listeners.push(( TypeId::of::(), Box::new(move |view, dragged_view, cx| { listener(view, dragged_view.downcast().unwrap(), cx); @@ -279,13 +304,13 @@ pub trait StatelessInteractive: Element { } pub trait StatefulInteractive: StatelessInteractive { - fn stateful_interaction(&mut self) -> &mut StatefulInteraction; + fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity; fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, { - self.stateful_interaction().active_style = f(StyleRefinement::default()); + self.stateful_interactivity().active_style = f(StyleRefinement::default()); self } @@ -297,7 +322,7 @@ pub trait StatefulInteractive: StatelessInteractive { where Self: Sized, { - self.stateful_interaction().group_active_style = Some(GroupStyle { + self.stateful_interactivity().group_active_style = Some(GroupStyle { group: group_name.into(), style: f(StyleRefinement::default()), }); @@ -311,7 +336,7 @@ pub trait StatefulInteractive: StatelessInteractive { where Self: Sized, { - self.stateful_interaction() + self.stateful_interactivity() .click_listeners .push(Box::new(move |view, event, cx| listener(view, event, cx))); self @@ -326,10 +351,10 @@ pub trait StatefulInteractive: StatelessInteractive { W: 'static + Render, { debug_assert!( - self.stateful_interaction().drag_listener.is_none(), + self.stateful_interactivity().drag_listener.is_none(), "calling on_drag more than once on the same element is not supported" ); - self.stateful_interaction().drag_listener = + self.stateful_interactivity().drag_listener = Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { view: listener(view_state, cx).into(), cursor_offset, @@ -342,10 +367,10 @@ pub trait StatefulInteractive: StatelessInteractive { Self: Sized, { debug_assert!( - self.stateful_interaction().hover_listener.is_none(), + self.stateful_interactivity().hover_listener.is_none(), "calling on_hover more than once on the same element is not supported" ); - self.stateful_interaction().hover_listener = Some(Box::new(listener)); + self.stateful_interactivity().hover_listener = Some(Box::new(listener)); self } @@ -358,10 +383,10 @@ pub trait StatefulInteractive: StatelessInteractive { W: 'static + Render, { debug_assert!( - self.stateful_interaction().tooltip_builder.is_none(), + self.stateful_interactivity().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); - self.stateful_interaction().tooltip_builder = Some(Arc::new(move |view_state, cx| { + self.stateful_interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| { build_tooltip(view_state, cx).into() })); @@ -369,11 +394,11 @@ pub trait StatefulInteractive: StatelessInteractive { } } -pub trait ElementInteraction: 'static { - fn as_stateless(&self) -> &StatelessInteraction; - fn as_stateless_mut(&mut self) -> &mut StatelessInteraction; - fn as_stateful(&self) -> Option<&StatefulInteraction>; - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction>; +pub trait ElementInteractivity: 'static { + fn as_stateless(&self) -> &StatelessInteractivity; + fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity; + fn as_stateful(&self) -> Option<&StatefulInteractivity>; + fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity>; fn initialize( &mut self, @@ -735,11 +760,11 @@ pub trait ElementInteraction: 'static { } #[derive(Deref, DerefMut)] -pub struct StatefulInteraction { +pub struct StatefulInteractivity { pub id: ElementId, #[deref] #[deref_mut] - stateless: StatelessInteraction, + stateless: StatelessInteractivity, click_listeners: SmallVec<[ClickListener; 2]>, active_style: StyleRefinement, group_active_style: Option, @@ -748,29 +773,29 @@ pub struct StatefulInteraction { tooltip_builder: Option>, } -impl ElementInteraction for StatefulInteraction { - fn as_stateful(&self) -> Option<&StatefulInteraction> { +impl ElementInteractivity for StatefulInteractivity { + fn as_stateful(&self) -> Option<&StatefulInteractivity> { Some(self) } - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction> { + fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity> { Some(self) } - fn as_stateless(&self) -> &StatelessInteraction { + fn as_stateless(&self) -> &StatelessInteractivity { &self.stateless } - fn as_stateless_mut(&mut self) -> &mut StatelessInteraction { + fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity { &mut self.stateless } } -impl From for StatefulInteraction { +impl From for StatefulInteractivity { fn from(id: ElementId) -> Self { Self { id, - stateless: StatelessInteraction::default(), + stateless: StatelessInteractivity::default(), click_listeners: SmallVec::new(), drag_listener: None, hover_listener: None, @@ -783,7 +808,7 @@ impl From for StatefulInteraction { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; -pub struct StatelessInteraction { +pub struct StatelessInteractivity { pub dispatch_context: DispatchContext, pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, @@ -797,9 +822,9 @@ pub struct StatelessInteraction { drop_listeners: SmallVec<[(TypeId, Box>); 2]>, } -impl StatelessInteraction { - pub fn into_stateful(self, id: impl Into) -> StatefulInteraction { - StatefulInteraction { +impl StatelessInteractivity { + pub fn into_stateful(self, id: impl Into) -> StatefulInteractivity { + StatefulInteractivity { id: id.into(), stateless: self, click_listeners: SmallVec::new(), @@ -877,7 +902,7 @@ impl InteractiveElementState { } } -impl Default for StatelessInteraction { +impl Default for StatelessInteractivity { fn default() -> Self { Self { dispatch_context: DispatchContext::default(), @@ -895,20 +920,20 @@ impl Default for StatelessInteraction { } } -impl ElementInteraction for StatelessInteraction { - fn as_stateful(&self) -> Option<&StatefulInteraction> { +impl ElementInteractivity for StatelessInteractivity { + fn as_stateful(&self) -> Option<&StatefulInteractivity> { None } - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction> { + fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity> { None } - fn as_stateless(&self) -> &StatelessInteraction { + fn as_stateless(&self) -> &StatelessInteractivity { self } - fn as_stateless_mut(&mut self) -> &mut StatelessInteraction { + fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity { self } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index d2571a3253522cbfed12ffce1d130d762ac59a5a..0fe3660d7c560c85c5fdecc5d49eef467c916047 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -277,7 +277,7 @@ impl Style { pub fn paint(&self, bounds: Bounds, cx: &mut ViewContext) { let rem_size = cx.rem_size(); - cx.stack(0, |cx| { + cx.with_z_index(0, |cx| { cx.paint_shadows( bounds, self.corner_radii.to_pixels(bounds.size, rem_size), @@ -287,7 +287,7 @@ impl Style { let background_color = self.background.as_ref().and_then(Fill::color); if background_color.is_some() || self.is_border_visible() { - cx.stack(1, |cx| { + cx.with_z_index(1, |cx| { cx.paint_quad( bounds, self.corner_radii.to_pixels(bounds.size, rem_size), diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 21a7dcea6fcc9ec1f20f1b6d066bb24ecf9ae8da..29956b70f26c14b7888d323daa30189ab00621f5 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -142,8 +142,6 @@ impl Line { color, )?; } - } else { - dbg!(content_mask.bounds, max_glyph_bounds); } } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 7c05018aa1aaca1fb64461cab20989827ac81161..8811c845a6c94eddd3596d25cef1f7a27d9b6385 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1698,8 +1698,8 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self.window_cx } - pub fn stack(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R { - self.window.z_index_stack.push(order); + pub fn with_z_index(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { + self.window.z_index_stack.push(z_index); let result = f(self); self.window.z_index_stack.pop(); result diff --git a/crates/menu2/Cargo.toml b/crates/menu2/Cargo.toml index 9bf61db82c6c01d30c12e9c01844c3db464df40f..5fc33ddb11d41416c4b64aa15d7e3d1564952f94 100644 --- a/crates/menu2/Cargo.toml +++ b/crates/menu2/Cargo.toml @@ -9,4 +9,5 @@ path = "src/menu2.rs" doctest = false [dependencies] -gpui = { package = "gpui2", path = "../gpui2" } +serde.workspace = true +serde_derive.workspace = true diff --git a/crates/menu2/src/menu2.rs b/crates/menu2/src/menu2.rs index decd4aca22be0bda9c9c5ceae87346213e9d4392..da21bdcd2282cd4834f42eb67839553ea5c6ae91 100644 --- a/crates/menu2/src/menu2.rs +++ b/crates/menu2/src/menu2.rs @@ -1,25 +1,25 @@ -// todo!(use actions! macro) +use serde_derive::Deserialize; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct Cancel; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct Confirm; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SecondaryConfirm; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SelectPrev; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SelectNext; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SelectFirst; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SelectLast; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct ShowContextMenu; diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 3bcf5b92037a48e3e99fa372c442820bf084d000..26ca810b0c9b6fa7d8b2f966bd842f7131592ccb 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -18,11 +18,11 @@ // Dismiss, // } -use std::ops::Range; +use std::cmp; use gpui::{ - div, list, red, AppContext, Component, Div, Element, ElementId, ParentElement, Render, Styled, - ViewContext, + div, list, Component, ElementId, FocusHandle, Focusable, ParentElement, StatelessInteractive, + Styled, ViewContext, }; // pub struct Picker { @@ -43,8 +43,9 @@ pub trait PickerDelegate: Sized + 'static { fn match_count(&self, picker_id: ElementId) -> usize; - // fn selected_index(&self) -> usize; - // fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); + fn selected_index(&self, picker_id: ElementId) -> usize; + fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext); + // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; // fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); // fn dismissed(&mut self, cx: &mut ViewContext>); @@ -53,8 +54,6 @@ pub trait PickerDelegate: Sized + 'static { fn render_match( &self, ix: usize, - active: bool, - hovered: bool, selected: bool, picker_id: ElementId, cx: &mut ViewContext, @@ -84,32 +83,72 @@ pub trait PickerDelegate: Sized + 'static { #[derive(Component)] pub struct Picker { id: ElementId, + focus_handle: FocusHandle, phantom: std::marker::PhantomData, } impl Picker { - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into, focus_handle: FocusHandle) -> Self { Self { id: id.into(), + focus_handle, phantom: std::marker::PhantomData, } } } impl Picker { - pub fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { - div().size_full().id(self.id.clone()).child( - list( - "candidates", - view.match_count(self.id.clone()), - move |this: &mut V, visible_range, cx| { - visible_range - .map(|ix| this.render_match(ix, false, false, false, self.id.clone(), cx)) - .collect() - }, + pub fn render(self, view: &mut V, _cx: &mut ViewContext) -> impl Component { + let id = self.id.clone(); + div() + .size_full() + .id(self.id.clone()) + .track_focus(&self.focus_handle) + .context("picker") + .on_focus(|v, e, cx| { + dbg!("FOCUSED!"); + }) + .on_blur(|v, e, cx| { + dbg!("BLURRED!"); + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::SelectNext, cx| { + let index = view.selected_index(id.clone()); + let count = view.match_count(id.clone()); + if count > 0 { + view.set_selected_index(cmp::min(index + 1, count - 1), id.clone(), cx); + } + } + }) + .on_action({ + let id = id.clone(); + move |view, _: &menu::SelectPrev, cx| { + let index = view.selected_index(id.clone()); + let count = view.match_count(id.clone()); + if count > 0 { + view.set_selected_index((index + 1) % count, id.clone(), cx); + } + } + }) + .on_action(|view, _: &menu::SelectFirst, cx| {}) + .on_action(|view, _: &menu::SelectLast, cx| {}) + .on_action(|view, _: &menu::Cancel, cx| {}) + .on_action(|view, _: &menu::Confirm, cx| {}) + .on_action(|view, _: &menu::SecondaryConfirm, cx| {}) + .child( + list( + "candidates", + view.match_count(self.id.clone()), + move |view: &mut V, visible_range, cx| { + let selected_ix = view.selected_index(self.id.clone()); + visible_range + .map(|ix| view.render_match(ix, ix == selected_ix, self.id.clone(), cx)) + .collect() + }, + ) + .size_full(), ) - .size_full(), - ) } } diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 0e35a7a66f3f892aec1df787456ce9e9d00acfb1..7a59dda6dfe5d351b89981560c4cc4781be4ae7e 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -25,6 +25,7 @@ smallvec.workspace = true strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } theme2 = { path = "../theme2" } +menu = { package = "menu2", path = "../menu2" } ui = { package = "ui2", path = "../ui2", features = ["stories"] } util = { path = "../util" } picker = { package = "picker2", path = "../picker2" } diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 16c03f87d5421af9d9336e462692102c4cb632bb..bfd7d5fa1c25496f402c9fcc1b1e7e46ce1a3f56 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,5 +1,5 @@ use gpui::{ - div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, StatefulInteraction, + div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext, }; use serde::Deserialize; @@ -31,7 +31,7 @@ impl FocusStory { } impl Render for FocusStory { - type Element = Div, FocusEnabled>; + type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -48,20 +48,18 @@ impl Render for FocusStory { .id("parent") .focusable() .context("parent") - .on_action(|_, action: &ActionA, phase, cx| { - println!("Action A dispatched on parent during {:?}", phase); + .on_action(|_, action: &ActionA, cx| { + println!("Action A dispatched on parent during"); }) - .on_action(|_, action: &ActionB, phase, cx| { - println!("Action B dispatched on parent during {:?}", phase); + .on_action(|_, action: &ActionB, cx| { + println!("Action B dispatched on parent during"); }) .on_focus(|_, _, _| println!("Parent focused")) .on_blur(|_, _, _| println!("Parent blurred")) .on_focus_in(|_, _, _| println!("Parent focus_in")) .on_focus_out(|_, _, _| println!("Parent focus_out")) - .on_key_down(|_, event, phase, _| { - println!("Key down on parent {:?} {:?}", phase, event) - }) - .on_key_up(|_, event, phase, _| println!("Key up on parent {:?} {:?}", phase, event)) + .on_key_down(|_, event, phase, _| println!("Key down on parent {:?}", event)) + .on_key_up(|_, event, phase, _| println!("Key up on parent {:?}", event)) .size_full() .bg(color_1) .focus(|style| style.bg(color_2)) @@ -70,8 +68,8 @@ impl Render for FocusStory { div() .track_focus(&child_1) .context("child-1") - .on_action(|_, action: &ActionB, phase, cx| { - println!("Action B dispatched on child 1 during {:?}", phase); + .on_action(|_, action: &ActionB, cx| { + println!("Action B dispatched on child 1 during"); }) .w_full() .h_6() @@ -82,20 +80,16 @@ impl Render for FocusStory { .on_blur(|_, _, _| println!("Child 1 blurred")) .on_focus_in(|_, _, _| println!("Child 1 focus_in")) .on_focus_out(|_, _, _| println!("Child 1 focus_out")) - .on_key_down(|_, event, phase, _| { - println!("Key down on child 1 {:?} {:?}", phase, event) - }) - .on_key_up(|_, event, phase, _| { - println!("Key up on child 1 {:?} {:?}", phase, event) - }) + .on_key_down(|_, event, phase, _| println!("Key down on child 1 {:?}", event)) + .on_key_up(|_, event, phase, _| println!("Key up on child 1 {:?}", event)) .child("Child 1"), ) .child( div() .track_focus(&child_2) .context("child-2") - .on_action(|_, action: &ActionC, phase, cx| { - println!("Action C dispatched on child 2 during {:?}", phase); + .on_action(|_, action: &ActionC, cx| { + println!("Action C dispatched on child 2 during"); }) .w_full() .h_6() @@ -104,12 +98,8 @@ impl Render for FocusStory { .on_blur(|_, _, _| println!("Child 2 blurred")) .on_focus_in(|_, _, _| println!("Child 2 focus_in")) .on_focus_out(|_, _, _| println!("Child 2 focus_out")) - .on_key_down(|_, event, phase, _| { - println!("Key down on child 2 {:?} {:?}", phase, event) - }) - .on_key_up(|_, event, phase, _| { - println!("Key up on child 2 {:?} {:?}", phase, event) - }) + .on_key_down(|_, event, phase, _| println!("Key down on child 2 {:?}", event)) + .on_key_up(|_, event, phase, _| println!("Key up on child 2 {:?}", event)) .child("Child 2"), ) } diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index 54d6f2a3a967cc3f6e5a29be97e36291c45bdeff..6831ae27220ca4691f5f8ac371c0f43e6dd7b013 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -1,5 +1,5 @@ use crate::{story::Story, story_selector::ComponentStory}; -use gpui::{Div, Render, StatefulInteraction, View, VisualContext}; +use gpui::{Div, Render, StatefulInteractivity, View, VisualContext}; use strum::IntoEnumIterator; use ui::prelude::*; @@ -12,7 +12,7 @@ impl KitchenSinkStory { } impl Render for KitchenSinkStory { - type Element = Div>; + type Element = Div>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let component_stories = ComponentStory::iter() diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 23d13b83fc80a53b8789bfd0fe73dd3af9d9e07f..13c3957979737f27c0eacfda71e5ecd41ae681e6 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,15 +1,18 @@ use gpui::{ - black, div, red, Div, Fill, ParentElement, Render, SharedString, Styled, View, VisualContext, - WindowContext, + div, Component, Div, FocusHandle, KeyBinding, ParentElement, Render, SharedString, + StatelessInteractive, Styled, View, VisualContext, WindowContext, }; use picker::{Picker, PickerDelegate}; +use theme2::ActiveTheme; pub struct PickerStory { + selected_ix: usize, candidates: Vec, + focus_handle: FocusHandle, } impl PickerDelegate for PickerStory { - type ListItem = SharedString; + type ListItem = Div; fn match_count(&self, _picker_id: gpui::ElementId) -> usize { self.candidates.len() @@ -18,46 +21,118 @@ impl PickerDelegate for PickerStory { fn render_match( &self, ix: usize, - _active: bool, - _hovered: bool, - _selected: bool, + selected: bool, _picker_id: gpui::ElementId, cx: &mut gpui::ViewContext, ) -> Self::ListItem { - self.candidates[ix].clone() + let colors = cx.theme().colors(); + + div() + .text_color(colors.text) + .when(selected, |s| { + s.border_l_10().border_color(colors.terminal_ansi_yellow) + }) + .hover(|style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .child(self.candidates[ix].clone()) + } + + fn selected_index(&self, picker_id: gpui::ElementId) -> usize { + self.selected_ix + } + + fn set_selected_index( + &mut self, + ix: usize, + _picker_id: gpui::ElementId, + _cx: &mut gpui::ViewContext, + ) { + self.selected_ix = ix; } } impl PickerStory { pub fn new(cx: &mut WindowContext) -> View { - cx.build_view(|cx| PickerStory { - candidates: vec![ - "Pizza (Italy)".into(), - "Sushi (Japan)".into(), - "Paella (Spain)".into(), - "Tacos (Mexico)".into(), - "Peking Duck (China)".into(), - "Fish and Chips (UK)".into(), - "Croissant (France)".into(), - "Bratwurst (Germany)".into(), - "Poutine (Canada)".into(), - "Chicken Tikka Masala (India)".into(), - "Feijoada (Brazil)".into(), - "Kimchi (Korea)".into(), - "Borscht (Ukraine)".into(), - "Falafel (Middle East)".into(), - "Baklava (Turkey)".into(), - "Shepherd's Pie (Ireland)".into(), - "Rendang (Indonesia)".into(), - "Kebab (Middle East)".into(), - "Ceviche (Peru)".into(), - "Pierogi (Poland)".into(), - "Churrasco (Brazil)".into(), - "Moussaka (Greece)".into(), - "Lasagna (Italy)".into(), - "Pad Thai (Thailand)".into(), - "Pho (Vietnam)".into(), - ], + cx.build_view(|cx| { + cx.bind_keys([ + KeyBinding::new("up", menu::SelectPrev, Some("picker")), + KeyBinding::new("pageup", menu::SelectFirst, Some("picker")), + KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")), + KeyBinding::new("ctrl-p", menu::SelectPrev, Some("picker")), + KeyBinding::new("down", menu::SelectNext, Some("picker")), + KeyBinding::new("pagedown", menu::SelectLast, Some("picker")), + KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")), + KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")), + KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")), + KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")), + KeyBinding::new("enter", menu::Confirm, Some("picker")), + KeyBinding::new("ctrl-enter", menu::ShowContextMenu, Some("picker")), + KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")), + KeyBinding::new("escape", menu::Cancel, Some("picker")), + KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")), + ]); + + let fh = cx.focus_handle(); + cx.focus(&fh); + + PickerStory { + focus_handle: fh, + candidates: vec![ + "Baguette (France)".into(), + "Baklava (Turkey)".into(), + "Beef Wellington (UK)".into(), + "Biryani (India)".into(), + "Borscht (Ukraine)".into(), + "Bratwurst (Germany)".into(), + "Bulgogi (Korea)".into(), + "Burrito (USA)".into(), + "Ceviche (Peru)".into(), + "Chicken Tikka Masala (India)".into(), + "Churrasco (Brazil)".into(), + "Couscous (North Africa)".into(), + "Croissant (France)".into(), + "Dim Sum (China)".into(), + "Empanada (Argentina)".into(), + "Fajitas (Mexico)".into(), + "Falafel (Middle East)".into(), + "Feijoada (Brazil)".into(), + "Fish and Chips (UK)".into(), + "Fondue (Switzerland)".into(), + "Goulash (Hungary)".into(), + "Haggis (Scotland)".into(), + "Kebab (Middle East)".into(), + "Kimchi (Korea)".into(), + "Lasagna (Italy)".into(), + "Maple Syrup Pancakes (Canada)".into(), + "Moussaka (Greece)".into(), + "Pad Thai (Thailand)".into(), + "Paella (Spain)".into(), + "Pancakes (USA)".into(), + "Pasta Carbonara (Italy)".into(), + "Pavlova (Australia)".into(), + "Peking Duck (China)".into(), + "Pho (Vietnam)".into(), + "Pierogi (Poland)".into(), + "Pizza (Italy)".into(), + "Poutine (Canada)".into(), + "Pretzel (Germany)".into(), + "Ramen (Japan)".into(), + "Rendang (Indonesia)".into(), + "Sashimi (Japan)".into(), + "Satay (Indonesia)".into(), + "Shepherd's Pie (Ireland)".into(), + "Sushi (Japan)".into(), + "Tacos (Mexico)".into(), + "Tandoori Chicken (India)".into(), + "Tortilla (Spain)".into(), + "Tzatziki (Greece)".into(), + "Wiener Schnitzel (Austria)".into(), + ], + selected_ix: 0, + } }) } } @@ -66,9 +141,11 @@ impl Render for PickerStory { type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + let theme = cx.theme(); + div() - .text_color(red()) + .bg(theme.styles.colors.background) .size_full() - .child(Picker::new("picker_story")) + .child(Picker::new("picker_story", self.focus_handle.clone())) } } diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index cdb48603e093f62e4ef5ee5968af068798e3bce9..31928bb9e40343d61c1bba0e9d34259f7853cc8f 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,5 +1,5 @@ use gpui::{ - div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteraction, Styled, + div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteractivity, Styled, View, VisualContext, WindowContext, }; use theme2::ActiveTheme; @@ -13,7 +13,7 @@ impl ScrollStory { } impl Render for ScrollStory { - type Element = Div>; + type Element = Div>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 9f7c10a10433eef59249e3ab0df408b94a8a2aff..20dad747124ca2a652679324f04590dbf9d3f05d 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -128,7 +128,7 @@ impl Checkbox { // click area for the checkbox. .size_5() // Because we've enlarged the click area, we need to create a - // `group` to pass down interaction events to the checkbox. + // `group` to pass down interactivity events to the checkbox. .group(group_id.clone()) .child( div() @@ -148,7 +148,7 @@ impl Checkbox { .bg(bg_color) .border() .border_color(border_color) - // We only want the interaction states to fire when we + // We only want the interactivity states to fire when we // are in a checkbox that isn't disabled. .when(!self.disabled, |this| { // Here instead of `hover()` we use `group_hover()` diff --git a/crates/zed/src/languages/racket/highlights.scm b/crates/zed/src/languages/racket/highlights.scm index 2c0caf89357cfbe8f966bffbbc712272b3c1e59d..3caf1d88e97dd0fcdc2e08d6ef866ad74c436a31 100644 --- a/crates/zed/src/languages/racket/highlights.scm +++ b/crates/zed/src/languages/racket/highlights.scm @@ -37,4 +37,3 @@ ((symbol) @comment (#match? @comment "^#[cC][iIsS]$")) - diff --git a/crates/zed2/src/languages/racket/highlights.scm b/crates/zed2/src/languages/racket/highlights.scm index 2c0caf89357cfbe8f966bffbbc712272b3c1e59d..3caf1d88e97dd0fcdc2e08d6ef866ad74c436a31 100644 --- a/crates/zed2/src/languages/racket/highlights.scm +++ b/crates/zed2/src/languages/racket/highlights.scm @@ -37,4 +37,3 @@ ((symbol) @comment (#match? @comment "^#[cC][iIsS]$")) - From 1d34b7b9fe45624dc1416ab65420b75562292db8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 09:46:41 -0800 Subject: [PATCH 05/14] Implement picker actions Co-authored-by: Mikayla --- crates/picker2/src/picker2.rs | 57 ++++++++++++++++++------- crates/storybook2/src/stories/picker.rs | 20 ++++++++- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 26ca810b0c9b6fa7d8b2f966bd842f7131592ccb..982d8360fb8f5300df57c7c50ad3fc8e4ffab5c8 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -42,13 +42,12 @@ pub trait PickerDelegate: Sized + 'static { // fn placeholder_text(&self) -> Arc; fn match_count(&self, picker_id: ElementId) -> usize; - fn selected_index(&self, picker_id: ElementId) -> usize; fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext); // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; - // fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); - // fn dismissed(&mut self, cx: &mut ViewContext>); + fn confirm(&mut self, secondary: bool, picker_id: ElementId, cx: &mut ViewContext); + fn dismissed(&mut self, picker_id: ElementId, cx: &mut ViewContext); // todo!("rename to render_candidate?") fn render_match( @@ -105,18 +104,18 @@ impl Picker { .id(self.id.clone()) .track_focus(&self.focus_handle) .context("picker") - .on_focus(|v, e, cx| { - dbg!("FOCUSED!"); + .on_focus(|_, _, _cx| { + eprintln!("picker focused"); }) - .on_blur(|v, e, cx| { - dbg!("BLURRED!"); + .on_blur(|_, _, _cx| { + eprintln!("picker blurred"); }) .on_action({ let id = id.clone(); move |view: &mut V, _: &menu::SelectNext, cx| { - let index = view.selected_index(id.clone()); let count = view.match_count(id.clone()); if count > 0 { + let index = view.selected_index(id.clone()); view.set_selected_index(cmp::min(index + 1, count - 1), id.clone(), cx); } } @@ -124,18 +123,46 @@ impl Picker { .on_action({ let id = id.clone(); move |view, _: &menu::SelectPrev, cx| { - let index = view.selected_index(id.clone()); let count = view.match_count(id.clone()); if count > 0 { - view.set_selected_index((index + 1) % count, id.clone(), cx); + let index = view.selected_index(id.clone()); + view.set_selected_index(index.saturating_sub(1), id.clone(), cx); + } + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::SelectFirst, cx| { + view.set_selected_index(0, id.clone(), cx); + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::SelectLast, cx| { + let count = view.match_count(id.clone()); + if count > 0 { + view.set_selected_index(count - 1, id.clone(), cx); } } }) - .on_action(|view, _: &menu::SelectFirst, cx| {}) - .on_action(|view, _: &menu::SelectLast, cx| {}) - .on_action(|view, _: &menu::Cancel, cx| {}) - .on_action(|view, _: &menu::Confirm, cx| {}) - .on_action(|view, _: &menu::SecondaryConfirm, cx| {}) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::Cancel, cx| { + view.dismissed(id.clone(), cx); + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::Confirm, cx| { + view.confirm(false, id.clone(), cx); + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::SecondaryConfirm, cx| { + view.confirm(true, id.clone(), cx); + } + }) .child( list( "candidates", diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 13c3957979737f27c0eacfda71e5ecd41ae681e6..f5f2456d42bd2137081a6c6f8df262dee5e6f22a 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -48,9 +48,27 @@ impl PickerDelegate for PickerStory { &mut self, ix: usize, _picker_id: gpui::ElementId, - _cx: &mut gpui::ViewContext, + cx: &mut gpui::ViewContext, ) { self.selected_ix = ix; + cx.notify(); + } + + fn confirm( + &mut self, + secondary: bool, + picker_id: gpui::ElementId, + cx: &mut gpui::ViewContext, + ) { + if secondary { + eprintln!("Secondary confirmed {}", self.candidates[self.selected_ix]) + } else { + eprintln!("Confirmed {}", self.candidates[self.selected_ix]) + } + } + + fn dismissed(&mut self, picker_id: gpui::ElementId, cx: &mut gpui::ViewContext) { + cx.quit(); } } From 742180a3a8963635e396410e7282ad710ddae016 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 10:45:38 -0800 Subject: [PATCH 06/14] Implement list scroll tracking Co-authored-by: Mikayla --- crates/gpui2/src/elements/list.rs | 53 ++++++++++++++++++++++++++++--- crates/gpui2/src/interactive.rs | 6 ++++ crates/picker2/src/picker2.rs | 45 ++++++++++++-------------- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 0719f2a92ee318f74ee25748e793c18aa2d6b5f2..acd1a524f32f0c0e174d0f07640a99ce025cbc22 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -1,11 +1,12 @@ use crate::{ point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId, - ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Size, StatefulInteractive, - StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, - ViewContext, + ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, + StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, + StyleRefinement, Styled, ViewContext, }; +use parking_lot::Mutex; use smallvec::SmallVec; -use std::{cmp, ops::Range}; +use std::{cmp, ops::Range, sync::Arc}; use taffy::style::Overflow; pub fn list( @@ -30,6 +31,7 @@ where .collect() }), interactivity: id.into(), + scroll_handle: None, } } @@ -45,6 +47,37 @@ pub struct List { ) -> SmallVec<[AnyElement; 64]>, >, interactivity: StatefulInteractivity, + scroll_handle: Option, +} + +#[derive(Clone)] +pub struct ListScrollHandle(Arc>>); + +#[derive(Clone, Debug)] +struct ListScrollHandleState { + item_height: Pixels, + list_height: Pixels, + scroll_offset: Arc>>, +} + +impl ListScrollHandle { + pub fn new() -> Self { + Self(Arc::new(Mutex::new(None))) + } + + pub fn scroll_to_item(&self, ix: usize) { + if let Some(state) = &*self.0.lock() { + let mut scroll_offset = state.scroll_offset.lock(); + let item_top = state.item_height * ix; + let item_bottom = item_top + state.item_height; + let scroll_top = -scroll_offset.y; + if item_top < scroll_top { + scroll_offset.y = -item_top; + } else if item_bottom > scroll_top + state.list_height { + scroll_offset.y = -(item_bottom - state.list_height); + } + } + } } #[derive(Default)] @@ -107,6 +140,13 @@ impl Element for List { let content_size; if self.item_count > 0 { let item_height = self.measure_item_height(view_state, padded_bounds, cx); + if let Some(scroll_handle) = self.scroll_handle.clone() { + scroll_handle.0.lock().replace(ListScrollHandleState { + item_height, + list_height: padded_bounds.size.height, + scroll_offset: element_state.interactive.track_scroll_offset(), + }); + } let visible_item_count = (padded_bounds.size.height / item_height).ceil() as usize + 1; let scroll_offset = element_state @@ -187,6 +227,11 @@ impl List { ); cx.layout_bounds(layout_id).size.height } + + pub fn track_scroll(mut self, handle: ListScrollHandle) -> Self { + self.scroll_handle = Some(handle); + self + } } impl StatelessInteractive for List { diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 2fa3ceeac2ed916ae5d8da99a086a7a3a829d449..e38a158eef2af4ca385d1c9ba8f23d6ae156b857 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -900,6 +900,12 @@ impl InteractiveElementState { .as_ref() .map(|offset| offset.lock().clone()) } + + pub fn track_scroll_offset(&mut self) -> Arc>> { + self.scroll_offset + .get_or_insert_with(|| Arc::new(Mutex::new(Default::default()))) + .clone() + } } impl Default for StatelessInteractivity { diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 982d8360fb8f5300df57c7c50ad3fc8e4ffab5c8..dafe45e4e973e05d074a6970ffadfc210f1d16e2 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,28 +1,8 @@ -// use editor::Editor; -// use gpui::{ -// elements::*, -// geometry::vector::{vec2f, Vector2F}, -// keymap_matcher::KeymapContext, -// platform::{CursorStyle, MouseButton}, -// AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext, -// ViewHandle, -// }; -// use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -// use parking_lot::Mutex; -// use std::{cmp, sync::Arc}; -// use util::ResultExt; -// use workspace::Modal; - -// #[derive(Clone, Copy)] -// pub enum PickerEvent { -// Dismiss, -// } - use std::cmp; use gpui::{ - div, list, Component, ElementId, FocusHandle, Focusable, ParentElement, StatelessInteractive, - Styled, ViewContext, + div, list, Component, ElementId, FocusHandle, Focusable, ListScrollHandle, ParentElement, + StatelessInteractive, Styled, ViewContext, }; // pub struct Picker { @@ -99,6 +79,7 @@ impl Picker { impl Picker { pub fn render(self, view: &mut V, _cx: &mut ViewContext) -> impl Component { let id = self.id.clone(); + let scroll_handle = ListScrollHandle::new(); div() .size_full() .id(self.id.clone()) @@ -112,36 +93,49 @@ impl Picker { }) .on_action({ let id = id.clone(); + let scroll_handle = scroll_handle.clone(); move |view: &mut V, _: &menu::SelectNext, cx| { let count = view.match_count(id.clone()); if count > 0 { let index = view.selected_index(id.clone()); - view.set_selected_index(cmp::min(index + 1, count - 1), id.clone(), cx); + let ix = cmp::min(index + 1, count - 1); + view.set_selected_index(ix, id.clone(), cx); + scroll_handle.scroll_to_item(ix); } } }) .on_action({ let id = id.clone(); + let scroll_handle = scroll_handle.clone(); move |view, _: &menu::SelectPrev, cx| { let count = view.match_count(id.clone()); if count > 0 { let index = view.selected_index(id.clone()); - view.set_selected_index(index.saturating_sub(1), id.clone(), cx); + let ix = index.saturating_sub(1); + view.set_selected_index(ix, id.clone(), cx); + scroll_handle.scroll_to_item(ix); } } }) .on_action({ let id = id.clone(); + let scroll_handle = scroll_handle.clone(); move |view: &mut V, _: &menu::SelectFirst, cx| { - view.set_selected_index(0, id.clone(), cx); + let count = view.match_count(id.clone()); + if count > 0 { + view.set_selected_index(0, id.clone(), cx); + scroll_handle.scroll_to_item(0); + } } }) .on_action({ let id = id.clone(); + let scroll_handle = scroll_handle.clone(); move |view: &mut V, _: &menu::SelectLast, cx| { let count = view.match_count(id.clone()); if count > 0 { view.set_selected_index(count - 1, id.clone(), cx); + scroll_handle.scroll_to_item(count - 1); } } }) @@ -174,6 +168,7 @@ impl Picker { .collect() }, ) + .track_scroll(scroll_handle.clone()) .size_full(), ) } From 24128737191495d2f3c22aed132d1eb9fdf0bb4c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 10:53:59 -0800 Subject: [PATCH 07/14] Remove commented-out code in picker2 Co-authored-by: Mikayla --- crates/picker2/src/picker2.rs | 362 +----------------------- crates/storybook2/src/stories/picker.rs | 6 +- 2 files changed, 14 insertions(+), 354 deletions(-) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index dafe45e4e973e05d074a6970ffadfc210f1d16e2..8161c0d0a2546d94b82ac6f640e3185074744df6 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,31 +1,26 @@ -use std::cmp; - use gpui::{ - div, list, Component, ElementId, FocusHandle, Focusable, ListScrollHandle, ParentElement, + div, list, Component, ElementId, FocusHandle, ListScrollHandle, ParentElement, StatelessInteractive, Styled, ViewContext, }; +use std::cmp; -// pub struct Picker { -// delegate: D, -// query_editor: ViewHandle, -// list_state: UniformListState, -// max_size: Vector2F, -// theme: Arc theme::Picker>>>, -// confirmed: bool, -// pending_update_matches: Option>>, -// confirm_on_update: Option, -// has_focus: bool, -// } +#[derive(Component)] +pub struct Picker { + id: ElementId, + focus_handle: FocusHandle, + phantom: std::marker::PhantomData, +} pub trait PickerDelegate: Sized + 'static { type ListItem: Component; - // fn placeholder_text(&self) -> Arc; fn match_count(&self, picker_id: ElementId) -> usize; fn selected_index(&self, picker_id: ElementId) -> usize; fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext); - // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; + // fn placeholder_text(&self) -> Arc; + // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; + fn confirm(&mut self, secondary: bool, picker_id: ElementId, cx: &mut ViewContext); fn dismissed(&mut self, picker_id: ElementId, cx: &mut ViewContext); @@ -37,33 +32,6 @@ pub trait PickerDelegate: Sized + 'static { picker_id: ElementId, cx: &mut ViewContext, ) -> Self::ListItem; - - // fn center_selection_after_match_updates(&self) -> bool { - // false - // } - // fn render_header( - // &self, - // _cx: &mut ViewContext>, - // ) -> Option>> { - // None - // } - // fn render_footer( - // &self, - // _cx: &mut ViewContext>, - // ) -> Option>> { - // None - // } -} - -// impl Entity for Picker { -// type Event = PickerEvent; -// } - -#[derive(Component)] -pub struct Picker { - id: ElementId, - focus_handle: FocusHandle, - phantom: std::marker::PhantomData, } impl Picker { @@ -85,12 +53,6 @@ impl Picker { .id(self.id.clone()) .track_focus(&self.focus_handle) .context("picker") - .on_focus(|_, _, _cx| { - eprintln!("picker focused"); - }) - .on_blur(|_, _, _cx| { - eprintln!("picker blurred"); - }) .on_action({ let id = id.clone(); let scroll_handle = scroll_handle.clone(); @@ -173,305 +135,3 @@ impl Picker { ) } } - -// impl View for Picker { -// fn ui_name() -> &'static str { -// "Picker" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let theme = (self.theme.lock())(theme::current(cx).as_ref()); -// let query = self.query(cx); -// let match_count = self.delegate.match_count(); - -// let container_style; -// let editor_style; -// if query.is_empty() && match_count == 0 { -// container_style = theme.empty_container; -// editor_style = theme.empty_input_editor.container; -// } else { -// container_style = theme.container; -// editor_style = theme.input_editor.container; -// }; - -// Flex::new(Axis::Vertical) -// .with_child( -// ChildView::new(&self.query_editor, cx) -// .contained() -// .with_style(editor_style), -// ) -// .with_children(self.delegate.render_header(cx)) -// .with_children(if match_count == 0 { -// if query.is_empty() { -// None -// } else { -// Some( -// Label::new("No matches", theme.no_matches.label.clone()) -// .contained() -// .with_style(theme.no_matches.container) -// .into_any(), -// ) -// } -// } else { -// Some( -// UniformList::new( -// self.list_state.clone(), -// match_count, -// cx, -// move |this, mut range, items, cx| { -// let selected_ix = this.delegate.selected_index(); -// range.end = cmp::min(range.end, this.delegate.match_count()); -// items.extend(range.map(move |ix| { -// MouseEventHandler::new::(ix, cx, |state, cx| { -// this.delegate.render_match(ix, state, ix == selected_ix, cx) -// }) -// // Capture mouse events -// .on_down(MouseButton::Left, |_, _, _| {}) -// .on_up(MouseButton::Left, |_, _, _| {}) -// .on_click(MouseButton::Left, move |click, picker, cx| { -// picker.select_index(ix, click.cmd, cx); -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .into_any() -// })); -// }, -// ) -// .contained() -// .with_margin_top(6.0) -// .flex(1., false) -// .into_any(), -// ) -// }) -// .with_children(self.delegate.render_footer(cx)) -// .contained() -// .with_style(container_style) -// .constrained() -// .with_max_width(self.max_size.x()) -// .with_max_height(self.max_size.y()) -// .into_any_named("picker") -// } - -// fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { -// Self::reset_to_default_keymap_context(keymap); -// keymap.add_identifier("menu"); -// } - -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// self.has_focus = true; -// if cx.is_self_focused() { -// cx.focus(&self.query_editor); -// } -// } - -// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { -// self.has_focus = false; -// } -// } - -// impl Modal for Picker { -// fn has_focus(&self) -> bool { -// self.has_focus -// } - -// fn dismiss_on_event(event: &Self::Event) -> bool { -// matches!(event, PickerEvent::Dismiss) -// } -// } - -// impl Picker { -// pub fn init(cx: &mut AppContext) { -// cx.add_action(Self::select_first); -// cx.add_action(Self::select_last); -// cx.add_action(Self::select_next); -// cx.add_action(Self::select_prev); -// cx.add_action(Self::confirm); -// cx.add_action(Self::secondary_confirm); -// cx.add_action(Self::cancel); -// } - -// pub fn new(delegate: D, cx: &mut ViewContext) -> Self { -// let theme = Arc::new(Mutex::new( -// Box::new(|theme: &theme::Theme| theme.picker.clone()) -// as Box theme::Picker>, -// )); -// let placeholder_text = delegate.placeholder_text(); -// let query_editor = cx.add_view({ -// let picker_theme = theme.clone(); -// |cx| { -// let mut editor = Editor::single_line( -// Some(Arc::new(move |theme| { -// (picker_theme.lock())(theme).input_editor.clone() -// })), -// cx, -// ); -// editor.set_placeholder_text(placeholder_text, cx); -// editor -// } -// }); -// cx.subscribe(&query_editor, Self::on_query_editor_event) -// .detach(); -// let mut this = Self { -// query_editor, -// list_state: Default::default(), -// delegate, -// max_size: vec2f(540., 420.), -// theme, -// confirmed: false, -// pending_update_matches: None, -// confirm_on_update: None, -// has_focus: false, -// }; -// this.update_matches(String::new(), cx); -// this -// Self { delegate } -// } - -// pub fn with_max_size(mut self, width: f32, height: f32) -> Self { -// self.max_size = vec2f(width, height); -// self -// } - -// pub fn with_theme(self, theme: F) -> Self -// where -// F: 'static + Fn(&theme::Theme) -> theme::Picker, -// { -// *self.theme.lock() = Box::new(theme); -// self -// } - -// pub fn delegate(&self) -> &D { -// &self.delegate -// } - -// pub fn delegate_mut(&mut self) -> &mut D { -// &mut self.delegate -// } - -// pub fn query(&self, cx: &AppContext) -> String { -// self.query_editor.read(cx).text(cx) -// } - -// pub fn set_query(&self, query: impl Into>, cx: &mut ViewContext) { -// self.query_editor -// .update(cx, |editor, cx| editor.set_text(query, cx)); -// } - -// fn on_query_editor_event( -// &mut self, -// _: ViewHandle, -// event: &editor::Event, -// cx: &mut ViewContext, -// ) { -// match event { -// editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx), -// editor::Event::Blurred if !self.confirmed => { -// self.dismiss(cx); -// } -// _ => {} -// } -// } - -// pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { -// let update = self.delegate.update_matches(query, cx); -// self.matches_updated(cx); -// self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move { -// update.await; -// this.update(&mut cx, |this, cx| { -// this.matches_updated(cx); -// }) -// .log_err() -// })); -// } - -// fn matches_updated(&mut self, cx: &mut ViewContext) { -// let index = self.delegate.selected_index(); -// let target = if self.delegate.center_selection_after_match_updates() { -// ScrollTarget::Center(index) -// } else { -// ScrollTarget::Show(index) -// }; -// self.list_state.scroll_to(target); -// self.pending_update_matches = None; -// if let Some(secondary) = self.confirm_on_update.take() { -// self.confirmed = true; -// self.delegate.confirm(secondary, cx) -// } -// cx.notify(); -// } - -// pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { -// if self.delegate.match_count() > 0 { -// self.delegate.set_selected_index(0, cx); -// self.list_state.scroll_to(ScrollTarget::Show(0)); -// } - -// cx.notify(); -// } - -// pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext) { -// if self.delegate.match_count() > 0 { -// self.confirmed = true; -// self.delegate.set_selected_index(index, cx); -// self.delegate.confirm(cmd, cx); -// } -// } - -// pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { -// let match_count = self.delegate.match_count(); -// if match_count > 0 { -// let index = match_count - 1; -// self.delegate.set_selected_index(index, cx); -// self.list_state.scroll_to(ScrollTarget::Show(index)); -// } -// cx.notify(); -// } - -// pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { -// let next_index = self.delegate.selected_index() + 1; -// if next_index < self.delegate.match_count() { -// self.delegate.set_selected_index(next_index, cx); -// self.list_state.scroll_to(ScrollTarget::Show(next_index)); -// } - -// cx.notify(); -// } - -// pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { -// let mut selected_index = self.delegate.selected_index(); -// if selected_index > 0 { -// selected_index -= 1; -// self.delegate.set_selected_index(selected_index, cx); -// self.list_state -// .scroll_to(ScrollTarget::Show(selected_index)); -// } - -// cx.notify(); -// } - -// pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { -// if self.pending_update_matches.is_some() { -// self.confirm_on_update = Some(false) -// } else { -// self.confirmed = true; -// self.delegate.confirm(false, cx); -// } -// } - -// pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext) { -// if self.pending_update_matches.is_some() { -// self.confirm_on_update = Some(true) -// } else { -// self.confirmed = true; -// self.delegate.confirm(true, cx); -// } -// } - -// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { -// self.dismiss(cx); -// } - -// fn dismiss(&mut self, cx: &mut ViewContext) { -// cx.emit(PickerEvent::Dismiss); -// self.delegate.dismissed(cx); -// } -// } diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index f5f2456d42bd2137081a6c6f8df262dee5e6f22a..061490779bb2fbb3b8c5a50d14efd532f2009c7c 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -93,11 +93,11 @@ impl PickerStory { KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")), ]); - let fh = cx.focus_handle(); - cx.focus(&fh); + let focus_handle = cx.focus_handle(); + cx.focus(&focus_handle); PickerStory { - focus_handle: fh, + focus_handle, candidates: vec![ "Baguette (France)".into(), "Baklava (Turkey)".into(), From 6928ad13352348851a486a7b636858c3dfd25efc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 11:00:53 -0800 Subject: [PATCH 08/14] Rename List -> UniformList Co-authored-by: Mikayla --- crates/gpui2/src/elements.rs | 4 +- .../src/elements/{list.rs => uniform_list.rs} | 54 ++++++++----------- crates/picker2/src/picker2.rs | 9 ++-- 3 files changed, 27 insertions(+), 40 deletions(-) rename crates/gpui2/src/elements/{list.rs => uniform_list.rs} (85%) diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements.rs index 3ebe5bf5d1fbb4196c91885c230d23fd0d5aa170..eb061f7d34c8e77c1a366123818b253d33620c7a 100644 --- a/crates/gpui2/src/elements.rs +++ b/crates/gpui2/src/elements.rs @@ -1,11 +1,11 @@ mod div; mod img; -mod list; mod svg; mod text; +mod uniform_list; pub use div::*; pub use img::*; -pub use list::*; pub use svg::*; pub use text::*; +pub use uniform_list::*; diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/uniform_list.rs similarity index 85% rename from crates/gpui2/src/elements/list.rs rename to crates/gpui2/src/elements/uniform_list.rs index acd1a524f32f0c0e174d0f07640a99ce025cbc22..64933951f5ea36e2a8e19ddf3e8256a0e318c34e 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -9,18 +9,18 @@ use smallvec::SmallVec; use std::{cmp, ops::Range, sync::Arc}; use taffy::style::Overflow; -pub fn list( +pub fn uniform_list( id: Id, item_count: usize, f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> SmallVec<[C; 64]>, -) -> List +) -> UniformList where Id: Into, V: 'static, C: Component, { let id = id.into(); - List { + UniformList { id: id.clone(), style: Default::default(), item_count, @@ -35,7 +35,7 @@ where } } -pub struct List { +pub struct UniformList { id: ElementId, style: StyleRefinement, item_count: usize, @@ -47,20 +47,20 @@ pub struct List { ) -> SmallVec<[AnyElement; 64]>, >, interactivity: StatefulInteractivity, - scroll_handle: Option, + scroll_handle: Option, } #[derive(Clone)] -pub struct ListScrollHandle(Arc>>); +pub struct UniformListScrollHandle(Arc>>); #[derive(Clone, Debug)] -struct ListScrollHandleState { +struct ScrollHandleState { item_height: Pixels, list_height: Pixels, scroll_offset: Arc>>, } -impl ListScrollHandle { +impl UniformListScrollHandle { pub fn new() -> Self { Self(Arc::new(Mutex::new(None))) } @@ -80,19 +80,14 @@ impl ListScrollHandle { } } -#[derive(Default)] -pub struct ListState { - interactive: InteractiveElementState, -} - -impl Styled for List { +impl Styled for UniformList { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } -impl Element for List { - type ElementState = ListState; +impl Element for UniformList { + type ElementState = InteractiveElementState; fn id(&self) -> Option { Some(self.id.clone()) @@ -104,8 +99,7 @@ impl Element for List { element_state: Option, _: &mut ViewContext, ) -> Self::ElementState { - let element_state = element_state.unwrap_or_default(); - element_state + element_state.unwrap_or_default() } fn layout( @@ -141,16 +135,15 @@ impl Element for List { if self.item_count > 0 { let item_height = self.measure_item_height(view_state, padded_bounds, cx); if let Some(scroll_handle) = self.scroll_handle.clone() { - scroll_handle.0.lock().replace(ListScrollHandleState { + scroll_handle.0.lock().replace(ScrollHandleState { item_height, list_height: padded_bounds.size.height, - scroll_offset: element_state.interactive.track_scroll_offset(), + scroll_offset: element_state.track_scroll_offset(), }); } let visible_item_count = (padded_bounds.size.height / item_height).ceil() as usize + 1; let scroll_offset = element_state - .interactive .scroll_offset() .map_or((0.0).into(), |offset| offset.y); let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; @@ -194,19 +187,14 @@ impl Element for List { let overflow = point(style.overflow.x, Overflow::Scroll); cx.with_z_index(0, |cx| { - self.interactivity.paint( - bounds, - content_size, - overflow, - &mut element_state.interactive, - cx, - ); + self.interactivity + .paint(bounds, content_size, overflow, element_state, cx); }); }) } } -impl List { +impl UniformList { fn measure_item_height( &self, view_state: &mut V, @@ -228,25 +216,25 @@ impl List { cx.layout_bounds(layout_id).size.height } - pub fn track_scroll(mut self, handle: ListScrollHandle) -> Self { + pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { self.scroll_handle = Some(handle); self } } -impl StatelessInteractive for List { +impl StatelessInteractive for UniformList { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.interactivity.as_stateless_mut() } } -impl StatefulInteractive for List { +impl StatefulInteractive for UniformList { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { &mut self.interactivity } } -impl Component for List { +impl Component for UniformList { fn render(self) -> AnyElement { AnyElement::new(self) } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 8161c0d0a2546d94b82ac6f640e3185074744df6..74bdcd19bd8bf28deec46d4000bf2de8d91a1301 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,6 +1,6 @@ use gpui::{ - div, list, Component, ElementId, FocusHandle, ListScrollHandle, ParentElement, - StatelessInteractive, Styled, ViewContext, + div, uniform_list, Component, ElementId, FocusHandle, ParentElement, StatelessInteractive, + Styled, UniformListScrollHandle, ViewContext, }; use std::cmp; @@ -24,7 +24,6 @@ pub trait PickerDelegate: Sized + 'static { fn confirm(&mut self, secondary: bool, picker_id: ElementId, cx: &mut ViewContext); fn dismissed(&mut self, picker_id: ElementId, cx: &mut ViewContext); - // todo!("rename to render_candidate?") fn render_match( &self, ix: usize, @@ -47,7 +46,7 @@ impl Picker { impl Picker { pub fn render(self, view: &mut V, _cx: &mut ViewContext) -> impl Component { let id = self.id.clone(); - let scroll_handle = ListScrollHandle::new(); + let scroll_handle = UniformListScrollHandle::new(); div() .size_full() .id(self.id.clone()) @@ -120,7 +119,7 @@ impl Picker { } }) .child( - list( + uniform_list( "candidates", view.match_count(self.id.clone()), move |view: &mut V, visible_range, cx| { From 10c94cc8b77c14ca61b60538169093844ceb601c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 11:47:49 -0800 Subject: [PATCH 09/14] Remove unused import --- crates/workspace2/src/modal_layer.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a0b759b1d92f67a987963dcfc0b1a91052007fc2..4121574b6a490867ec1dd612aca0b417cd138b6f 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,11 +1,8 @@ -use std::{any::TypeId, sync::Arc}; - +use crate::Workspace; use gpui::{ - div, AnyView, AppContext, Component, Div, ParentElement, Render, StatelessInteractive, View, - ViewContext, + div, AnyView, AppContext, Div, ParentElement, Render, StatelessInteractive, View, ViewContext, }; - -use crate::Workspace; +use std::{any::TypeId, sync::Arc}; pub struct ModalRegistry { registered_modals: Vec<(TypeId, Box) -> Div>)>, From 80e6427eec13a7483523cceac097d98385be48b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 13:03:36 -0800 Subject: [PATCH 10/14] :art: Co-authored-by: Mikayla --- crates/picker2/src/picker2.rs | 183 ++++++++++++++++++---------------- 1 file changed, 98 insertions(+), 85 deletions(-) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 74bdcd19bd8bf28deec46d4000bf2de8d91a1301..2040ff484f5bd933d9a5f799e48284c7bd381aab 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -41,96 +41,109 @@ impl Picker { phantom: std::marker::PhantomData, } } + + fn bind_actions>( + div: T, + id: ElementId, + scroll_handle: &UniformListScrollHandle, + ) -> T { + div.on_action({ + let id = id.clone(); + let scroll_handle = scroll_handle.clone(); + move |view: &mut V, _: &menu::SelectNext, cx| { + let count = view.match_count(id.clone()); + if count > 0 { + let index = view.selected_index(id.clone()); + let ix = cmp::min(index + 1, count - 1); + view.set_selected_index(ix, id.clone(), cx); + scroll_handle.scroll_to_item(ix); + } + } + }) + .on_action({ + let id = id.clone(); + let scroll_handle = scroll_handle.clone(); + move |view, _: &menu::SelectPrev, cx| { + let count = view.match_count(id.clone()); + if count > 0 { + let index = view.selected_index(id.clone()); + let ix = index.saturating_sub(1); + view.set_selected_index(ix, id.clone(), cx); + scroll_handle.scroll_to_item(ix); + } + } + }) + .on_action({ + let id = id.clone(); + let scroll_handle = scroll_handle.clone(); + move |view: &mut V, _: &menu::SelectFirst, cx| { + let count = view.match_count(id.clone()); + if count > 0 { + view.set_selected_index(0, id.clone(), cx); + scroll_handle.scroll_to_item(0); + } + } + }) + .on_action({ + let id = id.clone(); + let scroll_handle = scroll_handle.clone(); + move |view: &mut V, _: &menu::SelectLast, cx| { + let count = view.match_count(id.clone()); + if count > 0 { + view.set_selected_index(count - 1, id.clone(), cx); + scroll_handle.scroll_to_item(count - 1); + } + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::Cancel, cx| { + view.dismissed(id.clone(), cx); + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::Confirm, cx| { + view.confirm(false, id.clone(), cx); + } + }) + .on_action({ + let id = id.clone(); + move |view: &mut V, _: &menu::SecondaryConfirm, cx| { + view.confirm(true, id.clone(), cx); + } + }) + } } impl Picker { pub fn render(self, view: &mut V, _cx: &mut ViewContext) -> impl Component { let id = self.id.clone(); let scroll_handle = UniformListScrollHandle::new(); - div() - .size_full() - .id(self.id.clone()) - .track_focus(&self.focus_handle) - .context("picker") - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view: &mut V, _: &menu::SelectNext, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - let index = view.selected_index(id.clone()); - let ix = cmp::min(index + 1, count - 1); - view.set_selected_index(ix, id.clone(), cx); - scroll_handle.scroll_to_item(ix); - } - } - }) - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view, _: &menu::SelectPrev, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - let index = view.selected_index(id.clone()); - let ix = index.saturating_sub(1); - view.set_selected_index(ix, id.clone(), cx); - scroll_handle.scroll_to_item(ix); - } - } - }) - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view: &mut V, _: &menu::SelectFirst, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - view.set_selected_index(0, id.clone(), cx); - scroll_handle.scroll_to_item(0); - } - } - }) - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view: &mut V, _: &menu::SelectLast, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - view.set_selected_index(count - 1, id.clone(), cx); - scroll_handle.scroll_to_item(count - 1); - } - } - }) - .on_action({ - let id = id.clone(); - move |view: &mut V, _: &menu::Cancel, cx| { - view.dismissed(id.clone(), cx); - } - }) - .on_action({ - let id = id.clone(); - move |view: &mut V, _: &menu::Confirm, cx| { - view.confirm(false, id.clone(), cx); - } - }) - .on_action({ - let id = id.clone(); - move |view: &mut V, _: &menu::SecondaryConfirm, cx| { - view.confirm(true, id.clone(), cx); - } - }) - .child( - uniform_list( - "candidates", - view.match_count(self.id.clone()), - move |view: &mut V, visible_range, cx| { - let selected_ix = view.selected_index(self.id.clone()); - visible_range - .map(|ix| view.render_match(ix, ix == selected_ix, self.id.clone(), cx)) - .collect() - }, - ) - .track_scroll(scroll_handle.clone()) - .size_full(), - ) + Self::bind_actions( + div() + .id(self.id.clone()) + .size_full() + .track_focus(&self.focus_handle) + .context("picker") + .child( + uniform_list( + "candidates", + view.match_count(self.id.clone()), + move |view: &mut V, visible_range, cx| { + let selected_ix = view.selected_index(self.id.clone()); + visible_range + .map(|ix| { + view.render_match(ix, ix == selected_ix, self.id.clone(), cx) + }) + .collect() + }, + ) + .track_scroll(scroll_handle.clone()) + .size_full(), + ), + id, + &scroll_handle, + ) } } From b9d051eae758dc42125b85d8c30bdee1380ada16 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 13:37:10 -0800 Subject: [PATCH 11/14] Start work on adding a filter editor to the picker Implement picker as a view instead of as a component Co-authored-by: Mikayla Co-authored-by: Marshall --- Cargo.lock | 2 + crates/picker2/src/picker2.rs | 212 ++++++++++-------------- crates/storybook2/Cargo.toml | 2 + crates/storybook2/src/stories/picker.rs | 152 +++++++++-------- crates/storybook2/src/storybook2.rs | 2 + 5 files changed, 173 insertions(+), 197 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9443c71b20deccacf6d67ae33d00d5ae7846d85c..e52a8444e4f55bf06d615543ab43e34995e7c617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8557,8 +8557,10 @@ dependencies = [ "backtrace-on-stack-overflow", "chrono", "clap 4.4.4", + "editor2", "gpui2", "itertools 0.11.0", + "language2", "log", "menu2", "picker2", diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 2040ff484f5bd933d9a5f799e48284c7bd381aab..5b454d6edd2fbb39657547a1e047e16132d4ee3f 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,149 +1,121 @@ +use editor::Editor; use gpui::{ - div, uniform_list, Component, ElementId, FocusHandle, ParentElement, StatelessInteractive, - Styled, UniformListScrollHandle, ViewContext, + div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, + UniformListScrollHandle, View, ViewContext, VisualContext, }; use std::cmp; -#[derive(Component)] -pub struct Picker { - id: ElementId, - focus_handle: FocusHandle, - phantom: std::marker::PhantomData, +pub struct Picker { + pub delegate: D, + scroll_handle: UniformListScrollHandle, + editor: View, } pub trait PickerDelegate: Sized + 'static { - type ListItem: Component; + type ListItem: Component>; - fn match_count(&self, picker_id: ElementId) -> usize; - fn selected_index(&self, picker_id: ElementId) -> usize; - fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext); + fn match_count(&self) -> usize; + fn selected_index(&self) -> usize; + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); // fn placeholder_text(&self) -> Arc; // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; - fn confirm(&mut self, secondary: bool, picker_id: ElementId, cx: &mut ViewContext); - fn dismissed(&mut self, picker_id: ElementId, cx: &mut ViewContext); + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); + fn dismissed(&mut self, cx: &mut ViewContext>); fn render_match( &self, ix: usize, selected: bool, - picker_id: ElementId, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) -> Self::ListItem; } -impl Picker { - pub fn new(id: impl Into, focus_handle: FocusHandle) -> Self { +impl Picker { + pub fn new(delegate: D, cx: &mut ViewContext) -> Self { Self { - id: id.into(), - focus_handle, - phantom: std::marker::PhantomData, + delegate, + scroll_handle: UniformListScrollHandle::new(), + editor: cx.build_view(|cx| Editor::single_line(cx)), } } - fn bind_actions>( - div: T, - id: ElementId, - scroll_handle: &UniformListScrollHandle, - ) -> T { - div.on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view: &mut V, _: &menu::SelectNext, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - let index = view.selected_index(id.clone()); - let ix = cmp::min(index + 1, count - 1); - view.set_selected_index(ix, id.clone(), cx); - scroll_handle.scroll_to_item(ix); - } - } - }) - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view, _: &menu::SelectPrev, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - let index = view.selected_index(id.clone()); - let ix = index.saturating_sub(1); - view.set_selected_index(ix, id.clone(), cx); - scroll_handle.scroll_to_item(ix); - } - } - }) - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view: &mut V, _: &menu::SelectFirst, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - view.set_selected_index(0, id.clone(), cx); - scroll_handle.scroll_to_item(0); - } - } - }) - .on_action({ - let id = id.clone(); - let scroll_handle = scroll_handle.clone(); - move |view: &mut V, _: &menu::SelectLast, cx| { - let count = view.match_count(id.clone()); - if count > 0 { - view.set_selected_index(count - 1, id.clone(), cx); - scroll_handle.scroll_to_item(count - 1); - } - } - }) - .on_action({ - let id = id.clone(); - move |view: &mut V, _: &menu::Cancel, cx| { - view.dismissed(id.clone(), cx); - } - }) - .on_action({ - let id = id.clone(); - move |view: &mut V, _: &menu::Confirm, cx| { - view.confirm(false, id.clone(), cx); - } - }) - .on_action({ - let id = id.clone(); - move |view: &mut V, _: &menu::SecondaryConfirm, cx| { - view.confirm(true, id.clone(), cx); - } - }) + fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + if count > 0 { + let index = self.delegate.selected_index(); + let ix = cmp::min(index + 1, count - 1); + self.delegate.set_selected_index(ix, cx); + self.scroll_handle.scroll_to_item(ix); + } + } + + fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + if count > 0 { + let index = self.delegate.selected_index(); + let ix = index.saturating_sub(1); + self.delegate.set_selected_index(ix, cx); + self.scroll_handle.scroll_to_item(ix); + } + } + + fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + if count > 0 { + self.delegate.set_selected_index(0, cx); + self.scroll_handle.scroll_to_item(0); + } + } + + fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + if count > 0 { + self.delegate.set_selected_index(count - 1, cx); + self.scroll_handle.scroll_to_item(count - 1); + } + } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + self.delegate.dismissed(cx); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + self.delegate.confirm(false, cx); + } + + fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + self.delegate.confirm(true, cx); } } -impl Picker { - pub fn render(self, view: &mut V, _cx: &mut ViewContext) -> impl Component { - let id = self.id.clone(); - let scroll_handle = UniformListScrollHandle::new(); - Self::bind_actions( - div() - .id(self.id.clone()) - .size_full() - .track_focus(&self.focus_handle) - .context("picker") - .child( - uniform_list( - "candidates", - view.match_count(self.id.clone()), - move |view: &mut V, visible_range, cx| { - let selected_ix = view.selected_index(self.id.clone()); - visible_range - .map(|ix| { - view.render_match(ix, ix == selected_ix, self.id.clone(), cx) - }) - .collect() - }, - ) - .track_scroll(scroll_handle.clone()) - .size_full(), - ), - id, - &scroll_handle, - ) +impl Render for Picker { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .size_full() + .context("picker") + .on_action(Self::select_next) + .on_action(Self::select_prev) + .on_action(Self::select_first) + .on_action(Self::select_last) + .on_action(Self::cancel) + .on_action(Self::confirm) + .on_action(Self::secondary_confirm) + .child(self.editor.clone()) + .child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()) + .size_full(), + ) } } diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 7a59dda6dfe5d351b89981560c4cc4781be4ae7e..3bf06a97780b8c24c1086d009eec2c76ae34d65e 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -13,9 +13,11 @@ anyhow.workspace = true # TODO: Remove after diagnosing stack overflow. backtrace-on-stack-overflow = "0.3.0" clap = { version = "4.4", features = ["derive", "string"] } +editor = { package = "editor2", path = "../editor2" } chrono = "0.4" gpui = { package = "gpui2", path = "../gpui2" } itertools = "0.11.0" +language = { package = "language2", path = "../language2" } log.workspace = true rust-embed.workspace = true serde.workspace = true diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 061490779bb2fbb3b8c5a50d14efd532f2009c7c..e256d0f53c11f3136adc2ba14b1c00416ddf424a 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -6,15 +6,19 @@ use picker::{Picker, PickerDelegate}; use theme2::ActiveTheme; pub struct PickerStory { - selected_ix: usize, - candidates: Vec, + picker: View>, focus_handle: FocusHandle, } -impl PickerDelegate for PickerStory { - type ListItem = Div; +struct Delegate { + candidates: Vec, + selected_ix: usize, +} + +impl PickerDelegate for Delegate { + type ListItem = Div>; - fn match_count(&self, _picker_id: gpui::ElementId) -> usize { + fn match_count(&self) -> usize { self.candidates.len() } @@ -22,8 +26,7 @@ impl PickerDelegate for PickerStory { &self, ix: usize, selected: bool, - _picker_id: gpui::ElementId, - cx: &mut gpui::ViewContext, + cx: &mut gpui::ViewContext>, ) -> Self::ListItem { let colors = cx.theme().colors(); @@ -40,26 +43,16 @@ impl PickerDelegate for PickerStory { .child(self.candidates[ix].clone()) } - fn selected_index(&self, picker_id: gpui::ElementId) -> usize { + fn selected_index(&self) -> usize { self.selected_ix } - fn set_selected_index( - &mut self, - ix: usize, - _picker_id: gpui::ElementId, - cx: &mut gpui::ViewContext, - ) { + fn set_selected_index(&mut self, ix: usize, cx: &mut gpui::ViewContext>) { self.selected_ix = ix; cx.notify(); } - fn confirm( - &mut self, - secondary: bool, - picker_id: gpui::ElementId, - cx: &mut gpui::ViewContext, - ) { + fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext>) { if secondary { eprintln!("Secondary confirmed {}", self.candidates[self.selected_ix]) } else { @@ -67,7 +60,7 @@ impl PickerDelegate for PickerStory { } } - fn dismissed(&mut self, picker_id: gpui::ElementId, cx: &mut gpui::ViewContext) { + fn dismissed(&mut self, cx: &mut gpui::ViewContext>) { cx.quit(); } } @@ -98,58 +91,65 @@ impl PickerStory { PickerStory { focus_handle, - candidates: vec![ - "Baguette (France)".into(), - "Baklava (Turkey)".into(), - "Beef Wellington (UK)".into(), - "Biryani (India)".into(), - "Borscht (Ukraine)".into(), - "Bratwurst (Germany)".into(), - "Bulgogi (Korea)".into(), - "Burrito (USA)".into(), - "Ceviche (Peru)".into(), - "Chicken Tikka Masala (India)".into(), - "Churrasco (Brazil)".into(), - "Couscous (North Africa)".into(), - "Croissant (France)".into(), - "Dim Sum (China)".into(), - "Empanada (Argentina)".into(), - "Fajitas (Mexico)".into(), - "Falafel (Middle East)".into(), - "Feijoada (Brazil)".into(), - "Fish and Chips (UK)".into(), - "Fondue (Switzerland)".into(), - "Goulash (Hungary)".into(), - "Haggis (Scotland)".into(), - "Kebab (Middle East)".into(), - "Kimchi (Korea)".into(), - "Lasagna (Italy)".into(), - "Maple Syrup Pancakes (Canada)".into(), - "Moussaka (Greece)".into(), - "Pad Thai (Thailand)".into(), - "Paella (Spain)".into(), - "Pancakes (USA)".into(), - "Pasta Carbonara (Italy)".into(), - "Pavlova (Australia)".into(), - "Peking Duck (China)".into(), - "Pho (Vietnam)".into(), - "Pierogi (Poland)".into(), - "Pizza (Italy)".into(), - "Poutine (Canada)".into(), - "Pretzel (Germany)".into(), - "Ramen (Japan)".into(), - "Rendang (Indonesia)".into(), - "Sashimi (Japan)".into(), - "Satay (Indonesia)".into(), - "Shepherd's Pie (Ireland)".into(), - "Sushi (Japan)".into(), - "Tacos (Mexico)".into(), - "Tandoori Chicken (India)".into(), - "Tortilla (Spain)".into(), - "Tzatziki (Greece)".into(), - "Wiener Schnitzel (Austria)".into(), - ], - selected_ix: 0, + picker: cx.build_view(|cx| { + Picker::new( + Delegate { + candidates: vec![ + "Baguette (France)".into(), + "Baklava (Turkey)".into(), + "Beef Wellington (UK)".into(), + "Biryani (India)".into(), + "Borscht (Ukraine)".into(), + "Bratwurst (Germany)".into(), + "Bulgogi (Korea)".into(), + "Burrito (USA)".into(), + "Ceviche (Peru)".into(), + "Chicken Tikka Masala (India)".into(), + "Churrasco (Brazil)".into(), + "Couscous (North Africa)".into(), + "Croissant (France)".into(), + "Dim Sum (China)".into(), + "Empanada (Argentina)".into(), + "Fajitas (Mexico)".into(), + "Falafel (Middle East)".into(), + "Feijoada (Brazil)".into(), + "Fish and Chips (UK)".into(), + "Fondue (Switzerland)".into(), + "Goulash (Hungary)".into(), + "Haggis (Scotland)".into(), + "Kebab (Middle East)".into(), + "Kimchi (Korea)".into(), + "Lasagna (Italy)".into(), + "Maple Syrup Pancakes (Canada)".into(), + "Moussaka (Greece)".into(), + "Pad Thai (Thailand)".into(), + "Paella (Spain)".into(), + "Pancakes (USA)".into(), + "Pasta Carbonara (Italy)".into(), + "Pavlova (Australia)".into(), + "Peking Duck (China)".into(), + "Pho (Vietnam)".into(), + "Pierogi (Poland)".into(), + "Pizza (Italy)".into(), + "Poutine (Canada)".into(), + "Pretzel (Germany)".into(), + "Ramen (Japan)".into(), + "Rendang (Indonesia)".into(), + "Sashimi (Japan)".into(), + "Satay (Indonesia)".into(), + "Shepherd's Pie (Ireland)".into(), + "Sushi (Japan)".into(), + "Tacos (Mexico)".into(), + "Tandoori Chicken (India)".into(), + "Tortilla (Spain)".into(), + "Tzatziki (Greece)".into(), + "Wiener Schnitzel (Austria)".into(), + ], + selected_ix: 0, + }, + cx, + ) + }), } }) } @@ -159,11 +159,9 @@ impl Render for PickerStory { type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - let theme = cx.theme(); - div() - .bg(theme.styles.colors.background) + .bg(cx.theme().styles.colors.background) .size_full() - .child(Picker::new("picker_story", self.focus_handle.clone())) + .child(self.picker.clone()) } } diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c8849c134278ebe36d583c34a14b6e8961f4885b..f0ba124162d546bcdbdc1fce12e986364f950dad 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -72,6 +72,8 @@ fn main() { ThemeSettings::override_global(theme_settings, cx); ui::settings::init(cx); + language::init(cx); + editor::init(cx); let window = cx.open_window( WindowOptions { From ea603401e2cbd074aba4dc0d457ba381a0f66957 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 13:48:32 -0800 Subject: [PATCH 12/14] Get actions + focus working on picker, now that it's a view Co-authored-by: Marshall --- crates/editor2/src/editor.rs | 2 +- crates/picker2/src/picker2.rs | 15 +++++++++++---- crates/storybook2/src/stories/picker.rs | 15 ++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index c63c531cebddcfa1f807ee5460a54b1976d9c175..52ded35d57473192a22828a4e3344ab273530c6c 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9186,7 +9186,7 @@ impl Editor { // supports // } - fn focus(&self, cx: &mut WindowContext) { + pub fn focus(&self, cx: &mut WindowContext) { cx.focus(&self.focus_handle) } } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 5b454d6edd2fbb39657547a1e047e16132d4ee3f..e80801229d06101d17fcfb5c8fe2008e7c080b13 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,8 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, - UniformListScrollHandle, View, ViewContext, VisualContext, + div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, + StatelessInteractive, Styled, UniformListScrollHandle, View, ViewContext, VisualContext, + WindowContext, }; use std::cmp; @@ -41,6 +42,10 @@ impl Picker { } } + pub fn focus(&self, cx: &mut WindowContext) { + self.editor.update(cx, |editor, cx| editor.focus(cx)); + } + fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { @@ -91,12 +96,14 @@ impl Picker { } impl Render for Picker { - type Element = Div; + type Element = Div, FocusEnabled>; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { div() - .size_full() .context("picker") + .id("picker-container") + .focusable() + .size_full() .on_action(Self::select_next) .on_action(Self::select_prev) .on_action(Self::select_first) diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index e256d0f53c11f3136adc2ba14b1c00416ddf424a..0a0ee0c1d4513df617c55b16561352d1e90948e8 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,13 +1,12 @@ use gpui::{ - div, Component, Div, FocusHandle, KeyBinding, ParentElement, Render, SharedString, - StatelessInteractive, Styled, View, VisualContext, WindowContext, + div, Component, Div, KeyBinding, ParentElement, Render, SharedString, StatelessInteractive, + Styled, View, VisualContext, WindowContext, }; use picker::{Picker, PickerDelegate}; use theme2::ActiveTheme; pub struct PickerStory { picker: View>, - focus_handle: FocusHandle, } struct Delegate { @@ -86,13 +85,9 @@ impl PickerStory { KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")), ]); - let focus_handle = cx.focus_handle(); - cx.focus(&focus_handle); - PickerStory { - focus_handle, picker: cx.build_view(|cx| { - Picker::new( + let picker = Picker::new( Delegate { candidates: vec![ "Baguette (France)".into(), @@ -148,7 +143,9 @@ impl PickerStory { selected_ix: 0, }, cx, - ) + ); + picker.focus(cx); + picker }), } }) From 06960df287853505b84018eb3218525f4803aca3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Nov 2023 17:24:04 -0800 Subject: [PATCH 13/14] Implement basic fuzzy filtering in picker --- Cargo.lock | 1 + crates/picker2/src/picker2.rs | 41 +++++- crates/storybook2/Cargo.toml | 1 + crates/storybook2/src/stories/picker.rs | 176 +++++++++++++++--------- 4 files changed, 153 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e52a8444e4f55bf06d615543ab43e34995e7c617..580de35aa0c9b5ddedcd6dc9b4821cb39a5b5e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8558,6 +8558,7 @@ dependencies = [ "chrono", "clap 4.4.4", "editor2", + "fuzzy2", "gpui2", "itertools 0.11.0", "language2", diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index e80801229d06101d17fcfb5c8fe2008e7c080b13..075cf10ff6811eeb2ce8f8a82f31f9799c0a4a00 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, - StatelessInteractive, Styled, UniformListScrollHandle, View, ViewContext, VisualContext, + StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::cmp; @@ -10,6 +10,7 @@ pub struct Picker { pub delegate: D, scroll_handle: UniformListScrollHandle, editor: View, + pending_update_matches: Option>>, } pub trait PickerDelegate: Sized + 'static { @@ -20,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static { fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); // fn placeholder_text(&self) -> Arc; - // fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); fn dismissed(&mut self, cx: &mut ViewContext>); @@ -35,10 +36,13 @@ pub trait PickerDelegate: Sized + 'static { impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { + let editor = cx.build_view(|cx| Editor::single_line(cx)); + cx.subscribe(&editor, Self::on_input_editor_event).detach(); Self { delegate, scroll_handle: UniformListScrollHandle::new(), - editor: cx.build_view(|cx| Editor::single_line(cx)), + pending_update_matches: None, + editor, } } @@ -93,6 +97,37 @@ impl Picker { fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { self.delegate.confirm(true, cx); } + + fn on_input_editor_event( + &mut self, + _: View, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if let editor::Event::BufferEdited = event { + let query = self.editor.read(cx).text(cx); + self.update_matches(query, cx); + } + } + + pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { + let update = self.delegate.update_matches(query, cx); + self.matches_updated(cx); + self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move { + update.await; + this.update(&mut cx, |this, cx| { + this.matches_updated(cx); + }) + .ok() + })); + } + + fn matches_updated(&mut self, cx: &mut ViewContext) { + let index = self.delegate.selected_index(); + self.scroll_handle.scroll_to_item(index); + self.pending_update_matches = None; + cx.notify(); + } } impl Render for Picker { diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 3bf06a97780b8c24c1086d009eec2c76ae34d65e..7c6776c9309fd68352b7e18b846af8cd4e8fbdae 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -15,6 +15,7 @@ backtrace-on-stack-overflow = "0.3.0" clap = { version = "4.4", features = ["derive", "string"] } editor = { package = "editor2", path = "../editor2" } chrono = "0.4" +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } itertools = "0.11.0" language = { package = "language2", path = "../language2" } diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 0a0ee0c1d4513df617c55b16561352d1e90948e8..82a010e6b30e820681b903232b74c35cad6b8584 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,6 +1,9 @@ +use std::sync::Arc; + +use fuzzy::StringMatchCandidate; use gpui::{ - div, Component, Div, KeyBinding, ParentElement, Render, SharedString, StatelessInteractive, - Styled, View, VisualContext, WindowContext, + div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task, + View, VisualContext, WindowContext, }; use picker::{Picker, PickerDelegate}; use theme2::ActiveTheme; @@ -10,10 +13,30 @@ pub struct PickerStory { } struct Delegate { - candidates: Vec, + candidates: Arc<[StringMatchCandidate]>, + matches: Vec, selected_ix: usize, } +impl Delegate { + fn new(strings: &[&str]) -> Self { + Self { + candidates: strings + .iter() + .copied() + .enumerate() + .map(|(id, string)| StringMatchCandidate { + id, + char_bag: string.into(), + string: string.into(), + }) + .collect(), + matches: vec![], + selected_ix: 0, + } + } +} + impl PickerDelegate for Delegate { type ListItem = Div>; @@ -28,6 +51,10 @@ impl PickerDelegate for Delegate { cx: &mut gpui::ViewContext>, ) -> Self::ListItem { let colors = cx.theme().colors(); + let Some(candidate_ix) = self.matches.get(ix) else { + return div(); + }; + let candidate = self.candidates[*candidate_ix].string.clone(); div() .text_color(colors.text) @@ -39,7 +66,7 @@ impl PickerDelegate for Delegate { .bg(colors.element_active) .text_color(colors.text_accent) }) - .child(self.candidates[ix].clone()) + .child(candidate) } fn selected_index(&self) -> usize { @@ -52,16 +79,42 @@ impl PickerDelegate for Delegate { } fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext>) { + let candidate_ix = self.matches[self.selected_ix]; + let candidate = self.candidates[candidate_ix].string.clone(); + if secondary { - eprintln!("Secondary confirmed {}", self.candidates[self.selected_ix]) + eprintln!("Secondary confirmed {}", candidate) } else { - eprintln!("Confirmed {}", self.candidates[self.selected_ix]) + eprintln!("Confirmed {}", candidate) } } fn dismissed(&mut self, cx: &mut gpui::ViewContext>) { cx.quit(); } + + fn update_matches( + &mut self, + query: String, + cx: &mut gpui::ViewContext>, + ) -> Task<()> { + let candidates = self.candidates.clone(); + self.matches = cx + .background_executor() + .block(fuzzy::match_strings( + &candidates, + &query, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + )) + .into_iter() + .map(|r| r.candidate_id) + .collect(); + self.selected_ix = 0; + Task::ready(()) + } } impl PickerStory { @@ -87,63 +140,60 @@ impl PickerStory { PickerStory { picker: cx.build_view(|cx| { - let picker = Picker::new( - Delegate { - candidates: vec![ - "Baguette (France)".into(), - "Baklava (Turkey)".into(), - "Beef Wellington (UK)".into(), - "Biryani (India)".into(), - "Borscht (Ukraine)".into(), - "Bratwurst (Germany)".into(), - "Bulgogi (Korea)".into(), - "Burrito (USA)".into(), - "Ceviche (Peru)".into(), - "Chicken Tikka Masala (India)".into(), - "Churrasco (Brazil)".into(), - "Couscous (North Africa)".into(), - "Croissant (France)".into(), - "Dim Sum (China)".into(), - "Empanada (Argentina)".into(), - "Fajitas (Mexico)".into(), - "Falafel (Middle East)".into(), - "Feijoada (Brazil)".into(), - "Fish and Chips (UK)".into(), - "Fondue (Switzerland)".into(), - "Goulash (Hungary)".into(), - "Haggis (Scotland)".into(), - "Kebab (Middle East)".into(), - "Kimchi (Korea)".into(), - "Lasagna (Italy)".into(), - "Maple Syrup Pancakes (Canada)".into(), - "Moussaka (Greece)".into(), - "Pad Thai (Thailand)".into(), - "Paella (Spain)".into(), - "Pancakes (USA)".into(), - "Pasta Carbonara (Italy)".into(), - "Pavlova (Australia)".into(), - "Peking Duck (China)".into(), - "Pho (Vietnam)".into(), - "Pierogi (Poland)".into(), - "Pizza (Italy)".into(), - "Poutine (Canada)".into(), - "Pretzel (Germany)".into(), - "Ramen (Japan)".into(), - "Rendang (Indonesia)".into(), - "Sashimi (Japan)".into(), - "Satay (Indonesia)".into(), - "Shepherd's Pie (Ireland)".into(), - "Sushi (Japan)".into(), - "Tacos (Mexico)".into(), - "Tandoori Chicken (India)".into(), - "Tortilla (Spain)".into(), - "Tzatziki (Greece)".into(), - "Wiener Schnitzel (Austria)".into(), - ], - selected_ix: 0, - }, - cx, - ); + let mut delegate = Delegate::new(&[ + "Baguette (France)", + "Baklava (Turkey)", + "Beef Wellington (UK)", + "Biryani (India)", + "Borscht (Ukraine)", + "Bratwurst (Germany)", + "Bulgogi (Korea)", + "Burrito (USA)", + "Ceviche (Peru)", + "Chicken Tikka Masala (India)", + "Churrasco (Brazil)", + "Couscous (North Africa)", + "Croissant (France)", + "Dim Sum (China)", + "Empanada (Argentina)", + "Fajitas (Mexico)", + "Falafel (Middle East)", + "Feijoada (Brazil)", + "Fish and Chips (UK)", + "Fondue (Switzerland)", + "Goulash (Hungary)", + "Haggis (Scotland)", + "Kebab (Middle East)", + "Kimchi (Korea)", + "Lasagna (Italy)", + "Maple Syrup Pancakes (Canada)", + "Moussaka (Greece)", + "Pad Thai (Thailand)", + "Paella (Spain)", + "Pancakes (USA)", + "Pasta Carbonara (Italy)", + "Pavlova (Australia)", + "Peking Duck (China)", + "Pho (Vietnam)", + "Pierogi (Poland)", + "Pizza (Italy)", + "Poutine (Canada)", + "Pretzel (Germany)", + "Ramen (Japan)", + "Rendang (Indonesia)", + "Sashimi (Japan)", + "Satay (Indonesia)", + "Shepherd's Pie (Ireland)", + "Sushi (Japan)", + "Tacos (Mexico)", + "Tandoori Chicken (India)", + "Tortilla (Spain)", + "Tzatziki (Greece)", + "Wiener Schnitzel (Austria)", + ]); + delegate.update_matches("".into(), cx).detach(); + + let picker = Picker::new(delegate, cx); picker.focus(cx); picker }), From e9650c025fe42cf37ad92ee8f52bccc2163eb12a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 8 Nov 2023 11:26:26 -0500 Subject: [PATCH 14/14] Fix overflow in `UniformList` --- crates/gpui2/src/elements/uniform_list.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 64933951f5ea36e2a8e19ddf3e8256a0e318c34e..d43c6b59926de499959304d5af754bc8a97aec07 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -141,8 +141,11 @@ impl Element for UniformList { scroll_offset: element_state.track_scroll_offset(), }); } - let visible_item_count = - (padded_bounds.size.height / item_height).ceil() as usize + 1; + let visible_item_count = if item_height > px(0.) { + (padded_bounds.size.height / item_height).ceil() as usize + 1 + } else { + 0 + }; let scroll_offset = element_state .scroll_offset() .map_or((0.0).into(), |offset| offset.y);