logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
34
crates/picker/Cargo.toml
Normal file
34
crates/picker/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "picker"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/picker.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
settings.workspace = true
|
||||
1
crates/picker/LICENSE-GPL
Symbolic link
1
crates/picker/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
79
crates/picker/src/head.rs
Normal file
79
crates/picker/src/head.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{App, Entity, FocusHandle, Focusable, prelude::*};
|
||||
use ui::prelude::*;
|
||||
use ui_input::{ErasedEditor, ErasedEditorEvent};
|
||||
|
||||
/// The head of a [`Picker`](crate::Picker).
|
||||
pub(crate) enum Head {
|
||||
/// Picker has an editor that allows the user to filter the list.
|
||||
Editor(Arc<dyn ErasedEditor>),
|
||||
|
||||
/// Picker has no head, it's just a list of items.
|
||||
Empty(Entity<EmptyHead>),
|
||||
}
|
||||
|
||||
impl Head {
|
||||
pub fn editor<V: 'static>(
|
||||
placeholder_text: Arc<str>,
|
||||
mut edit_handler: impl FnMut(&mut V, &ErasedEditorEvent, &mut Window, &mut Context<V>) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<V>,
|
||||
) -> Self {
|
||||
let editor = (ui_input::ERASED_EDITOR_FACTORY.get().unwrap())(window, cx);
|
||||
|
||||
editor.set_placeholder_text(placeholder_text.as_ref(), window, cx);
|
||||
let this = cx.weak_entity();
|
||||
editor
|
||||
.subscribe(
|
||||
Box::new(move |event, window, cx| {
|
||||
this.update(cx, |this, cx| (edit_handler)(this, &event, window, cx))
|
||||
.ok();
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
// cx.subscribe_in(&editor, window, |v, _, event, window, cx| {
|
||||
// edit_handler(v, event, window, cx);
|
||||
// })
|
||||
// .detach();
|
||||
Self::Editor(editor)
|
||||
}
|
||||
|
||||
pub fn empty<V: 'static>(
|
||||
blur_handler: impl FnMut(&mut V, &mut Window, &mut Context<V>) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<V>,
|
||||
) -> Self {
|
||||
let head = cx.new(EmptyHead::new);
|
||||
cx.on_blur(&head.focus_handle(cx), window, blur_handler)
|
||||
.detach();
|
||||
Self::Empty(head)
|
||||
}
|
||||
}
|
||||
|
||||
/// An invisible element that can hold focus.
|
||||
pub(crate) struct EmptyHead {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl EmptyHead {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EmptyHead {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().track_focus(&self.focus_handle(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for EmptyHead {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
132
crates/picker/src/highlighted_match_with_paths.rs
Normal file
132
crates/picker/src/highlighted_match_with_paths.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use ui::{HighlightedLabel, prelude::*};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HighlightedMatchWithPaths {
|
||||
pub prefix: Option<SharedString>,
|
||||
pub match_label: HighlightedMatch,
|
||||
pub paths: Vec<HighlightedMatch>,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub struct HighlightedMatch {
|
||||
pub text: String,
|
||||
pub highlight_positions: Vec<usize>,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl HighlightedMatch {
|
||||
pub fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
|
||||
// Track a running byte offset and insert separators between parts.
|
||||
let mut first = true;
|
||||
let mut byte_offset = 0;
|
||||
let mut text = String::new();
|
||||
let mut highlight_positions = Vec::new();
|
||||
for component in components {
|
||||
if !first {
|
||||
text.push_str(separator);
|
||||
byte_offset += separator.len();
|
||||
}
|
||||
first = false;
|
||||
|
||||
highlight_positions.extend(
|
||||
component
|
||||
.highlight_positions
|
||||
.iter()
|
||||
.map(|position| position + byte_offset),
|
||||
);
|
||||
text.push_str(&component.text);
|
||||
byte_offset += component.text.len();
|
||||
}
|
||||
|
||||
Self {
|
||||
text,
|
||||
highlight_positions,
|
||||
color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(self, color: Color) -> Self {
|
||||
Self { color, ..self }
|
||||
}
|
||||
}
|
||||
impl RenderOnce for HighlightedMatch {
|
||||
fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
HighlightedLabel::new(self.text, self.highlight_positions)
|
||||
.color(self.color)
|
||||
.truncate()
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightedMatchWithPaths {
|
||||
pub fn render_paths_children(&mut self, element: Div) -> Div {
|
||||
element.children(self.paths.clone().into_iter().map(|path| {
|
||||
HighlightedLabel::new(path.text, path.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn is_active(mut self, active: bool) -> Self {
|
||||
self.active = active;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedMatchWithPaths {
|
||||
fn render(mut self, _window: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.min_w_0()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.match_label.clone())
|
||||
.when_some(self.prefix.as_ref(), |this, prefix| {
|
||||
this.child(Label::new(format!("({})", prefix)).color(Color::Muted))
|
||||
})
|
||||
.when(self.active, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(!self.paths.is_empty(), |this| {
|
||||
self.render_paths_children(this)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn join_offsets_positions_by_bytes_not_chars() {
|
||||
// "αβγ" is 3 Unicode scalar values, 6 bytes in UTF-8.
|
||||
let left_text = "αβγ".to_string();
|
||||
let right_text = "label".to_string();
|
||||
let left = HighlightedMatch {
|
||||
text: left_text,
|
||||
highlight_positions: vec![],
|
||||
color: Color::Default,
|
||||
};
|
||||
let right = HighlightedMatch {
|
||||
text: right_text,
|
||||
highlight_positions: vec![0, 1],
|
||||
color: Color::Default,
|
||||
};
|
||||
let joined = HighlightedMatch::join([left, right].into_iter(), "");
|
||||
|
||||
assert!(
|
||||
joined
|
||||
.highlight_positions
|
||||
.iter()
|
||||
.all(|&p| joined.text.is_char_boundary(p)),
|
||||
"join produced non-boundary positions {:?} for text {:?}",
|
||||
joined.highlight_positions,
|
||||
joined.text
|
||||
);
|
||||
}
|
||||
}
|
||||
1253
crates/picker/src/picker.rs
Normal file
1253
crates/picker/src/picker.rs
Normal file
File diff suppressed because it is too large
Load Diff
101
crates/picker/src/popover_menu.rs
Normal file
101
crates/picker/src/popover_menu.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use gpui::{
|
||||
Anchor, AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Point,
|
||||
Subscription,
|
||||
};
|
||||
use ui::{
|
||||
FluentBuilder as _, IntoElement, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{Picker, PickerDelegate};
|
||||
|
||||
pub struct PickerPopoverMenu<T, TT, P>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
P: PickerDelegate,
|
||||
{
|
||||
picker: Entity<Picker<P>>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
handle: Option<PopoverMenuHandle<Picker<P>>>,
|
||||
anchor: Anchor,
|
||||
offset: Option<Point<Pixels>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl<T, TT, P> PickerPopoverMenu<T, TT, P>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
P: PickerDelegate,
|
||||
{
|
||||
pub fn new(
|
||||
picker: Entity<Picker<P>>,
|
||||
trigger: T,
|
||||
tooltip: TT,
|
||||
anchor: Anchor,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
Self {
|
||||
_subscriptions: vec![cx.subscribe(&picker, |picker, &DismissEvent, cx| {
|
||||
picker.update(cx, |_, cx| cx.emit(DismissEvent));
|
||||
})],
|
||||
picker,
|
||||
trigger,
|
||||
tooltip,
|
||||
handle: None,
|
||||
offset: Some(Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
}),
|
||||
anchor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<P>>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
|
||||
self.offset = Some(offset);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT, P> EventEmitter<DismissEvent> for PickerPopoverMenu<T, TT, P>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
P: PickerDelegate,
|
||||
{
|
||||
}
|
||||
|
||||
impl<T, TT, P> Focusable for PickerPopoverMenu<T, TT, P>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
P: PickerDelegate,
|
||||
{
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT, P> RenderOnce for PickerPopoverMenu<T, TT, P>
|
||||
where
|
||||
T: PopoverTrigger + ButtonCommon,
|
||||
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
P: PickerDelegate,
|
||||
{
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let picker = self.picker.clone();
|
||||
|
||||
PopoverMenu::new("popover-menu")
|
||||
.menu(move |_window, _cx| Some(picker.clone()))
|
||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||
.anchor(self.anchor)
|
||||
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
|
||||
.when_some(self.offset, |menu, offset| menu.offset(offset))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user