Share application adapter implementations (reduce LLVM IR bloat again) (#64262)
Move settings-update routing, fallible deserialization recovery,
selection effects, context-menu initialization, picker resize handles,
and ACP client setup into shared implementations. Inline the small
button callback adapters.
`cargo llvm-lines --locked -p settings_ui`:
Before (bec63dee23): 1571017 lines, 53634 copies
After: 1177376 lines (-25.1%), 43380 copies (-19.1%)
With compiler wrappers disabled, `cargo clean --target-dir
target/binary-size-split-build` and `cargo build --locked -p zed`:
Before: 135.6s wall, 1569.8s CPU, zed binary 1175563800 bytes
After: 140.9s wall (+3.9%), 1588.8s CPU (+1.2%), zed binary 1157304600
bytes (-1.6%, -18.3MB)
Release Notes:
- N/A
This commit is contained in:
1 parent
5e04c64b52
commit
8b9a2df705
9 files changed
+245
-165
No files matched your search
@@ -7,7 +7,9 @@ use agent_client_protocol::schema::{
|
|||||||
ProtocolVersion,
|
ProtocolVersion,
|
||||||
v1::{self as acp, ErrorCode},
|
v1::{self as acp, ErrorCode},
|
||||||
};
|
};
|
||||||
use agent_client_protocol::{Agent, Client, ConnectionTo, JsonRpcResponse, Lines, Responder};
|
use agent_client_protocol::{
|
||||||
|
Agent, Builder, Client, ConnectionTo, HandleDispatchFrom, JsonRpcResponse, Lines, Responder,
|
||||||
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use async_channel;
|
use async_channel;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
@@ -667,23 +669,39 @@ fn connect_client_future(
|
|||||||
dispatch_tx: mpsc::UnboundedSender<ForegroundWork>,
|
dispatch_tx: mpsc::UnboundedSender<ForegroundWork>,
|
||||||
connection_tx: futures::channel::oneshot::Sender<ConnectionTo<Agent>>,
|
connection_tx: futures::channel::oneshot::Sender<ConnectionTo<Agent>>,
|
||||||
) -> impl Future<Output = Result<(), acp::Error>> {
|
) -> impl Future<Output = Result<(), acp::Error>> {
|
||||||
|
client_builder(name, dispatch_tx).connect_with(
|
||||||
|
transport,
|
||||||
|
move |connection: ConnectionTo<Agent>| async move {
|
||||||
|
if connection_tx.send(connection).is_err() {
|
||||||
|
log::error!("failed to send ACP connection handle — receiver was dropped");
|
||||||
|
}
|
||||||
|
// Keep the connection alive until the transport closes.
|
||||||
|
futures::future::pending::<Result<(), acp::Error>>().await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_builder(
|
||||||
|
name: &'static str,
|
||||||
|
dispatch_sender: mpsc::UnboundedSender<ForegroundWork>,
|
||||||
|
) -> Builder<Client, impl HandleDispatchFrom<Agent>> {
|
||||||
// Each handler forwards its inputs onto the foreground dispatch queue.
|
// Each handler forwards its inputs onto the foreground dispatch queue.
|
||||||
// The SDK requires the closure to be `Send`, so we move a clone of
|
// The SDK requires the closure to be `Send`, so we move a clone of
|
||||||
// `dispatch_tx` into each one.
|
// `dispatch_tx` into each one.
|
||||||
macro_rules! on_request {
|
macro_rules! on_request {
|
||||||
($handler:ident) => {{
|
($handler:ident) => {{
|
||||||
let dispatch_tx = dispatch_tx.clone();
|
let dispatch_sender = dispatch_sender.clone();
|
||||||
async move |req, responder, _connection| {
|
async move |request, responder, _connection| {
|
||||||
enqueue_request(&dispatch_tx, req, responder, $handler);
|
enqueue_request(&dispatch_sender, request, responder, $handler);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
macro_rules! on_notification {
|
macro_rules! on_notification {
|
||||||
($handler:ident) => {{
|
($handler:ident) => {{
|
||||||
let dispatch_tx = dispatch_tx.clone();
|
let dispatch_sender = dispatch_sender.clone();
|
||||||
async move |notif, _connection| {
|
async move |notification, _connection| {
|
||||||
enqueue_notification(&dispatch_tx, notif, $handler);
|
enqueue_notification(&dispatch_sender, notification, $handler);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
@@ -738,16 +756,6 @@ fn connect_client_future(
|
|||||||
on_notification!(handle_complete_elicitation),
|
on_notification!(handle_complete_elicitation),
|
||||||
agent_client_protocol::on_receive_notification!(),
|
agent_client_protocol::on_receive_notification!(),
|
||||||
)
|
)
|
||||||
.connect_with(
|
|
||||||
transport,
|
|
||||||
move |connection: ConnectionTo<Agent>| async move {
|
|
||||||
if connection_tx.send(connection).is_err() {
|
|
||||||
log::error!("failed to send ACP connection handle — receiver was dropped");
|
|
||||||
}
|
|
||||||
// Keep the connection alive until the transport closes.
|
|
||||||
futures::future::pending::<Result<(), acp::Error>>().await
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_capabilities_for_agent(
|
fn client_capabilities_for_agent(
|
||||||
|
|||||||
@@ -66,24 +66,10 @@ impl Editor {
|
|||||||
state.changed |= changed;
|
state.changed |= changed;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
let mut state = DeferredSelectionEffectsState {
|
let mut state = self.prepare_selection_effects(effects);
|
||||||
changed: false,
|
|
||||||
effects,
|
|
||||||
old_cursor_position: self.selections.newest_anchor().head(),
|
|
||||||
history_entry: SelectionHistoryEntry {
|
|
||||||
selections: self.selections.disjoint_anchors_arc(),
|
|
||||||
select_next_state: self.select_next_state.clone(),
|
|
||||||
select_prev_state: self.select_prev_state.clone(),
|
|
||||||
add_selections_state: self.add_selections_state.clone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let (changed, result) = self.selections.change_with(&snapshot, change);
|
let (changed, result) = self.selections.change_with(&snapshot, change);
|
||||||
state.changed = state.changed || changed;
|
state.changed = state.changed || changed;
|
||||||
if self.defer_selection_effects {
|
self.apply_selection_effects(state, window, cx);
|
||||||
self.deferred_selection_effects_state = Some(state);
|
|
||||||
} else {
|
|
||||||
self.apply_selection_effects(state, window, cx);
|
|
||||||
}
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,12 +105,7 @@ impl Editor {
|
|||||||
let already_deferred = self.defer_selection_effects;
|
let already_deferred = self.defer_selection_effects;
|
||||||
self.defer_selection_effects = true;
|
self.defer_selection_effects = true;
|
||||||
let result = update(self, window, cx);
|
let result = update(self, window, cx);
|
||||||
if !already_deferred {
|
self.finish_deferred_selection_effects(already_deferred, window, cx);
|
||||||
self.defer_selection_effects = false;
|
|
||||||
if let Some(state) = self.deferred_selection_effects_state.take() {
|
|
||||||
self.apply_selection_effects(state, window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1776,12 +1757,51 @@ impl Editor {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn prepare_selection_effects(
|
||||||
|
&self,
|
||||||
|
effects: SelectionEffects,
|
||||||
|
) -> DeferredSelectionEffectsState {
|
||||||
|
DeferredSelectionEffectsState {
|
||||||
|
changed: false,
|
||||||
|
effects,
|
||||||
|
old_cursor_position: self.selections.newest_anchor().head(),
|
||||||
|
history_entry: SelectionHistoryEntry {
|
||||||
|
selections: self.selections.disjoint_anchors_arc(),
|
||||||
|
select_next_state: self.select_next_state.clone(),
|
||||||
|
select_prev_state: self.select_prev_state.clone(),
|
||||||
|
add_selections_state: self.add_selections_state.clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn finish_deferred_selection_effects(
|
||||||
|
&mut self,
|
||||||
|
already_deferred: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if !already_deferred {
|
||||||
|
self.defer_selection_effects = false;
|
||||||
|
if let Some(state) = self.deferred_selection_effects_state.take() {
|
||||||
|
self.apply_selection_effects(state, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
fn apply_selection_effects(
|
fn apply_selection_effects(
|
||||||
&mut self,
|
&mut self,
|
||||||
state: DeferredSelectionEffectsState,
|
state: DeferredSelectionEffectsState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if self.defer_selection_effects {
|
||||||
|
self.deferred_selection_effects_state = Some(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if state.changed {
|
if state.changed {
|
||||||
self.selection_history.push(state.history_entry);
|
self.selection_history.push(state.history_entry);
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,9 @@
|
|||||||
|
|
||||||
use std::{any::type_name, marker::PhantomData};
|
use std::{any::type_name, marker::PhantomData};
|
||||||
|
|
||||||
use gpui::{ClickEvent, Context, CursorStyle, DragMoveEvent, MouseButton, Point, Styled, Window};
|
use gpui::{
|
||||||
|
ClickEvent, Context, CursorStyle, DragMoveEvent, MouseButton, Point, Stateful, Styled, Window,
|
||||||
|
};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
||||||
use crate::shape::{Centered, PositionAndShape, Shape, SizeBounds};
|
use crate::shape::{Centered, PositionAndShape, Shape, SizeBounds};
|
||||||
@@ -430,50 +432,31 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
div()
|
render_resize_handle(
|
||||||
.id(S::id())
|
side,
|
||||||
.absolute()
|
self.shape,
|
||||||
.cursor(side.cursor())
|
&self.size_bounds,
|
||||||
.map(|this| {
|
self.preview_layout_rendered(window),
|
||||||
side.position(
|
window,
|
||||||
this,
|
)
|
||||||
self.shape.clamped_position_and_size(
|
.on_drag_move::<ResizeDrag<S>>(cx.listener(
|
||||||
self.preview_layout_rendered(window),
|
move |picker, event: &DragMoveEvent<ResizeDrag<S>>, window, cx| {
|
||||||
&self.size_bounds,
|
let drag = event.drag(cx);
|
||||||
window,
|
let delta = event.event.position - drag.mouse_pos_before;
|
||||||
),
|
let mut working = side.current_position_and_shape(drag.shape_before, delta);
|
||||||
|
side.clamp(
|
||||||
|
&mut working,
|
||||||
|
&picker.size_bounds,
|
||||||
|
picker.preview_layout_rendered(window),
|
||||||
window,
|
window,
|
||||||
)
|
);
|
||||||
})
|
picker.shape = Shape::Resizing(working);
|
||||||
.block_mouse_except_scroll()
|
cx.notify();
|
||||||
.on_mouse_down(MouseButton::Left, do_nothing)
|
},
|
||||||
.on_drag(
|
))
|
||||||
ResizeDrag::<S>::start_new(
|
.on_click(cx.listener(move |picker, event: &ClickEvent, window, cx| {
|
||||||
self.shape,
|
picker.reset_size_to_default_on_double_click(side, event, window, cx)
|
||||||
&self.size_bounds,
|
}))
|
||||||
self.preview_layout_rendered(window),
|
|
||||||
window,
|
|
||||||
),
|
|
||||||
|_, _, _, cx| cx.new(|_| DragPreview),
|
|
||||||
)
|
|
||||||
.on_drag_move::<ResizeDrag<S>>(cx.listener(
|
|
||||||
move |this, event: &DragMoveEvent<ResizeDrag<S>>, window, cx| {
|
|
||||||
let drag = event.drag(cx);
|
|
||||||
let delta = event.event.position - drag.mouse_pos_before;
|
|
||||||
let mut working = side.current_position_and_shape(drag.shape_before, delta);
|
|
||||||
side.clamp(
|
|
||||||
&mut working,
|
|
||||||
&this.size_bounds,
|
|
||||||
this.preview_layout_rendered(window),
|
|
||||||
window,
|
|
||||||
);
|
|
||||||
this.shape = Shape::Resizing(working);
|
|
||||||
cx.notify();
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
|
||||||
this.reset_size_to_default_on_double_click(side, event, window, cx)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_size_to_default_on_double_click<S: Side>(
|
fn reset_size_to_default_on_double_click<S: Side>(
|
||||||
@@ -497,6 +480,34 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
#[track_caller]
|
||||||
|
fn render_resize_handle<S: Side>(
|
||||||
|
side: S,
|
||||||
|
shape: Shape,
|
||||||
|
bounds: &SizeBounds,
|
||||||
|
layout: Option<Layout>,
|
||||||
|
window: &mut Window,
|
||||||
|
) -> Stateful<Div> {
|
||||||
|
div()
|
||||||
|
.id(S::id())
|
||||||
|
.absolute()
|
||||||
|
.cursor(side.cursor())
|
||||||
|
.map(|handle| {
|
||||||
|
side.position(
|
||||||
|
handle,
|
||||||
|
shape.clamped_position_and_size(layout, bounds, window),
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.block_mouse_except_scroll()
|
||||||
|
.on_mouse_down(MouseButton::Left, do_nothing)
|
||||||
|
.on_drag(
|
||||||
|
ResizeDrag::<S>::start_new(shape, bounds, layout, window),
|
||||||
|
|_, _, _, cx| cx.new(|_| DragPreview),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn do_nothing(_: &gpui::MouseDownEvent, window: &mut Window, cx: &mut App) {
|
fn do_nothing(_: &gpui::MouseDownEvent, window: &mut Window, cx: &mut App) {
|
||||||
window.prevent_default();
|
window.prevent_default();
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::cell::RefCell;
|
use std::{cell::RefCell, fmt::Display};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
@@ -50,14 +50,22 @@ where
|
|||||||
{
|
{
|
||||||
match T::deserialize(deserializer) {
|
match T::deserialize(deserializer) {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
Err(e) => ERRORS.with_borrow_mut(|errors| {
|
Err(error) => {
|
||||||
if let Some(errors) = errors {
|
let mut error = Some(error);
|
||||||
errors.push(anyhow::anyhow!("{}", e));
|
let mut fallback = None;
|
||||||
Ok(Default::default())
|
recover_deserialization_error(&mut |record_error| {
|
||||||
} else {
|
if let Some(error) = error.take() {
|
||||||
Err(e)
|
record_error(&error);
|
||||||
|
let value = T::default();
|
||||||
|
drop(error);
|
||||||
|
fallback = Some(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match error {
|
||||||
|
Some(error) => Err(error),
|
||||||
|
None => Ok(fallback.expect("recovery creates a fallback")),
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +154,15 @@ where
|
|||||||
T::deserialize(rest)
|
T::deserialize(rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn recover_deserialization_error(recover: &mut dyn FnMut(&mut dyn FnMut(&dyn Display))) {
|
||||||
|
ERRORS.with_borrow_mut(|errors| {
|
||||||
|
if let Some(errors) = errors {
|
||||||
|
recover(&mut |error| errors.push(anyhow::anyhow!("{error}")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|||||||
@@ -4694,6 +4694,20 @@ fn update_settings_file(
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
|
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut update = Some(update);
|
||||||
|
update_settings_file_inner(file, file_name, window, cx, &mut || {
|
||||||
|
Box::new(update.take().expect("called once"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn update_settings_file_inner(
|
||||||
|
file: SettingsUiFile,
|
||||||
|
file_name: Option<&'static str>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
update: &mut dyn FnMut() -> Box<dyn Send + FnOnce(&mut SettingsContent, &App)>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
telemetry::event!("Settings Change", setting = file_name, type = file.setting_type());
|
telemetry::event!("Settings Change", setting = file_name, type = file.setting_type());
|
||||||
|
|
||||||
@@ -4708,7 +4722,7 @@ fn update_settings_file(
|
|||||||
}
|
}
|
||||||
SettingsUiFile::User => {
|
SettingsUiFile::User => {
|
||||||
// todo(settings_ui) error?
|
// todo(settings_ui) error?
|
||||||
SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
|
SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
SettingsUiFile::Server(_) => unimplemented!(),
|
SettingsUiFile::Server(_) => unimplemented!(),
|
||||||
@@ -4833,10 +4847,11 @@ impl ProjectSettingsUpdateQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
fn update_project_setting_file(
|
fn update_project_setting_file(
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
rel_path: Arc<RelPath>,
|
rel_path: Arc<RelPath>,
|
||||||
update: impl 'static + FnOnce(&mut SettingsContent, &App),
|
update: &mut dyn FnMut() -> Box<dyn Send + FnOnce(&mut SettingsContent, &App)>,
|
||||||
settings_window: Entity<SettingsWindow>,
|
settings_window: Entity<SettingsWindow>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -4857,7 +4872,7 @@ fn update_project_setting_file(
|
|||||||
settings_window: settings_window.downgrade(),
|
settings_window: settings_window.downgrade(),
|
||||||
project: project.downgrade(),
|
project: project.downgrade(),
|
||||||
worktree: worktree.downgrade(),
|
worktree: worktree.downgrade(),
|
||||||
update: Box::new(update),
|
update: update(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ProjectSettingsUpdateQueue::enqueue(cx, entry);
|
ProjectSettingsUpdateQueue::enqueue(cx, entry);
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ impl Disableable for Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Clickable for Button {
|
impl Clickable for Button {
|
||||||
|
#[inline(always)]
|
||||||
fn on_click(
|
fn on_click(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
|
handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
@@ -393,6 +394,7 @@ impl ButtonCommon for Button {
|
|||||||
/// Button::new("tooltip_button", "Hover Me")
|
/// Button::new("tooltip_button", "Hover Me")
|
||||||
/// .tooltip(Tooltip::text("This is a tooltip"));
|
/// .tooltip(Tooltip::text("This is a tooltip"));
|
||||||
/// ```
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
||||||
self.base = self.base.tooltip(tooltip);
|
self.base = self.base.tooltip(tooltip);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -670,6 +670,7 @@ impl SelectableButton for ButtonLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Clickable for ButtonLike {
|
impl Clickable for ButtonLike {
|
||||||
|
#[inline(always)]
|
||||||
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
|
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
|
||||||
self.on_click = Some(Box::new(handler));
|
self.on_click = Some(Box::new(handler));
|
||||||
self
|
self
|
||||||
@@ -708,6 +709,7 @@ impl ButtonCommon for ButtonLike {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
||||||
self.tooltip = Some(Box::new(tooltip));
|
self.tooltip = Some(Box::new(tooltip));
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ impl SelectableButton for IconButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Clickable for IconButton {
|
impl Clickable for IconButton {
|
||||||
|
#[inline(always)]
|
||||||
fn on_click(
|
fn on_click(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
|
handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
@@ -201,6 +202,7 @@ impl ButtonCommon for IconButton {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
||||||
self.base = self.base.tooltip(tooltip);
|
self.base = self.base.tooltip(tooltip);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -271,81 +271,14 @@ impl EventEmitter<DismissEvent> for ContextMenu {}
|
|||||||
impl FluentBuilder for ContextMenu {}
|
impl FluentBuilder for ContextMenu {}
|
||||||
|
|
||||||
impl ContextMenu {
|
impl ContextMenu {
|
||||||
|
#[inline(always)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
|
build_menu: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let menu = Self::new_inner(window, cx);
|
||||||
let _on_blur_subscription = cx.on_blur(
|
build_menu(menu, window, cx)
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
|this: &mut ContextMenu, window, cx| {
|
|
||||||
if let Some(ignore_until) = this.ignore_blur_until {
|
|
||||||
if Instant::now() < ignore_until {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.ignore_blur_until = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if this.main_menu.is_none() {
|
|
||||||
if let SubmenuState::Open(open_submenu) = &this.submenu_state {
|
|
||||||
let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();
|
|
||||||
if submenu_focus.contains_focused(window, cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cancel(&menu::Cancel, window, cx)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
window.refresh();
|
|
||||||
|
|
||||||
// When the menu first receives focus (i.e. when it opens), move the
|
|
||||||
// selection onto a menu item so assistive technology announces a real
|
|
||||||
// item rather than the bare menu container. Per the ARIA menu button
|
|
||||||
// pattern, opening a menu places focus on a menu item; for select-style
|
|
||||||
// menus we prefer the currently-checked item. We only do this when
|
|
||||||
// nothing is selected yet so we don't override an existing selection.
|
|
||||||
cx.on_focus_in(&focus_handle, window, |this, window, cx| {
|
|
||||||
if this.selected_index.is_none() && !this.suppress_focus_selection {
|
|
||||||
this.select_toggled_or_first(window, cx);
|
|
||||||
}
|
|
||||||
this.suppress_focus_selection = false;
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
f(
|
|
||||||
Self {
|
|
||||||
builder: None,
|
|
||||||
items: Default::default(),
|
|
||||||
focus_handle,
|
|
||||||
action_context: None,
|
|
||||||
selected_index: None,
|
|
||||||
delayed: false,
|
|
||||||
clicked: false,
|
|
||||||
end_slot_action: None,
|
|
||||||
key_context: "menu".into(),
|
|
||||||
_on_blur_subscription,
|
|
||||||
keep_open_on_confirm: false,
|
|
||||||
fixed_width: None,
|
|
||||||
main_menu: None,
|
|
||||||
main_menu_observed_bounds: Rc::new(Cell::new(None)),
|
|
||||||
documentation_aside: None,
|
|
||||||
aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
|
|
||||||
submenu_state: SubmenuState::Closed,
|
|
||||||
hover_target: HoverTarget::MainMenu,
|
|
||||||
submenu_safety_threshold_x: None,
|
|
||||||
submenu_trigger_bounds: Rc::new(Cell::new(None)),
|
|
||||||
submenu_trigger_mouse_down: false,
|
|
||||||
ignore_blur_until: None,
|
|
||||||
suppress_focus_selection: false,
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(
|
pub fn build(
|
||||||
@@ -2175,6 +2108,76 @@ impl ContextMenu {
|
|||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn new_inner(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
let _on_blur_subscription = cx.on_blur(
|
||||||
|
&focus_handle,
|
||||||
|
window,
|
||||||
|
|context_menu: &mut ContextMenu, window, cx| {
|
||||||
|
if let Some(ignore_until) = context_menu.ignore_blur_until {
|
||||||
|
if Instant::now() < ignore_until {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
context_menu.ignore_blur_until = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if context_menu.main_menu.is_none() {
|
||||||
|
if let SubmenuState::Open(open_submenu) = &context_menu.submenu_state {
|
||||||
|
let submenu_focus = open_submenu.entity.read(cx).focus_handle.clone();
|
||||||
|
if submenu_focus.contains_focused(window, cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context_menu.cancel(&menu::Cancel, window, cx)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
window.refresh();
|
||||||
|
|
||||||
|
// When the menu first receives focus (i.e. when it opens), move the
|
||||||
|
// selection onto a menu item so assistive technology announces a real
|
||||||
|
// item rather than the bare menu container. Per the ARIA menu button
|
||||||
|
// pattern, opening a menu places focus on a menu item; for select-style
|
||||||
|
// menus we prefer the currently-checked item. We only do this when
|
||||||
|
// nothing is selected yet so we don't override an existing selection.
|
||||||
|
cx.on_focus_in(&focus_handle, window, |context_menu, window, cx| {
|
||||||
|
if context_menu.selected_index.is_none() && !context_menu.suppress_focus_selection {
|
||||||
|
context_menu.select_toggled_or_first(window, cx);
|
||||||
|
}
|
||||||
|
context_menu.suppress_focus_selection = false;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
builder: None,
|
||||||
|
items: Vec::new(),
|
||||||
|
focus_handle,
|
||||||
|
action_context: None,
|
||||||
|
selected_index: None,
|
||||||
|
delayed: false,
|
||||||
|
clicked: false,
|
||||||
|
end_slot_action: None,
|
||||||
|
key_context: SharedString::from("menu"),
|
||||||
|
_on_blur_subscription,
|
||||||
|
keep_open_on_confirm: false,
|
||||||
|
fixed_width: None,
|
||||||
|
main_menu: None,
|
||||||
|
main_menu_observed_bounds: Rc::new(Cell::new(None)),
|
||||||
|
documentation_aside: None,
|
||||||
|
aside_trigger_bounds: Rc::new(RefCell::new(HashMap::default())),
|
||||||
|
submenu_state: SubmenuState::Closed,
|
||||||
|
hover_target: HoverTarget::MainMenu,
|
||||||
|
submenu_safety_threshold_x: None,
|
||||||
|
submenu_trigger_bounds: Rc::new(Cell::new(None)),
|
||||||
|
submenu_trigger_mouse_down: false,
|
||||||
|
ignore_blur_until: None,
|
||||||
|
suppress_focus_selection: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextMenuItem {
|
impl ContextMenuItem {
|
||||||
|
|||||||
Reference in new issue
Block a user