Action release handlers (#8782)
This PR adds support for handling action releases — events that are fired when the user releases all the modifier keys that were part of an action-triggering shortcut. If the user holds modifiers and invokes several actions sequentially via shortcuts (same or different), only the last action is "released" when its modifier keys released. ~The following methods were added to `Div`:~ - ~`capture_action_release()`~ - ~`on_action_release()`~ - ~`on_boxed_action_release()`~ ~They work similarly to `capture_action()`, `on_action()` and `on_boxed_action()`.~ See the implementation details in [this comment](https://github.com/zed-industries/zed/pull/8782#issuecomment-2009154646). Release Notes: - Added a fast-switch mode to the file finder: hit `p` or `shift-p` while holding down `cmd` to select a file immediately. (#8258). Related Issues: - Implements #8757 - Implements #8258 - Part of #7653 Co-authored-by: @ConradIrwin
This commit is contained in:
parent
91ab95ec82
commit
5602c48136
9 changed files with 260 additions and 31 deletions
|
@ -15,6 +15,7 @@
|
||||||
"shift-f10": "menu::ShowContextMenu",
|
"shift-f10": "menu::ShowContextMenu",
|
||||||
"ctrl-enter": "menu::SecondaryConfirm",
|
"ctrl-enter": "menu::SecondaryConfirm",
|
||||||
"escape": "menu::Cancel",
|
"escape": "menu::Cancel",
|
||||||
|
"ctrl-escape": "menu::Cancel",
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"shift-enter": "menu::UseSelectedQuery",
|
"shift-enter": "menu::UseSelectedQuery",
|
||||||
"ctrl-shift-w": "workspace::CloseWindow",
|
"ctrl-shift-w": "workspace::CloseWindow",
|
||||||
|
@ -558,6 +559,10 @@
|
||||||
"escape": "chat_panel::CloseReplyPreview"
|
"escape": "chat_panel::CloseReplyPreview"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "FileFinder",
|
||||||
|
"bindings": { "ctrl-shift-p": "menu::SelectPrev" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"ctrl-enter": "menu::ShowContextMenu",
|
"ctrl-enter": "menu::ShowContextMenu",
|
||||||
"cmd-enter": "menu::SecondaryConfirm",
|
"cmd-enter": "menu::SecondaryConfirm",
|
||||||
"escape": "menu::Cancel",
|
"escape": "menu::Cancel",
|
||||||
|
"cmd-escape": "menu::Cancel",
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"shift-enter": "menu::UseSelectedQuery",
|
"shift-enter": "menu::UseSelectedQuery",
|
||||||
"cmd-shift-w": "workspace::CloseWindow",
|
"cmd-shift-w": "workspace::CloseWindow",
|
||||||
|
@ -597,6 +598,10 @@
|
||||||
"tab": "channel_modal::ToggleMode"
|
"tab": "channel_modal::ToggleMode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "FileFinder",
|
||||||
|
"bindings": { "cmd-shift-p": "menu::SelectPrev" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Terminal",
|
"context": "Terminal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -5,8 +5,9 @@ use collections::{HashMap, HashSet};
|
||||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View,
|
||||||
|
ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
@ -30,6 +31,7 @@ impl ModalView for FileFinder {}
|
||||||
|
|
||||||
pub struct FileFinder {
|
pub struct FileFinder {
|
||||||
picker: View<Picker<FileFinderDelegate>>,
|
picker: View<Picker<FileFinderDelegate>>,
|
||||||
|
init_modifiers: Option<Modifiers>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
@ -94,6 +96,23 @@ impl FileFinder {
|
||||||
fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
|
fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)),
|
picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)),
|
||||||
|
init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_modifiers_changed(
|
||||||
|
&mut self,
|
||||||
|
event: &ModifiersChangedEvent,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(init_modifiers) = self.init_modifiers else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.picker.read(cx).delegate.has_changed_selected_index {
|
||||||
|
if !event.modified() || !init_modifiers.is_subset_of(&event) {
|
||||||
|
self.init_modifiers = None;
|
||||||
|
cx.dispatch_action(menu::Confirm.boxed_clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,8 +126,12 @@ impl FocusableView for FileFinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for FileFinder {
|
impl Render for FileFinder {
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
v_flex()
|
||||||
|
.key_context("FileFinder")
|
||||||
|
.w(rems(34.))
|
||||||
|
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||||
|
.child(self.picker.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +146,7 @@ pub struct FileFinderDelegate {
|
||||||
currently_opened_path: Option<FoundPath>,
|
currently_opened_path: Option<FoundPath>,
|
||||||
matches: Matches,
|
matches: Matches,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
has_changed_selected_index: bool,
|
||||||
cancel_flag: Arc<AtomicBool>,
|
cancel_flag: Arc<AtomicBool>,
|
||||||
history_items: Vec<FoundPath>,
|
history_items: Vec<FoundPath>,
|
||||||
}
|
}
|
||||||
|
@ -376,6 +400,7 @@ impl FileFinderDelegate {
|
||||||
latest_search_query: None,
|
latest_search_query: None,
|
||||||
currently_opened_path,
|
currently_opened_path,
|
||||||
matches: Matches::default(),
|
matches: Matches::default(),
|
||||||
|
has_changed_selected_index: false,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||||
history_items,
|
history_items,
|
||||||
|
@ -683,6 +708,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.has_changed_selected_index = true;
|
||||||
self.selected_index = ix;
|
self.selected_index = ix;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -721,7 +747,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.selected_index = self.calculate_selected_index();
|
self.selected_index = 0;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -872,7 +872,6 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
// generate some history to select from
|
// generate some history to select from
|
||||||
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
||||||
cx.executor().run_until_parked();
|
|
||||||
open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
||||||
open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
|
open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
|
||||||
let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
||||||
|
@ -1125,12 +1124,12 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||||
let picker = open_file_picker(&workspace, cx);
|
let picker = open_file_picker(&workspace, cx);
|
||||||
picker.update(cx, |finder, _| {
|
picker.update(cx, |finder, _| {
|
||||||
assert_eq!(finder.delegate.matches.len(), 3);
|
assert_eq!(finder.delegate.matches.len(), 3);
|
||||||
assert_match_at_position(finder, 0, "main.rs");
|
assert_match_selection(finder, 0, "main.rs");
|
||||||
assert_match_selection(finder, 1, "lib.rs");
|
assert_match_at_position(finder, 1, "lib.rs");
|
||||||
assert_match_at_position(finder, 2, "bar.rs");
|
assert_match_at_position(finder, 2, "bar.rs");
|
||||||
});
|
});
|
||||||
|
|
||||||
// all files match, main.rs is still on top
|
// all files match, main.rs is still on top, but the second item is selected
|
||||||
picker
|
picker
|
||||||
.update(cx, |finder, cx| {
|
.update(cx, |finder, cx| {
|
||||||
finder.delegate.update_matches(".rs".to_string(), cx)
|
finder.delegate.update_matches(".rs".to_string(), cx)
|
||||||
|
@ -1173,8 +1172,8 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||||
.await;
|
.await;
|
||||||
picker.update(cx, |finder, _| {
|
picker.update(cx, |finder, _| {
|
||||||
assert_eq!(finder.delegate.matches.len(), 3);
|
assert_eq!(finder.delegate.matches.len(), 3);
|
||||||
assert_match_at_position(finder, 0, "main.rs");
|
assert_match_selection(finder, 0, "main.rs");
|
||||||
assert_match_selection(finder, 1, "lib.rs");
|
assert_match_at_position(finder, 1, "lib.rs");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1207,29 +1206,31 @@ async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
|
||||||
let picker = open_file_picker(&workspace, cx);
|
let picker = open_file_picker(&workspace, cx);
|
||||||
picker.update(cx, |finder, _| {
|
picker.update(cx, |finder, _| {
|
||||||
assert_eq!(finder.delegate.matches.len(), 3);
|
assert_eq!(finder.delegate.matches.len(), 3);
|
||||||
assert_match_at_position(finder, 0, "3.txt");
|
assert_match_selection(finder, 0, "3.txt");
|
||||||
assert_match_selection(finder, 1, "2.txt");
|
assert_match_at_position(finder, 1, "2.txt");
|
||||||
assert_match_at_position(finder, 2, "1.txt");
|
assert_match_at_position(finder, 2, "1.txt");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.dispatch_action(SelectNext);
|
||||||
cx.dispatch_action(Confirm); // Open 2.txt
|
cx.dispatch_action(Confirm); // Open 2.txt
|
||||||
|
|
||||||
let picker = open_file_picker(&workspace, cx);
|
let picker = open_file_picker(&workspace, cx);
|
||||||
picker.update(cx, |finder, _| {
|
picker.update(cx, |finder, _| {
|
||||||
assert_eq!(finder.delegate.matches.len(), 3);
|
assert_eq!(finder.delegate.matches.len(), 3);
|
||||||
assert_match_at_position(finder, 0, "2.txt");
|
assert_match_selection(finder, 0, "2.txt");
|
||||||
assert_match_selection(finder, 1, "3.txt");
|
assert_match_at_position(finder, 1, "3.txt");
|
||||||
assert_match_at_position(finder, 2, "1.txt");
|
assert_match_at_position(finder, 2, "1.txt");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.dispatch_action(SelectNext);
|
||||||
cx.dispatch_action(SelectNext);
|
cx.dispatch_action(SelectNext);
|
||||||
cx.dispatch_action(Confirm); // Open 1.txt
|
cx.dispatch_action(Confirm); // Open 1.txt
|
||||||
|
|
||||||
let picker = open_file_picker(&workspace, cx);
|
let picker = open_file_picker(&workspace, cx);
|
||||||
picker.update(cx, |finder, _| {
|
picker.update(cx, |finder, _| {
|
||||||
assert_eq!(finder.delegate.matches.len(), 3);
|
assert_eq!(finder.delegate.matches.len(), 3);
|
||||||
assert_match_at_position(finder, 0, "1.txt");
|
assert_match_selection(finder, 0, "1.txt");
|
||||||
assert_match_selection(finder, 1, "2.txt");
|
assert_match_at_position(finder, 1, "2.txt");
|
||||||
assert_match_at_position(finder, 2, "3.txt");
|
assert_match_at_position(finder, 2, "3.txt");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1469,6 +1470,98 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/test",
|
||||||
|
json!({
|
||||||
|
"1.txt": "// One",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
|
|
||||||
|
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||||
|
|
||||||
|
cx.simulate_modifiers_change(Modifiers::command());
|
||||||
|
open_file_picker(&workspace, cx);
|
||||||
|
|
||||||
|
cx.simulate_modifiers_change(Modifiers::none());
|
||||||
|
active_file_picker(&workspace, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/test",
|
||||||
|
json!({
|
||||||
|
"1.txt": "// One",
|
||||||
|
"2.txt": "// Two",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
|
|
||||||
|
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||||
|
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
|
||||||
|
|
||||||
|
cx.simulate_modifiers_change(Modifiers::command());
|
||||||
|
let picker = open_file_picker(&workspace, cx);
|
||||||
|
picker.update(cx, |finder, _| {
|
||||||
|
assert_eq!(finder.delegate.matches.len(), 2);
|
||||||
|
assert_match_selection(finder, 0, "2.txt");
|
||||||
|
assert_match_at_position(finder, 1, "1.txt");
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.dispatch_action(SelectNext);
|
||||||
|
cx.simulate_modifiers_change(Modifiers::none());
|
||||||
|
cx.read(|cx| {
|
||||||
|
let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
|
||||||
|
assert_eq!(active_editor.read(cx).title(cx), "1.txt");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/test",
|
||||||
|
json!({
|
||||||
|
"1.txt": "// One",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
|
|
||||||
|
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||||
|
|
||||||
|
cx.simulate_modifiers_change(Modifiers::command());
|
||||||
|
open_file_picker(&workspace, cx);
|
||||||
|
|
||||||
|
cx.simulate_modifiers_change(Modifiers::command_shift());
|
||||||
|
active_file_picker(&workspace, cx);
|
||||||
|
}
|
||||||
|
|
||||||
async fn open_close_queried_buffer(
|
async fn open_close_queried_buffer(
|
||||||
input: &str,
|
input: &str,
|
||||||
expected_matches: usize,
|
expected_matches: usize,
|
||||||
|
@ -1581,7 +1674,7 @@ fn active_file_picker(
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
.active_modal::<FileFinder>(cx)
|
.active_modal::<FileFinder>(cx)
|
||||||
.unwrap()
|
.expect("file finder is not open")
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.picker
|
.picker
|
||||||
.clone()
|
.clone()
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
|
point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
|
||||||
ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, Hitbox,
|
ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, Hitbox,
|
||||||
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton,
|
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
|
||||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render,
|
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||||
ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility,
|
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||||
WindowContext,
|
StyleRefinement, Styled, Task, View, Visibility, WindowContext,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use refineable::Refineable;
|
use refineable::Refineable;
|
||||||
|
@ -389,6 +389,18 @@ impl Interactivity {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bind the given callback to modifiers changing events.
|
||||||
|
/// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`]
|
||||||
|
///
|
||||||
|
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
|
||||||
|
pub fn on_modifiers_changed(
|
||||||
|
&mut self,
|
||||||
|
listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static,
|
||||||
|
) {
|
||||||
|
self.modifiers_changed_listeners
|
||||||
|
.push(Box::new(move |event, cx| listener(event, cx)));
|
||||||
|
}
|
||||||
|
|
||||||
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
|
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
|
||||||
/// The imperative API equivalent to [`InteractiveElement::on_drop`]
|
/// The imperative API equivalent to [`InteractiveElement::on_drop`]
|
||||||
///
|
///
|
||||||
|
@ -775,6 +787,18 @@ pub trait InteractiveElement: Sized {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bind the given callback to modifiers changing events.
|
||||||
|
/// The fluent API equivalent to [`Interactivity::on_modifiers_changed`]
|
||||||
|
///
|
||||||
|
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
|
||||||
|
fn on_modifiers_changed(
|
||||||
|
mut self,
|
||||||
|
listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.interactivity().on_modifiers_changed(listener);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply the given style when the given data type is dragged over this element
|
/// Apply the given style when the given data type is dragged over this element
|
||||||
fn drag_over<S: 'static>(
|
fn drag_over<S: 'static>(
|
||||||
mut self,
|
mut self,
|
||||||
|
@ -999,6 +1023,9 @@ pub(crate) type KeyDownListener =
|
||||||
pub(crate) type KeyUpListener =
|
pub(crate) type KeyUpListener =
|
||||||
Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>;
|
Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>;
|
||||||
|
|
||||||
|
pub(crate) type ModifiersChangedListener =
|
||||||
|
Box<dyn Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static>;
|
||||||
|
|
||||||
pub(crate) type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
pub(crate) type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
||||||
|
|
||||||
/// Construct a new [`Div`] element
|
/// Construct a new [`Div`] element
|
||||||
|
@ -1188,6 +1215,7 @@ pub struct Interactivity {
|
||||||
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
|
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
|
||||||
pub(crate) key_down_listeners: Vec<KeyDownListener>,
|
pub(crate) key_down_listeners: Vec<KeyDownListener>,
|
||||||
pub(crate) key_up_listeners: Vec<KeyUpListener>,
|
pub(crate) key_up_listeners: Vec<KeyUpListener>,
|
||||||
|
pub(crate) modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
||||||
pub(crate) action_listeners: Vec<(TypeId, ActionListener)>,
|
pub(crate) action_listeners: Vec<(TypeId, ActionListener)>,
|
||||||
pub(crate) drop_listeners: Vec<(TypeId, DropListener)>,
|
pub(crate) drop_listeners: Vec<(TypeId, DropListener)>,
|
||||||
pub(crate) can_drop_predicate: Option<CanDropPredicate>,
|
pub(crate) can_drop_predicate: Option<CanDropPredicate>,
|
||||||
|
@ -1873,6 +1901,7 @@ impl Interactivity {
|
||||||
fn paint_keyboard_listeners(&mut self, cx: &mut ElementContext) {
|
fn paint_keyboard_listeners(&mut self, cx: &mut ElementContext) {
|
||||||
let key_down_listeners = mem::take(&mut self.key_down_listeners);
|
let key_down_listeners = mem::take(&mut self.key_down_listeners);
|
||||||
let key_up_listeners = mem::take(&mut self.key_up_listeners);
|
let key_up_listeners = mem::take(&mut self.key_up_listeners);
|
||||||
|
let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners);
|
||||||
let action_listeners = mem::take(&mut self.action_listeners);
|
let action_listeners = mem::take(&mut self.action_listeners);
|
||||||
if let Some(context) = self.key_context.clone() {
|
if let Some(context) = self.key_context.clone() {
|
||||||
cx.set_key_context(context);
|
cx.set_key_context(context);
|
||||||
|
@ -1893,6 +1922,12 @@ impl Interactivity {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for listener in modifiers_changed_listeners {
|
||||||
|
cx.on_modifiers_changed(move |event: &ModifiersChangedEvent, cx| {
|
||||||
|
listener(event, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (action_type, listener) in action_listeners {
|
for (action_type, listener) in action_listeners {
|
||||||
cx.on_action(action_type, listener)
|
cx.on_action(action_type, listener)
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,8 @@
|
||||||
///
|
///
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
|
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
|
||||||
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
|
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use collections::FxHashMap;
|
use collections::FxHashMap;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -82,6 +83,7 @@ pub(crate) struct DispatchTree {
|
||||||
pub(crate) struct DispatchNode {
|
pub(crate) struct DispatchNode {
|
||||||
pub key_listeners: Vec<KeyListener>,
|
pub key_listeners: Vec<KeyListener>,
|
||||||
pub action_listeners: Vec<DispatchActionListener>,
|
pub action_listeners: Vec<DispatchActionListener>,
|
||||||
|
pub modifiers_changed_listeners: Vec<ModifiersChangedListener>,
|
||||||
pub context: Option<KeyContext>,
|
pub context: Option<KeyContext>,
|
||||||
pub focus_id: Option<FocusId>,
|
pub focus_id: Option<FocusId>,
|
||||||
view_id: Option<EntityId>,
|
view_id: Option<EntityId>,
|
||||||
|
@ -106,6 +108,7 @@ impl ReusedSubtree {
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
|
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
|
||||||
|
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut ElementContext)>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct DispatchActionListener {
|
pub(crate) struct DispatchActionListener {
|
||||||
|
@ -241,6 +244,7 @@ impl DispatchTree {
|
||||||
let target = self.active_node();
|
let target = self.active_node();
|
||||||
target.key_listeners = mem::take(&mut source.key_listeners);
|
target.key_listeners = mem::take(&mut source.key_listeners);
|
||||||
target.action_listeners = mem::take(&mut source.action_listeners);
|
target.action_listeners = mem::take(&mut source.action_listeners);
|
||||||
|
target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reuse_subtree(&mut self, old_range: Range<usize>, source: &mut Self) -> ReusedSubtree {
|
pub fn reuse_subtree(&mut self, old_range: Range<usize>, source: &mut Self) -> ReusedSubtree {
|
||||||
|
@ -310,6 +314,12 @@ impl DispatchTree {
|
||||||
self.active_node().key_listeners.push(listener);
|
self.active_node().key_listeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) {
|
||||||
|
self.active_node()
|
||||||
|
.modifiers_changed_listeners
|
||||||
|
.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_action(
|
pub fn on_action(
|
||||||
&mut self,
|
&mut self,
|
||||||
action_type: TypeId,
|
action_type: TypeId,
|
||||||
|
|
|
@ -229,4 +229,13 @@ impl Modifiers {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if this Modifiers is a subset of another Modifiers
|
||||||
|
pub fn is_subset_of(&self, other: &Modifiers) -> bool {
|
||||||
|
(other.control || !self.control)
|
||||||
|
&& (other.alt || !self.alt)
|
||||||
|
&& (other.shift || !self.shift)
|
||||||
|
&& (other.command || !self.command)
|
||||||
|
&& (other.function || !self.function)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@ use crate::{
|
||||||
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
||||||
FileDropEvent, Flatten, Global, GlobalElementId, GlobalPixels, Hsla, KeyBinding, KeyDownEvent,
|
FileDropEvent, Flatten, Global, GlobalElementId, GlobalPixels, Hsla, KeyBinding, KeyDownEvent,
|
||||||
KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers,
|
KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers,
|
||||||
MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
|
ModifiersChangedEvent, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas,
|
||||||
PlatformInput, PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size,
|
PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render, ScaledPixels,
|
||||||
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, View,
|
SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
|
||||||
VisualContext, WeakView, WindowAppearance, WindowOptions, WindowParams, WindowTextSystem,
|
TextStyleRefinement, View, VisualContext, WeakView, WindowAppearance, WindowOptions,
|
||||||
|
WindowParams, WindowTextSystem,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::FxHashSet;
|
use collections::FxHashSet;
|
||||||
|
@ -1381,6 +1382,11 @@ impl<'a> WindowContext<'a> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.dispatch_modifiers_changed_event(event, &dispatch_path);
|
||||||
|
if !self.propagate_event {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.dispatch_keystroke_observers(event, None);
|
self.dispatch_keystroke_observers(event, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1418,6 +1424,27 @@ impl<'a> WindowContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch_modifiers_changed_event(
|
||||||
|
&mut self,
|
||||||
|
event: &dyn Any,
|
||||||
|
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||||
|
) {
|
||||||
|
let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for node_id in dispatch_path.iter().rev() {
|
||||||
|
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||||
|
for listener in node.modifiers_changed_listeners.clone() {
|
||||||
|
self.with_element_context(|cx| {
|
||||||
|
listener(event, cx);
|
||||||
|
});
|
||||||
|
if !self.propagate_event {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Determine whether a potential multi-stroke key binding is in progress on this window.
|
/// Determine whether a potential multi-stroke key binding is in progress on this window.
|
||||||
pub fn has_pending_keystrokes(&self) -> bool {
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
self.window
|
self.window
|
||||||
|
|
|
@ -33,10 +33,11 @@ use crate::{
|
||||||
ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase, DispatchTree,
|
ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase, DispatchTree,
|
||||||
DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId,
|
DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId,
|
||||||
GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId,
|
GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId,
|
||||||
LineLayoutIndex, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler,
|
LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels,
|
||||||
Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
|
PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams,
|
||||||
Shadow, SharedString, Size, StrikethroughStyle, Style, TextStyleRefinement,
|
RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
|
||||||
TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
|
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext,
|
||||||
|
SUBPIXEL_VARIANTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) type AnyMouseListener =
|
pub(crate) type AnyMouseListener =
|
||||||
|
@ -1324,4 +1325,22 @@ impl<'a> ElementContext<'a> {
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a modifiers changed event listener on the window for the next frame.
|
||||||
|
///
|
||||||
|
/// This is a fairly low-level method, so prefer using event handlers on elements unless you have
|
||||||
|
/// a specific need to register a global listener.
|
||||||
|
pub fn on_modifiers_changed(
|
||||||
|
&mut self,
|
||||||
|
listener: impl Fn(&ModifiersChangedEvent, &mut ElementContext) + 'static,
|
||||||
|
) {
|
||||||
|
self.window
|
||||||
|
.next_frame
|
||||||
|
.dispatch_tree
|
||||||
|
.on_modifiers_changed(Rc::new(
|
||||||
|
move |event: &ModifiersChangedEvent, cx: &mut ElementContext<'_>| {
|
||||||
|
listener(event, cx)
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue