Detailed changes
@@ -1880,6 +1880,30 @@ dependencies = [
"zed-actions",
]
+[[package]]
+name = "command_palette2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "ctor",
+ "editor2",
+ "env_logger 0.9.3",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "picker2",
+ "project2",
+ "serde",
+ "serde_json",
+ "settings2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+ "zed_actions2",
+]
+
[[package]]
name = "component_test"
version = "0.1.0"
@@ -6135,6 +6159,7 @@ dependencies = [
"serde_json",
"settings2",
"theme2",
+ "ui2",
"util",
]
@@ -11371,6 +11396,7 @@ dependencies = [
"cli",
"client2",
"collections",
+ "command_palette2",
"copilot2",
"ctor",
"db2",
@@ -11457,6 +11483,15 @@ dependencies = [
"util",
"uuid 1.4.1",
"workspace2",
+ "zed_actions2",
+]
+
+[[package]]
+name = "zed_actions2"
+version = "0.1.0"
+dependencies = [
+ "gpui2",
+ "serde",
]
[[package]]
@@ -20,6 +20,7 @@ members = [
"crates/collab_ui",
"crates/collections",
"crates/command_palette",
+ "crates/command_palette2",
"crates/component_test",
"crates/context_menu",
"crates/copilot",
@@ -110,7 +111,8 @@ members = [
"crates/xtask",
"crates/zed",
"crates/zed2",
- "crates/zed-actions"
+ "crates/zed-actions",
+ "crates/zed_actions2"
]
default-members = ["crates/zed"]
resolver = "2"
@@ -0,0 +1,34 @@
+[package]
+name = "command_palette2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/command_palette.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+ui = { package = "ui2", path = "../ui2" }
+util = { path = "../util" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package="workspace2", path = "../workspace2" }
+zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
+anyhow.workspace = true
+serde.workspace = true
+[dev-dependencies]
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+language = { package="language2", path = "../language2", features = ["test-support"] }
+project = { package="project2", path = "../project2", features = ["test-support"] }
+serde_json.workspace = true
+workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
+ctor.workspace = true
+env_logger.workspace = true
@@ -0,0 +1,534 @@
+use collections::{CommandPaletteFilter, HashMap};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+ actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke,
+ ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext,
+ WeakView, WindowContext,
+};
+use picker::{Picker, PickerDelegate};
+use std::cmp::{self, Reverse};
+use theme::ActiveTheme;
+use ui::{modal, Label};
+use util::{
+ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
+ ResultExt,
+};
+use workspace::{Modal, ModalEvent, Workspace};
+use zed_actions::OpenZedURL;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+ cx.set_global(HitCounts::default());
+ cx.observe_new_views(CommandPalette::register).detach();
+}
+
+pub struct CommandPalette {
+ picker: View<Picker<CommandPaletteDelegate>>,
+}
+
+impl CommandPalette {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(|workspace, _: &Toggle, cx| {
+ let Some(previous_focus_handle) = cx.focused() else {
+ return;
+ };
+ workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx));
+ });
+ }
+
+ fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext<Self>) -> Self {
+ let filter = cx.try_global::<CommandPaletteFilter>();
+
+ let commands = cx
+ .available_actions()
+ .into_iter()
+ .filter_map(|action| {
+ let name = action.name();
+ let namespace = name.split("::").next().unwrap_or("malformed action name");
+ if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
+ return None;
+ }
+
+ Some(Command {
+ name: humanize_action_name(&name),
+ action,
+ keystrokes: vec![], // todo!()
+ })
+ })
+ .collect();
+
+ let delegate =
+ CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
+
+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+ Self { picker }
+ }
+}
+
+impl EventEmitter<ModalEvent> for CommandPalette {}
+impl Modal for CommandPalette {
+ fn focus(&self, cx: &mut WindowContext) {
+ self.picker.update(cx, |picker, cx| picker.focus(cx));
+ }
+}
+
+impl Render for CommandPalette {
+ type Element = Div<Self>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ modal(cx).w_96().child(self.picker.clone())
+ }
+}
+
+pub type CommandPaletteInterceptor =
+ Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
+
+pub struct CommandInterceptResult {
+ pub action: Box<dyn Action>,
+ pub string: String,
+ pub positions: Vec<usize>,
+}
+
+pub struct CommandPaletteDelegate {
+ command_palette: WeakView<CommandPalette>,
+ commands: Vec<Command>,
+ matches: Vec<StringMatch>,
+ selected_ix: usize,
+ previous_focus_handle: FocusHandle,
+}
+
+struct Command {
+ name: String,
+ action: Box<dyn Action>,
+ keystrokes: Vec<Keystroke>,
+}
+
+impl Clone for Command {
+ fn clone(&self) -> Self {
+ Self {
+ name: self.name.clone(),
+ action: self.action.boxed_clone(),
+ keystrokes: self.keystrokes.clone(),
+ }
+ }
+}
+/// Hit count for each command in the palette.
+/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
+/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
+#[derive(Default)]
+struct HitCounts(HashMap<String, usize>);
+
+impl CommandPaletteDelegate {
+ fn new(
+ command_palette: WeakView<CommandPalette>,
+ commands: Vec<Command>,
+ previous_focus_handle: FocusHandle,
+ ) -> Self {
+ Self {
+ command_palette,
+ matches: commands
+ .iter()
+ .enumerate()
+ .map(|(i, command)| StringMatch {
+ candidate_id: i,
+ string: command.name.clone(),
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect(),
+ commands,
+ selected_ix: 0,
+ previous_focus_handle,
+ }
+ }
+}
+
+impl PickerDelegate for CommandPaletteDelegate {
+ type ListItem = Div<Picker<Self>>;
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_ix
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+ self.selected_ix = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> gpui::Task<()> {
+ let mut commands = self.commands.clone();
+
+ cx.spawn(move |picker, mut cx| async move {
+ cx.read_global::<HitCounts, _>(|hit_counts, _| {
+ commands.sort_by_key(|action| {
+ (
+ Reverse(hit_counts.0.get(&action.name).cloned()),
+ action.name.clone(),
+ )
+ });
+ })
+ .ok();
+
+ let candidates = commands
+ .iter()
+ .enumerate()
+ .map(|(ix, command)| StringMatchCandidate {
+ id: ix,
+ string: command.name.to_string(),
+ char_bag: command.name.chars().collect(),
+ })
+ .collect::<Vec<_>>();
+ let mut matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ 10000,
+ &Default::default(),
+ cx.background_executor().clone(),
+ )
+ .await
+ };
+
+ let mut intercept_result = cx
+ .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| {
+ (interceptor)(&query, cx)
+ })
+ .flatten();
+
+ if *RELEASE_CHANNEL == ReleaseChannel::Dev {
+ if parse_zed_link(&query).is_some() {
+ intercept_result = Some(CommandInterceptResult {
+ action: OpenZedURL { url: query.clone() }.boxed_clone(),
+ string: query.clone(),
+ positions: vec![],
+ })
+ }
+ }
+ if let Some(CommandInterceptResult {
+ action,
+ string,
+ positions,
+ }) = intercept_result
+ {
+ if let Some(idx) = matches
+ .iter()
+ .position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
+ {
+ matches.remove(idx);
+ }
+ commands.push(Command {
+ name: string.clone(),
+ action,
+ keystrokes: vec![],
+ });
+ matches.insert(
+ 0,
+ StringMatch {
+ candidate_id: commands.len() - 1,
+ string,
+ positions,
+ score: 0.0,
+ },
+ )
+ }
+ picker
+ .update(&mut cx, |picker, _| {
+ let delegate = &mut picker.delegate;
+ delegate.commands = commands;
+ delegate.matches = matches;
+ if delegate.matches.is_empty() {
+ delegate.selected_ix = 0;
+ } else {
+ delegate.selected_ix =
+ cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
+ }
+ })
+ .log_err();
+ })
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ self.command_palette
+ .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+ .log_err();
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if self.matches.is_empty() {
+ self.dismissed(cx);
+ return;
+ }
+ let action_ix = self.matches[self.selected_ix].candidate_id;
+ let command = self.commands.swap_remove(action_ix);
+ cx.update_global(|hit_counts: &mut HitCounts, _| {
+ *hit_counts.0.entry(command.name).or_default() += 1;
+ });
+ let action = command.action;
+ cx.focus(&self.previous_focus_handle);
+ cx.dispatch_action(action);
+ self.dismissed(cx);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Self::ListItem {
+ let colors = cx.theme().colors();
+ let Some(command) = self
+ .matches
+ .get(ix)
+ .and_then(|m| self.commands.get(m.candidate_id))
+ else {
+ return div();
+ };
+
+ 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(Label::new(command.name.clone()))
+ }
+
+ // fn render_match(
+ // &self,
+ // ix: usize,
+ // mouse_state: &mut MouseState,
+ // selected: bool,
+ // cx: &gpui::AppContext,
+ // ) -> AnyElement<Picker<Self>> {
+ // let mat = &self.matches[ix];
+ // let command = &self.actions[mat.candidate_id];
+ // let theme = theme::current(cx);
+ // let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+ // let key_style = &theme.command_palette.key.in_state(selected);
+ // let keystroke_spacing = theme.command_palette.keystroke_spacing;
+
+ // Flex::row()
+ // .with_child(
+ // Label::new(mat.string.clone(), style.label.clone())
+ // .with_highlights(mat.positions.clone()),
+ // )
+ // .with_children(command.keystrokes.iter().map(|keystroke| {
+ // Flex::row()
+ // .with_children(
+ // [
+ // (keystroke.ctrl, "^"),
+ // (keystroke.alt, "⌥"),
+ // (keystroke.cmd, "⌘"),
+ // (keystroke.shift, "⇧"),
+ // ]
+ // .into_iter()
+ // .filter_map(|(modifier, label)| {
+ // if modifier {
+ // Some(
+ // Label::new(label, key_style.label.clone())
+ // .contained()
+ // .with_style(key_style.container),
+ // )
+ // } else {
+ // None
+ // }
+ // }),
+ // )
+ // .with_child(
+ // Label::new(keystroke.key.clone(), key_style.label.clone())
+ // .contained()
+ // .with_style(key_style.container),
+ // )
+ // .contained()
+ // .with_margin_left(keystroke_spacing)
+ // .flex_float()
+ // }))
+ // .contained()
+ // .with_style(style.container)
+ // .into_any()
+ // }
+}
+
+fn humanize_action_name(name: &str) -> String {
+ let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
+ let mut result = String::with_capacity(capacity);
+ for char in name.chars() {
+ if char == ':' {
+ if result.ends_with(':') {
+ result.push(' ');
+ } else {
+ result.push(':');
+ }
+ } else if char == '_' {
+ result.push(' ');
+ } else if char.is_uppercase() {
+ if !result.ends_with(' ') {
+ result.push(' ');
+ }
+ result.extend(char.to_lowercase());
+ } else {
+ result.push(char);
+ }
+ }
+ result
+}
+
+impl std::fmt::Debug for Command {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Command")
+ .field("name", &self.name)
+ .field("keystrokes", &self.keystrokes)
+ .finish()
+ }
+}
+
+// #[cfg(test)]
+// mod tests {
+// use std::sync::Arc;
+
+// use super::*;
+// use editor::Editor;
+// use gpui::{executor::Deterministic, TestAppContext};
+// use project::Project;
+// use workspace::{AppState, Workspace};
+
+// #[test]
+// fn test_humanize_action_name() {
+// assert_eq!(
+// humanize_action_name("editor::GoToDefinition"),
+// "editor: go to definition"
+// );
+// assert_eq!(
+// humanize_action_name("editor::Backspace"),
+// "editor: backspace"
+// );
+// assert_eq!(
+// humanize_action_name("go_to_line::Deploy"),
+// "go to line: deploy"
+// );
+// }
+
+// #[gpui::test]
+// async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+
+// let project = Project::test(app_state.fs.clone(), [], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+// let workspace = window.root(cx);
+// let editor = window.add_view(cx, |cx| {
+// let mut editor = Editor::single_line(None, cx);
+// editor.set_text("abc", cx);
+// editor
+// });
+
+// workspace.update(cx, |workspace, cx| {
+// cx.focus(&editor);
+// workspace.add_item(Box::new(editor.clone()), cx)
+// });
+
+// workspace.update(cx, |workspace, cx| {
+// toggle_command_palette(workspace, &Toggle, cx);
+// });
+
+// let palette = workspace.read_with(cx, |workspace, _| {
+// workspace.modal::<CommandPalette>().unwrap()
+// });
+
+// palette
+// .update(cx, |palette, cx| {
+// // Fill up palette's command list by running an empty query;
+// // we only need it to subsequently assert that the palette is initially
+// // sorted by command's name.
+// palette.delegate_mut().update_matches("".to_string(), cx)
+// })
+// .await;
+
+// palette.update(cx, |palette, _| {
+// let is_sorted =
+// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
+// assert!(is_sorted(&palette.delegate().actions));
+// });
+
+// palette
+// .update(cx, |palette, cx| {
+// palette
+// .delegate_mut()
+// .update_matches("bcksp".to_string(), cx)
+// })
+// .await;
+
+// palette.update(cx, |palette, cx| {
+// assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
+// palette.confirm(&Default::default(), cx);
+// });
+// deterministic.run_until_parked();
+// editor.read_with(cx, |editor, cx| {
+// assert_eq!(editor.text(cx), "ab");
+// });
+
+// // Add namespace filter, and redeploy the palette
+// cx.update(|cx| {
+// cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
+// filter.filtered_namespaces.insert("editor");
+// })
+// });
+
+// workspace.update(cx, |workspace, cx| {
+// toggle_command_palette(workspace, &Toggle, cx);
+// });
+
+// // Assert editor command not present
+// let palette = workspace.read_with(cx, |workspace, _| {
+// workspace.modal::<CommandPalette>().unwrap()
+// });
+
+// palette
+// .update(cx, |palette, cx| {
+// palette
+// .delegate_mut()
+// .update_matches("bcksp".to_string(), cx)
+// })
+// .await;
+
+// palette.update(cx, |palette, _| {
+// assert!(palette.delegate().matches.is_empty())
+// });
+// }
+
+// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+// cx.update(|cx| {
+// let app_state = AppState::test(cx);
+// theme::init(cx);
+// language::init(cx);
+// editor::init(cx);
+// workspace::init(app_state.clone(), cx);
+// init(cx);
+// Project::init_settings(cx);
+// app_state
+// })
+// }
+// }
@@ -8,25 +8,12 @@ use text::{Bias, Point};
use theme::ActiveTheme;
use ui::{h_stack, modal, v_stack, Label, LabelColor};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{ModalEvent, Workspace};
+use workspace::{Modal, ModalEvent, Workspace};
actions!(Toggle);
pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(
- |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
- workspace
- .modal_layer()
- .register_modal(Toggle, |workspace, cx| {
- let editor = workspace
- .active_item(cx)
- .and_then(|active_item| active_item.downcast::<Editor>())?;
-
- Some(cx.build_view(|cx| GoToLine::new(editor, cx)))
- });
- },
- )
- .detach();
+ cx.observe_new_views(GoToLine::register).detach();
}
pub struct GoToLine {
@@ -38,14 +25,28 @@ pub struct GoToLine {
}
impl EventEmitter<ModalEvent> for GoToLine {}
+impl Modal for GoToLine {
+ fn focus(&self, cx: &mut WindowContext) {
+ self.line_editor.update(cx, |editor, cx| editor.focus(cx))
+ }
+}
impl GoToLine {
- pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
- let line_editor = cx.build_view(|cx| {
- let editor = Editor::single_line(cx);
- editor.focus(cx);
- editor
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(|workspace, _: &Toggle, cx| {
+ let Some(editor) = workspace
+ .active_item(cx)
+ .and_then(|active_item| active_item.downcast::<Editor>())
+ else {
+ return;
+ };
+
+ workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
});
+ }
+
+ pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
+ let line_editor = cx.build_view(|cx| Editor::single_line(cx));
let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
let editor = active_editor.read(cx);
@@ -123,10 +124,6 @@ impl GoToLine {
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- self.active_editor.update(cx, |editor, cx| {
- editor.focus(cx);
- cx.notify();
- });
cx.emit(ModalEvent::Dismissed);
}
@@ -4,7 +4,7 @@ use collections::{HashMap, HashSet};
use lazy_static::lazy_static;
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use serde::Deserialize;
-use std::any::{type_name, Any};
+use std::any::{type_name, Any, TypeId};
/// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and
@@ -100,6 +100,21 @@ where
}
}
+impl dyn Action {
+ pub fn type_id(&self) -> TypeId {
+ self.as_any().type_id()
+ }
+
+ pub fn name(&self) -> SharedString {
+ ACTION_REGISTRY
+ .read()
+ .names_by_type_id
+ .get(&self.type_id())
+ .expect("type is not a registered action")
+ .clone()
+ }
+}
+
type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
lazy_static! {
@@ -109,6 +124,7 @@ lazy_static! {
#[derive(Default)]
struct ActionRegistry {
builders_by_name: HashMap<SharedString, ActionBuilder>,
+ names_by_type_id: HashMap<TypeId, SharedString>,
all_names: Vec<SharedString>, // So we can return a static slice.
}
@@ -117,9 +133,24 @@ pub fn register_action<A: Action>() {
let name = A::qualified_name();
let mut lock = ACTION_REGISTRY.write();
lock.builders_by_name.insert(name.clone(), A::build);
+ lock.names_by_type_id
+ .insert(TypeId::of::<A>(), name.clone());
lock.all_names.push(name);
}
+/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
+ let lock = ACTION_REGISTRY.read();
+ let name = lock
+ .names_by_type_id
+ .get(type_id)
+ .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+ .clone();
+ drop(lock);
+
+ build_action(&name, None)
+}
+
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
let lock = ACTION_REGISTRY.read();
@@ -1,6 +1,6 @@
use crate::{
- point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId,
- ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size,
+ point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
+ ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size,
StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity,
StyleRefinement, Styled, ViewContext,
};
@@ -9,6 +9,9 @@ use smallvec::SmallVec;
use std::{cmp, ops::Range, sync::Arc};
use taffy::style::Overflow;
+/// uniform_list provides lazy rendering for a set of items that are of uniform height.
+/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
+/// uniform_list will only render the visibile subset of items.
pub fn uniform_list<Id, V, C>(
id: Id,
item_count: usize,
@@ -20,9 +23,12 @@ where
C: Component<V>,
{
let id = id.into();
+ let mut style = StyleRefinement::default();
+ style.overflow.y = Some(Overflow::Hidden);
+
UniformList {
id: id.clone(),
- style: Default::default(),
+ style,
item_count,
render_items: Box::new(move |view, visible_range, cx| {
f(view, visible_range, cx)
@@ -86,8 +92,14 @@ impl<V: 'static> Styled for UniformList<V> {
}
}
+#[derive(Default)]
+pub struct UniformListState {
+ interactive: InteractiveElementState,
+ item_size: Size<Pixels>,
+}
+
impl<V: 'static> Element<V> for UniformList<V> {
- type ElementState = InteractiveElementState;
+ type ElementState = UniformListState;
fn id(&self) -> Option<crate::ElementId> {
Some(self.id.clone())
@@ -95,20 +107,47 @@ impl<V: 'static> Element<V> for UniformList<V> {
fn initialize(
&mut self,
- _: &mut V,
+ view_state: &mut V,
element_state: Option<Self::ElementState>,
- _: &mut ViewContext<V>,
+ cx: &mut ViewContext<V>,
) -> Self::ElementState {
- element_state.unwrap_or_default()
+ element_state.unwrap_or_else(|| {
+ let item_size = self.measure_first_item(view_state, None, cx);
+ UniformListState {
+ interactive: InteractiveElementState::default(),
+ item_size,
+ }
+ })
}
fn layout(
&mut self,
_view_state: &mut V,
- _element_state: &mut Self::ElementState,
+ element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>,
) -> LayoutId {
- cx.request_layout(&self.computed_style(), None)
+ let max_items = self.item_count;
+ let item_size = element_state.item_size;
+ let rem_size = cx.rem_size();
+
+ cx.request_measured_layout(
+ self.computed_style(),
+ rem_size,
+ move |known_dimensions: Size<Option<Pixels>>, available_space: Size<AvailableSpace>| {
+ let desired_height = item_size.height * max_items;
+ let width = known_dimensions
+ .width
+ .unwrap_or(match available_space.width {
+ AvailableSpace::Definite(x) => x,
+ AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
+ });
+ let height = match available_space.height {
+ AvailableSpace::Definite(x) => desired_height.min(x),
+ AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
+ };
+ size(width, height)
+ },
+ )
}
fn paint(
@@ -133,12 +172,14 @@ impl<V: 'static> Element<V> for UniformList<V> {
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 item_height = self
+ .measure_first_item(view_state, Some(padded_bounds.size.width), cx)
+ .height;
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.lock().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
- scroll_offset: element_state.track_scroll_offset(),
+ scroll_offset: element_state.interactive.track_scroll_offset(),
});
}
let visible_item_count = if item_height > px(0.) {
@@ -147,6 +188,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
0
};
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;
@@ -190,20 +232,25 @@ impl<V: 'static> Element<V> for UniformList<V> {
let overflow = point(style.overflow.x, Overflow::Scroll);
cx.with_z_index(0, |cx| {
- self.interactivity
- .paint(bounds, content_size, overflow, element_state, cx);
+ self.interactivity.paint(
+ bounds,
+ content_size,
+ overflow,
+ &mut element_state.interactive,
+ cx,
+ );
});
})
}
}
impl<V> UniformList<V> {
- fn measure_item_height(
+ fn measure_first_item(
&self,
view_state: &mut V,
- list_bounds: Bounds<Pixels>,
+ list_width: Option<Pixels>,
cx: &mut ViewContext<V>,
- ) -> Pixels {
+ ) -> Size<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();
@@ -212,11 +259,13 @@ impl<V> UniformList<V> {
cx.compute_layout(
layout_id,
Size {
- width: AvailableSpace::Definite(list_bounds.size.width),
+ width: list_width.map_or(AvailableSpace::MinContent, |width| {
+ AvailableSpace::Definite(width)
+ }),
height: AvailableSpace::MinContent,
},
);
- cx.layout_bounds(layout_id).size.height
+ cx.layout_bounds(layout_id).size
}
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
@@ -94,7 +94,6 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
fn on_mouse_down_out(
mut self,
- button: MouseButton,
handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
) -> Self
where
@@ -103,10 +102,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
self.stateless_interactivity()
.mouse_down_listeners
.push(Box::new(move |view, event, bounds, phase, cx| {
- if phase == DispatchPhase::Capture
- && event.button == button
- && !bounds.contains_point(&event.position)
- {
+ if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
handler(view, event, cx)
}
}));
@@ -184,6 +184,10 @@ impl AnyView {
.compute_layout(layout_id, available_space);
(self.paint)(self, &mut rendered_element, cx);
}
+
+ pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) {
+ (self.initialize)(self, cx);
+ }
}
impl<V: 'static> Component<V> for AnyView {
@@ -1,14 +1,15 @@
use crate::{
- px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
- Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId,
- Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId,
- GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch,
- KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton,
- MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay,
- PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render,
- RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow,
- SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline,
- UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+ build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
+ AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
+ DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
+ FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent,
+ IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers,
+ MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
+ PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite,
+ PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
+ SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
+ TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
+ WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Result};
use collections::HashMap;
@@ -145,6 +146,11 @@ impl FocusHandle {
}
}
+ /// Moves the focus to the element associated with this handle.
+ pub fn focus(&self, cx: &mut WindowContext) {
+ cx.focus(self)
+ }
+
/// Obtains whether the element associated with this handle is currently focused.
pub fn is_focused(&self, cx: &WindowContext) -> bool {
self.id.is_focused(cx)
@@ -227,7 +233,7 @@ pub(crate) struct Frame {
key_matchers: HashMap<GlobalElementId, KeyMatcher>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
pub(crate) focus_listeners: Vec<AnyFocusListener>,
- key_dispatch_stack: Vec<KeyDispatchStackFrame>,
+ pub(crate) key_dispatch_stack: Vec<KeyDispatchStackFrame>,
freeze_key_dispatch_stack: bool,
focus_parents_by_child: HashMap<FocusId, FocusId>,
pub(crate) scene_builder: SceneBuilder,
@@ -326,7 +332,7 @@ impl Window {
/// find the focused element. We interleave key listeners with dispatch contexts so we can use the
/// contexts when matching key events against the keymap. A key listener can be either an action
/// handler or a [KeyDown] / [KeyUp] event listener.
-enum KeyDispatchStackFrame {
+pub(crate) enum KeyDispatchStackFrame {
Listener {
event_type: TypeId,
listener: AnyKeyListener,
@@ -401,11 +407,18 @@ impl<'a> WindowContext<'a> {
/// Move focus to the element associated with the given `FocusHandle`.
pub fn focus(&mut self, handle: &FocusHandle) {
+ if self.window.focus == Some(handle.id) {
+ return;
+ }
+
if self.window.last_blur.is_none() {
self.window.last_blur = Some(self.window.focus);
}
self.window.focus = Some(handle.id);
+
+ // self.window.current_frame.key_dispatch_stack.clear()
+ // self.window.root_view.initialize()
self.app.push_effect(Effect::FocusChanged {
window_handle: self.window.handle,
focused: Some(handle.id),
@@ -427,6 +440,14 @@ impl<'a> WindowContext<'a> {
self.notify();
}
+ pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
+ self.defer(|cx| {
+ cx.app.propagate_event = true;
+ let stack = cx.dispatch_stack();
+ cx.dispatch_action_internal(action, &stack[..])
+ })
+ }
+
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
/// that are currently on the stack to be returned to the app.
pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
@@ -1054,6 +1075,26 @@ impl<'a> WindowContext<'a> {
self.window.dirty = false;
}
+ pub(crate) fn dispatch_stack(&mut self) -> Vec<KeyDispatchStackFrame> {
+ let root_view = self.window.root_view.take().unwrap();
+ let window = &mut *self.window;
+ let mut spare_frame = Frame::default();
+ mem::swap(&mut spare_frame, &mut window.previous_frame);
+
+ self.start_frame();
+
+ root_view.draw_dispatch_stack(self);
+
+ let window = &mut *self.window;
+ // restore the old values of current and previous frame,
+ // putting the new frame into spare_frame.
+ mem::swap(&mut window.current_frame, &mut window.previous_frame);
+ mem::swap(&mut spare_frame, &mut window.previous_frame);
+ self.window.root_view = Some(root_view);
+
+ spare_frame.key_dispatch_stack
+ }
+
/// Rotate the current frame and the previous frame, then clear the current frame.
/// We repopulate all state in the current frame during each paint.
fn start_frame(&mut self) {
@@ -1196,7 +1237,7 @@ impl<'a> WindowContext<'a> {
DispatchPhase::Capture,
self,
) {
- self.dispatch_action(action, &key_dispatch_stack[..ix]);
+ self.dispatch_action_internal(action, &key_dispatch_stack[..ix]);
}
if !self.app.propagate_event {
break;
@@ -1223,7 +1264,10 @@ impl<'a> WindowContext<'a> {
DispatchPhase::Bubble,
self,
) {
- self.dispatch_action(action, &key_dispatch_stack[..ix]);
+ self.dispatch_action_internal(
+ action,
+ &key_dispatch_stack[..ix],
+ );
}
if !self.app.propagate_event {
@@ -1295,7 +1339,29 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.prompt(level, msg, answers)
}
- fn dispatch_action(
+ pub fn available_actions(&self) -> impl Iterator<Item = Box<dyn Action>> + '_ {
+ let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack;
+ key_dispatch_stack.iter().filter_map(|frame| {
+ match frame {
+ // todo!factor out a KeyDispatchStackFrame::Action
+ KeyDispatchStackFrame::Listener {
+ event_type,
+ listener: _,
+ } => {
+ match build_action_from_type(event_type) {
+ Ok(action) => Some(action),
+ Err(err) => {
+ dbg!(err);
+ None
+ } // we'll hit his if TypeId == KeyDown
+ }
+ }
+ KeyDispatchStackFrame::Context(_) => None,
+ }
+ })
+ }
+
+ pub(crate) fn dispatch_action_internal(
&mut self,
action: Box<dyn Action>,
dispatch_stack: &[KeyDispatchStackFrame],
@@ -10,6 +10,7 @@ doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
+ui = { package = "ui2", path = "../ui2" }
gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" }
settings = { package = "settings2", path = "../settings2" }
@@ -5,6 +5,8 @@ use gpui::{
WindowContext,
};
use std::cmp;
+use theme::ActiveTheme;
+use ui::v_stack;
pub struct Picker<D: PickerDelegate> {
pub delegate: D,
@@ -57,6 +59,7 @@ impl<D: PickerDelegate> Picker<D> {
let ix = cmp::min(index + 1, count - 1);
self.delegate.set_selected_index(ix, cx);
self.scroll_handle.scroll_to_item(ix);
+ cx.notify();
}
}
@@ -67,6 +70,7 @@ impl<D: PickerDelegate> Picker<D> {
let ix = index.saturating_sub(1);
self.delegate.set_selected_index(ix, cx);
self.scroll_handle.scroll_to_item(ix);
+ cx.notify();
}
}
@@ -75,6 +79,7 @@ impl<D: PickerDelegate> Picker<D> {
if count > 0 {
self.delegate.set_selected_index(0, cx);
self.scroll_handle.scroll_to_item(0);
+ cx.notify();
}
}
@@ -83,6 +88,7 @@ impl<D: PickerDelegate> Picker<D> {
if count > 0 {
self.delegate.set_selected_index(count - 1, cx);
self.scroll_handle.scroll_to_item(count - 1);
+ cx.notify();
}
}
@@ -133,7 +139,7 @@ impl<D: PickerDelegate> Picker<D> {
impl<D: PickerDelegate> Render for Picker<D> {
type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
.context("picker")
.id("picker-container")
@@ -146,18 +152,38 @@ impl<D: PickerDelegate> Render for Picker<D> {
.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(),
+ v_stack().gap_px().child(
+ v_stack()
+ .py_0p5()
+ .px_1()
+ .child(div().px_2().py_0p5().child(self.editor.clone())),
+ ),
+ )
+ .child(
+ div()
+ .h_px()
+ .w_full()
+ .bg(cx.theme().colors().element_background),
+ )
+ .child(
+ v_stack()
+ .py_0p5()
+ .px_1()
+ .grow()
+ .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()),
+ )
+ .max_h_72()
+ .overflow_hidden(),
)
}
}
@@ -1,15 +1,22 @@
-use crate::Workspace;
use gpui::{
- div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatelessInteractive,
- Styled, Subscription, View, ViewContext,
+ div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive,
+ Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
};
-use std::{any::TypeId, sync::Arc};
use ui::v_stack;
+pub struct ActiveModal {
+ modal: AnyView,
+ subscription: Subscription,
+ previous_focus_handle: Option<FocusHandle>,
+ focus_handle: FocusHandle,
+}
+
pub struct ModalLayer {
- open_modal: Option<AnyView>,
- subscription: Option<Subscription>,
- registered_modals: Vec<(TypeId, Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>)>,
+ active_modal: Option<ActiveModal>,
+}
+
+pub trait Modal: Render + EventEmitter<ModalEvent> {
+ fn focus(&self, cx: &mut WindowContext);
}
pub enum ModalEvent {
@@ -18,98 +25,82 @@ pub enum ModalEvent {
impl ModalLayer {
pub fn new() -> Self {
- Self {
- open_modal: None,
- subscription: None,
- registered_modals: Vec::new(),
- }
+ Self { active_modal: None }
}
- pub fn register_modal<A: 'static, V, B>(&mut self, action: A, build_view: B)
+ pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
where
- V: EventEmitter<ModalEvent> + Render,
- B: Fn(&mut Workspace, &mut ViewContext<Workspace>) -> Option<View<V>> + 'static,
+ V: Modal,
+ B: FnOnce(&mut ViewContext<V>) -> V,
{
- let build_view = Arc::new(build_view);
-
- self.registered_modals.push((
- TypeId::of::<A>(),
- Box::new(move |mut div| {
- let build_view = build_view.clone();
-
- div.on_action(move |workspace, event: &A, cx| {
- let Some(new_modal) = (build_view)(workspace, cx) else {
- return;
- };
- workspace.modal_layer().show_modal(new_modal, cx);
- })
- }),
- ));
+ let previous_focus = cx.focused();
+
+ if let Some(active_modal) = &self.active_modal {
+ let is_close = active_modal.modal.clone().downcast::<V>().is_ok();
+ self.hide_modal(cx);
+ if is_close {
+ return;
+ }
+ }
+ let new_modal = cx.build_view(build_view);
+ self.show_modal(new_modal, cx);
}
- pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Workspace>)
+ pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
where
- V: EventEmitter<ModalEvent> + Render,
+ V: Modal,
{
- self.subscription = Some(cx.subscribe(&new_modal, |this, modal, e, cx| match e {
- ModalEvent::Dismissed => this.modal_layer().hide_modal(cx),
- }));
- self.open_modal = Some(new_modal.into());
- cx.notify();
- }
-
- pub fn hide_modal(&mut self, cx: &mut ViewContext<Workspace>) {
- self.open_modal.take();
- self.subscription.take();
+ self.active_modal = Some(ActiveModal {
+ modal: new_modal.clone().into(),
+ subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e {
+ ModalEvent::Dismissed => this.hide_modal(cx),
+ }),
+ previous_focus_handle: cx.focused(),
+ focus_handle: cx.focus_handle(),
+ });
+ new_modal.update(cx, |modal, cx| modal.focus(cx));
cx.notify();
}
- pub fn wrapper_element(&self, cx: &ViewContext<Workspace>) -> Div<Workspace> {
- let mut parent = div().relative().size_full();
-
- for (_, action) in self.registered_modals.iter() {
- parent = (action)(parent);
+ pub fn hide_modal(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(active_modal) = self.active_modal.take() {
+ if let Some(previous_focus) = active_modal.previous_focus_handle {
+ if active_modal.focus_handle.contains_focused(cx) {
+ previous_focus.focus(cx);
+ }
+ }
}
- parent.when_some(self.open_modal.as_ref(), |parent, open_modal| {
- let container1 = div()
- .absolute()
- .flex()
- .flex_col()
- .items_center()
- .size_full()
- .top_0()
- .left_0()
- .z_index(400);
-
- // transparent layer
- let container2 = v_stack().h(px(0.0)).relative().top_20();
-
- parent.child(container1.child(container2.child(open_modal.clone())))
- })
+ cx.notify();
}
}
-// impl Render for ModalLayer {
-// type Element = Div<Self>;
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-// let mut div = div();
-// for (type_id, build_view) in cx.global::<ModalRegistry>().registered_modals {
-// div = div.useful_on_action(
-// type_id,
-// Box::new(|this, _: dyn Any, phase, cx: &mut ViewContext<Self>| {
-// if phase == DispatchPhase::Capture {
-// return;
-// }
-// self.workspace.update(cx, |workspace, cx| {
-// self.open_modal = Some(build_view(workspace, cx));
-// });
-// cx.notify();
-// }),
-// )
-// }
-
-// div
-// }
-// }
+impl Render for ModalLayer {
+ type Element = Div<Self>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let Some(active_modal) = &self.active_modal else {
+ return div();
+ };
+
+ div()
+ .absolute()
+ .flex()
+ .flex_col()
+ .items_center()
+ .size_full()
+ .top_0()
+ .left_0()
+ .z_index(400)
+ .child(
+ v_stack()
+ .h(px(0.0))
+ .top_20()
+ .track_focus(&active_modal.focus_handle)
+ .on_mouse_down_out(|this: &mut Self, event, cx| {
+ this.hide_modal(cx);
+ })
+ .child(active_modal.modal.clone()),
+ )
+ }
+}
@@ -36,11 +36,12 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
- actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
- AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle,
- GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive,
- Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds,
- WindowContext, WindowHandle, WindowOptions,
+ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
+ AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId,
+ EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render,
+ Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription,
+ Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
+ WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -530,6 +531,13 @@ pub enum Event {
pub struct Workspace {
weak_self: WeakView<Self>,
focus_handle: FocusHandle,
+ workspace_actions: Vec<
+ Box<
+ dyn Fn(
+ Div<Workspace, StatefulInteractivity<Workspace>>,
+ ) -> Div<Workspace, StatefulInteractivity<Workspace>>,
+ >,
+ >,
zoomed: Option<AnyWeakView>,
zoomed_position: Option<DockPosition>,
center: PaneGroup,
@@ -542,7 +550,7 @@ pub struct Workspace {
last_active_center_pane: Option<WeakView<Pane>>,
last_active_view_id: Option<proto::ViewId>,
status_bar: View<StatusBar>,
- modal_layer: ModalLayer,
+ modal_layer: View<ModalLayer>,
// titlebar_item: Option<AnyViewHandle>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
@@ -694,7 +702,7 @@ impl Workspace {
});
let workspace_handle = cx.view().downgrade();
- let modal_layer = ModalLayer::new();
+ let modal_layer = cx.build_view(|cx| ModalLayer::new());
// todo!()
// cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
@@ -775,13 +783,10 @@ impl Workspace {
leader_updates_tx,
subscriptions,
pane_history_timestamp,
+ workspace_actions: Default::default(),
}
}
- pub fn modal_layer(&mut self) -> &mut ModalLayer {
- &mut self.modal_layer
- }
-
fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
@@ -3495,6 +3500,35 @@ impl Workspace {
// )
// }
// }
+ pub fn register_action<A: Action>(
+ &mut self,
+ callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
+ ) {
+ let callback = Arc::new(callback);
+
+ self.workspace_actions.push(Box::new(move |div| {
+ let callback = callback.clone();
+ div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx))
+ }));
+ }
+
+ fn add_workspace_actions_listeners(
+ &self,
+ mut div: Div<Workspace, StatefulInteractivity<Workspace>>,
+ ) -> Div<Workspace, StatefulInteractivity<Workspace>> {
+ for action in self.workspace_actions.iter() {
+ div = (action)(div)
+ }
+ div
+ }
+
+ pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
+ where
+ B: FnOnce(&mut ViewContext<V>) -> V,
+ {
+ self.modal_layer
+ .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
+ }
}
fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
@@ -3709,157 +3743,160 @@ impl Render for Workspace {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- div()
- .relative()
- .size_full()
- .flex()
- .flex_col()
- .font("Zed Sans")
- .gap_0()
- .justify_start()
- .items_start()
- .text_color(cx.theme().colors().text)
- .bg(cx.theme().colors().background)
- .child(self.render_titlebar(cx))
- .child(
- // todo! should this be a component a view?
- self.modal_layer
- .wrapper_element(cx)
- .relative()
- .flex_1()
- .w_full()
- .flex()
- .overflow_hidden()
- .border_t()
- .border_b()
- .border_color(cx.theme().colors().border)
- // .children(
- // Some(
- // Panel::new("project-panel-outer", cx)
- // .side(PanelSide::Left)
- // .child(ProjectPanel::new("project-panel-inner")),
- // )
- // .filter(|_| self.is_project_panel_open()),
- // )
- // .children(
- // Some(
- // Panel::new("collab-panel-outer", cx)
- // .child(CollabPanel::new("collab-panel-inner"))
- // .side(PanelSide::Left),
- // )
- // .filter(|_| self.is_collab_panel_open()),
- // )
- // .child(NotificationToast::new(
- // "maxbrunsfeld has requested to add you as a contact.".into(),
- // ))
- .child(
- div().flex().flex_col().flex_1().h_full().child(
- div().flex().flex_1().child(self.center.render(
- &self.project,
- &self.follower_states,
- self.active_call(),
- &self.active_pane,
- self.zoomed.as_ref(),
- &self.app_state,
- cx,
- )),
+ let mut context = DispatchContext::default();
+ context.insert("Workspace");
+ cx.with_key_dispatch_context(context, |cx| {
+ div()
+ .relative()
+ .size_full()
+ .flex()
+ .flex_col()
+ .font("Zed Sans")
+ .gap_0()
+ .justify_start()
+ .items_start()
+ .text_color(cx.theme().colors().text)
+ .bg(cx.theme().colors().background)
+ .child(self.render_titlebar(cx))
+ .child(
+ // todo! should this be a component a view?
+ self.add_workspace_actions_listeners(div().id("workspace"))
+ .relative()
+ .flex_1()
+ .w_full()
+ .flex()
+ .overflow_hidden()
+ .border_t()
+ .border_b()
+ .border_color(cx.theme().colors().border)
+ .child(self.modal_layer.clone())
+ // .children(
+ // Some(
+ // Panel::new("project-panel-outer", cx)
+ // .side(PanelSide::Left)
+ // .child(ProjectPanel::new("project-panel-inner")),
+ // )
+ // .filter(|_| self.is_project_panel_open()),
+ // )
+ // .children(
+ // Some(
+ // Panel::new("collab-panel-outer", cx)
+ // .child(CollabPanel::new("collab-panel-inner"))
+ // .side(PanelSide::Left),
+ // )
+ // .filter(|_| self.is_collab_panel_open()),
+ // )
+ // .child(NotificationToast::new(
+ // "maxbrunsfeld has requested to add you as a contact.".into(),
+ // ))
+ .child(
+ div().flex().flex_col().flex_1().h_full().child(
+ div().flex().flex_1().child(self.center.render(
+ &self.project,
+ &self.follower_states,
+ self.active_call(),
+ &self.active_pane,
+ self.zoomed.as_ref(),
+ &self.app_state,
+ cx,
+ )),
+ ), // .children(
+ // Some(
+ // Panel::new("terminal-panel", cx)
+ // .child(Terminal::new())
+ // .allowed_sides(PanelAllowedSides::BottomOnly)
+ // .side(PanelSide::Bottom),
+ // )
+ // .filter(|_| self.is_terminal_open()),
+ // ),
), // .children(
// Some(
- // Panel::new("terminal-panel", cx)
- // .child(Terminal::new())
- // .allowed_sides(PanelAllowedSides::BottomOnly)
- // .side(PanelSide::Bottom),
+ // Panel::new("chat-panel-outer", cx)
+ // .side(PanelSide::Right)
+ // .child(ChatPanel::new("chat-panel-inner").messages(vec![
+ // ChatMessage::new(
+ // "osiewicz".to_string(),
+ // "is this thing on?".to_string(),
+ // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+ // .unwrap()
+ // .naive_local(),
+ // ),
+ // ChatMessage::new(
+ // "maxdeviant".to_string(),
+ // "Reading you loud and clear!".to_string(),
+ // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+ // .unwrap()
+ // .naive_local(),
+ // ),
+ // ])),
+ // )
+ // .filter(|_| self.is_chat_panel_open()),
+ // )
+ // .children(
+ // Some(
+ // Panel::new("notifications-panel-outer", cx)
+ // .side(PanelSide::Right)
+ // .child(NotificationsPanel::new("notifications-panel-inner")),
+ // )
+ // .filter(|_| self.is_notifications_panel_open()),
+ // )
+ // .children(
+ // Some(
+ // Panel::new("assistant-panel-outer", cx)
+ // .child(AssistantPanel::new("assistant-panel-inner")),
// )
- // .filter(|_| self.is_terminal_open()),
+ // .filter(|_| self.is_assistant_panel_open()),
// ),
- ), // .children(
- // Some(
- // Panel::new("chat-panel-outer", cx)
- // .side(PanelSide::Right)
- // .child(ChatPanel::new("chat-panel-inner").messages(vec![
- // ChatMessage::new(
- // "osiewicz".to_string(),
- // "is this thing on?".to_string(),
- // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
- // .unwrap()
- // .naive_local(),
- // ),
- // ChatMessage::new(
- // "maxdeviant".to_string(),
- // "Reading you loud and clear!".to_string(),
- // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
- // .unwrap()
- // .naive_local(),
- // ),
- // ])),
- // )
- // .filter(|_| self.is_chat_panel_open()),
- // )
- // .children(
- // Some(
- // Panel::new("notifications-panel-outer", cx)
- // .side(PanelSide::Right)
- // .child(NotificationsPanel::new("notifications-panel-inner")),
- // )
- // .filter(|_| self.is_notifications_panel_open()),
- // )
- // .children(
- // Some(
- // Panel::new("assistant-panel-outer", cx)
- // .child(AssistantPanel::new("assistant-panel-inner")),
- // )
- // .filter(|_| self.is_assistant_panel_open()),
- // ),
- )
- .child(self.status_bar.clone())
- // .when(self.debug.show_toast, |this| {
- // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
- // })
- // .children(
- // Some(
- // div()
- // .absolute()
- // .top(px(50.))
- // .left(px(640.))
- // .z_index(8)
- // .child(LanguageSelector::new("language-selector")),
- // )
- // .filter(|_| self.is_language_selector_open()),
- // )
- .z_index(8)
- // Debug
- .child(
- div()
- .flex()
- .flex_col()
- .z_index(9)
- .absolute()
- .top_20()
- .left_1_4()
- .w_40()
- .gap_2(), // .when(self.show_debug, |this| {
- // this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
- // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
- // ))
- // .child(
- // Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
- // |workspace, cx| workspace.debug_toggle_toast(cx),
- // )),
- // )
- // .child(
- // Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
- // |workspace, cx| workspace.debug_toggle_livestream(cx),
- // )),
- // )
- // })
- // .child(
- // Button::<Workspace>::new("Toggle Debug")
- // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
- // ),
- )
+ )
+ .child(self.status_bar.clone())
+ // .when(self.debug.show_toast, |this| {
+ // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
+ // })
+ // .children(
+ // Some(
+ // div()
+ // .absolute()
+ // .top(px(50.))
+ // .left(px(640.))
+ // .z_index(8)
+ // .child(LanguageSelector::new("language-selector")),
+ // )
+ // .filter(|_| self.is_language_selector_open()),
+ // )
+ .z_index(8)
+ // Debug
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .z_index(9)
+ .absolute()
+ .top_20()
+ .left_1_4()
+ .w_40()
+ .gap_2(), // .when(self.show_debug, |this| {
+ // this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
+ // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
+ // ))
+ // .child(
+ // Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
+ // |workspace, cx| workspace.debug_toggle_toast(cx),
+ // )),
+ // )
+ // .child(
+ // Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
+ // |workspace, cx| workspace.debug_toggle_livestream(cx),
+ // )),
+ // )
+ // })
+ // .child(
+ // Button::<Workspace>::new("Toggle Debug")
+ // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
+ // ),
+ )
+ })
}
}
-
// todo!()
// impl Entity for Workspace {
// type Event = Event;
@@ -25,7 +25,7 @@ call = { package = "call2", path = "../call2" }
cli = { path = "../cli" }
# collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
-# command_palette = { path = "../command_palette" }
+command_palette = { package="command_palette2", path = "../command_palette2" }
# component_test = { path = "../component_test" }
# context_menu = { path = "../context_menu" }
client = { package = "client2", path = "../client2" }
@@ -74,7 +74,7 @@ util = { path = "../util" }
# vim = { path = "../vim" }
workspace = { package = "workspace2", path = "../workspace2" }
# welcome = { path = "../welcome" }
-# zed-actions = {path = "../zed-actions"}
+zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
anyhow.workspace = true
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
@@ -142,7 +142,7 @@ fn main() {
// context_menu::init(cx);
project::Project::init(&client, cx);
client::init(&client, cx);
- // command_palette::init(cx);
+ command_palette::init(cx);
language::init(cx);
editor::init(cx);
copilot::init(
@@ -761,7 +761,7 @@ fn load_embedded_fonts(cx: &AppContext) {
// #[cfg(not(debug_assertions))]
// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
// None
-// }
+//
// #[cfg(not(debug_assertions))]
// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
@@ -0,0 +1,11 @@
+[package]
+name = "zed_actions2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+gpui = { package = "gpui2", path = "../gpui2" }
+serde.workspace = true
@@ -0,0 +1,34 @@
+use gpui::{action, actions};
+
+actions!(
+ About,
+ DebugElements,
+ DecreaseBufferFontSize,
+ Hide,
+ HideOthers,
+ IncreaseBufferFontSize,
+ Minimize,
+ OpenDefaultKeymap,
+ OpenDefaultSettings,
+ OpenKeymap,
+ OpenLicenses,
+ OpenLocalSettings,
+ OpenLog,
+ OpenSettings,
+ OpenTelemetryLog,
+ Quit,
+ ResetBufferFontSize,
+ ResetDatabase,
+ ShowAll,
+ ToggleFullScreen,
+ Zoom,
+);
+
+#[action]
+pub struct OpenBrowser {
+ pub url: String,
+}
+#[action]
+pub struct OpenZedURL {
+ pub url: String,
+}