merge: pop-os/iced a l'etat pinne par libcosmic 08a09e3df (epoch-1.9.0)
Some checks failed
Audit / vulnerabilities (push) Has been cancelled
Check / wasm (push) Has been cancelled
Check / widget (push) Has been cancelled
Document / all (push) Has been cancelled
Format / all (push) Has been cancelled
Lint / all (push) Has been cancelled
Test / all (macOS-latest, 1.88) (push) Has been cancelled
Test / all (macOS-latest, beta) (push) Has been cancelled
Test / all (macOS-latest, stable) (push) Has been cancelled
Test / all (ubuntu-latest, 1.88) (push) Has been cancelled
Test / all (ubuntu-latest, beta) (push) Has been cancelled
Test / all (ubuntu-latest, stable) (push) Has been cancelled
Test / all (windows-latest, 1.88) (push) Has been cancelled
Test / all (windows-latest, beta) (push) Has been cancelled
Test / all (windows-latest, stable) (push) Has been cancelled

# Conflicts:
#	Cargo.toml
This commit is contained in:
Lionel DARNIS 2026-09-24 20:45:11 +02:00
commit 24da1dc0e4
23 changed files with 1602 additions and 1231 deletions

2125
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,10 +10,10 @@ async-io = ["accesskit_winit?/async-io"]
tokio = ["accesskit_winit?/tokio"] tokio = ["accesskit_winit?/tokio"]
[dependencies] [dependencies]
accesskit = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14" } accesskit = { git = "https://github.com/wash2/accesskit", rev = "6c20249" }
accesskit_windows = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true } accesskit_windows = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true }
accesskit_macos = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true } accesskit_macos = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true }
accesskit_winit = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true, default-features = false, features = [ accesskit_winit = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true, default-features = false, features = [
"rwh_06", "rwh_06",
] } ] }
# accesskit = { path = "../../../accesskit/common" } # accesskit = { path = "../../../accesskit/common" }

View file

@ -9,7 +9,7 @@ pub use scrollable::Scrollable;
pub use text_input::TextInput; pub use text_input::TextInput;
use crate::widget::Id; use crate::widget::Id;
use crate::{Rectangle, Vector}; use crate::{Rectangle, Vector, window};
use std::any::Any; use std::any::Any;
use std::fmt; use std::fmt;
@ -75,6 +75,12 @@ pub trait Operation<T = ()>: Send {
fn finish(&self) -> Outcome<T> { fn finish(&self) -> Outcome<T> {
Outcome::None Outcome::None
} }
/// Track the active window id being processed if relevant to the operation
fn set_window_id(&mut self, _id: window::Id) {}
/// Used to mark the beginning of operations for a widget and its children
fn pre_operation(&mut self, _id: Option<&Id>) {}
} }
impl<T, O> Operation<O> for Box<T> impl<T, O> Operation<O> for Box<T>
@ -140,6 +146,14 @@ where
fn finish(&self) -> Outcome<O> { fn finish(&self) -> Outcome<O> {
self.as_ref().finish() self.as_ref().finish()
} }
fn set_window_id(&mut self, id: window::Id) {
self.as_mut().set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.as_mut().pre_operation(id);
}
} }
/// The result of an [`Operation`]. /// The result of an [`Operation`].
@ -243,6 +257,14 @@ where
fn finish(&self) -> Outcome<O> { fn finish(&self) -> Outcome<O> {
Outcome::None Outcome::None
} }
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
} }
BlackBox { operation } BlackBox { operation }
@ -341,6 +363,10 @@ where
) { ) {
self.operation.custom(id, bounds, state); self.operation.custom(id, bounds, state);
} }
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
} }
self.operation.traverse(&mut |operation| { self.operation.traverse(&mut |operation| {
@ -410,6 +436,14 @@ where
})), })),
} }
} }
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
} }
Map { Map {
@ -515,6 +549,14 @@ where
} }
} }
} }
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
} }
Chain { Chain {
@ -566,6 +608,14 @@ pub fn scoped<T: 'static>(
outcome => outcome, outcome => outcome,
} }
} }
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
} }
ScopedOperation { ScopedOperation {

View file

@ -224,6 +224,7 @@ pub struct State<P: Paragraph> {
focused: bool, focused: bool,
keyboard_focused: bool, keyboard_focused: bool,
context_menu_position: Option<Point>, context_menu_position: Option<Point>,
clipboard_has_text: bool,
} }
impl<P: Paragraph> Default for State<P> { impl<P: Paragraph> Default for State<P> {
@ -234,6 +235,7 @@ impl<P: Paragraph> Default for State<P> {
focused: false, focused: false,
keyboard_focused: false, keyboard_focused: false,
context_menu_position: None, context_menu_position: None,
clipboard_has_text: false,
} }
} }
} }
@ -377,45 +379,65 @@ where
let style = theme.style(&self.class); let style = theme.style(&self.class);
let bounds = layout.bounds(); let bounds = layout.bounds();
let paragraph = state.paragraph.raw(); let paragraph = state.paragraph.raw();
if let Some(sel) = &state.selection { let anchor = bounds.anchor(
let left = sel.anchor.min(sel.end); paragraph.min_bounds(),
let right = sel.anchor.max(sel.end); paragraph.align_x(),
let content: &str = self.fragment.as_ref(); paragraph.align_y(),
);
if left != right { let rects: Vec<Rectangle> = state
let lo_byte = grapheme_to_byte(content, left); .selection
let hi_byte = grapheme_to_byte(content, right); .as_ref()
.filter(|sel| sel.anchor != sel.end)
.map(|sel| {
let content: &str = self.fragment.as_ref();
let lo_byte =
grapheme_to_byte(content, sel.anchor.min(sel.end));
let hi_byte =
grapheme_to_byte(content, sel.anchor.max(sel.end));
let anchor = bounds.anchor( paragraph
paragraph.min_bounds(), .highlight(
paragraph.align_x(), 0,
paragraph.align_y(), (lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before),
)
.into_iter()
.map(|r| Rectangle {
x: anchor.x + r.x,
y: anchor.y + r.y,
width: r.width,
height: r.height,
})
.collect()
})
.unwrap_or_default();
let fill_selection = |renderer: &mut Renderer| {
for r in &rects {
renderer.fill_quad(
renderer::Quad {
bounds: *r,
..renderer::Quad::default()
},
style.selected_fill,
); );
}
};
let rects = paragraph.highlight( if style.selected_text_color.is_none() {
0, fill_selection(renderer);
(lo_byte, text::Affinity::After), }
(hi_byte, text::Affinity::Before), draw(renderer, defaults, bounds, paragraph, style, viewport);
); if let Some(color) = style.selected_text_color {
fill_selection(renderer);
for r in rects { for r in &rects {
renderer.fill_quad( renderer.with_layer(*r, |renderer| {
renderer::Quad { renderer
bounds: Rectangle { .fill_paragraph(paragraph, anchor, color, *viewport);
x: anchor.x + r.x, });
y: anchor.y + r.y,
width: r.width,
height: r.height,
},
..renderer::Quad::default()
},
style.selected_fill,
);
}
} }
} }
draw(renderer, defaults, bounds, paragraph, style, viewport);
} }
fn update( fn update(
@ -529,6 +551,7 @@ where
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
if let Some(pos) = cursor.position_over(bounds) { if let Some(pos) = cursor.position_over(bounds) {
state.context_menu_position = Some(pos); state.context_menu_position = Some(pos);
state.clipboard_has_text = clipboard_has_text(clipboard);
state.focused = true; state.focused = true;
state.keyboard_focused = false; state.keyboard_focused = false;
shell.capture_event(); shell.capture_event();
@ -928,6 +951,10 @@ pub struct Style {
pub color: Option<Color>, pub color: Option<Color>,
/// The fill [`Color`] of the selection highlight. /// The fill [`Color`] of the selection highlight.
pub selected_fill: Color, pub selected_fill: Color,
/// The [`Color`] of selected text.
///
/// The default, `None`, keeps the regular text color.
pub selected_text_color: Option<Color>,
} }
impl Default for Style { impl Default for Style {
@ -935,6 +962,7 @@ impl Default for Style {
Self { Self {
color: None, color: None,
selected_fill: DEFAULT_SELECTION_COLOR, selected_fill: DEFAULT_SELECTION_COLOR,
selected_text_color: None,
} }
} }
} }
@ -1023,6 +1051,13 @@ pub fn danger(theme: &Theme) -> Style {
use crate::widget::tree::Tree as WidgetTree; use crate::widget::tree::Tree as WidgetTree;
/// Returns `true` if the clipboard currently holds non-empty text.
pub fn clipboard_has_text(clipboard: &dyn Clipboard) -> bool {
clipboard
.read(crate::clipboard::Kind::Standard)
.is_some_and(|s| !s.is_empty())
}
/// Implement this on a **widget** to enable context menu support for /// Implement this on a **widget** to enable context menu support for
/// text selection (Copy, Select All, and optionally Cut / Paste) in libcosmic /// text selection (Copy, Select All, and optionally Cut / Paste) in libcosmic
pub trait HasSelectableText { pub trait HasSelectableText {
@ -1032,11 +1067,23 @@ pub trait HasSelectableText {
/// Selects all text. /// Selects all text.
fn select_all(&self, tree: &mut WidgetTree); fn select_all(&self, tree: &mut WidgetTree);
/// Returns `true` if the widget is editable (enables Cut / Paste). /// Returns `true` if the widget is editable (shows Cut / Paste).
fn is_editable(&self) -> bool { fn is_editable(&self) -> bool {
false false
} }
/// Returns `true` if the widget has any text (enables Select All).
fn has_text(&self, _tree: &WidgetTree) -> bool {
true
}
/// Returns whether the clipboard held text when the context menu was
/// requested (enables Paste). Widgets cache this on right-click because
/// overlays have no clipboard access.
fn clipboard_has_text(&self, _tree: &WidgetTree) -> bool {
true
}
/// Returns `true` if the widget is currently focused. /// Returns `true` if the widget is currently focused.
fn is_focused(&self, tree: &WidgetTree) -> bool; fn is_focused(&self, tree: &WidgetTree) -> bool;
@ -1106,6 +1153,16 @@ impl<Theme: Catalog, Renderer: text::Renderer> HasSelectableText
sel.end = count; sel.end = count;
} }
fn has_text(&self, tree: &WidgetTree) -> bool {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
!state.paragraph.content().is_empty()
}
fn clipboard_has_text(&self, tree: &WidgetTree) -> bool {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
state.clipboard_has_text
}
fn is_focused(&self, tree: &WidgetTree) -> bool { fn is_focused(&self, tree: &WidgetTree) -> bool {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
state.is_focused() state.is_focused()

View file

@ -157,6 +157,11 @@ pub enum Action {
/// padding of the surface geometry /// padding of the surface geometry
padding: IcedMargin, padding: IcedMargin,
}, },
ShowOnLock {
/// id of the layer surface
id: Id,
value: bool,
}
} }
impl fmt::Debug for Action { impl fmt::Debug for Action {
@ -215,6 +220,11 @@ impl fmt::Debug for Action {
"Action::LayerSurfaceAction::Padding {{ id: {:#?}, padding: {:?} }}", "Action::LayerSurfaceAction::Padding {{ id: {:#?}, padding: {:?} }}",
id, padding id, padding
), ),
Action::ShowOnLock { id, value } => write!(
f,
"Action::LayerSurfaceAction::ShowOnLock {{ id: {:#?}, value: {:?} }}",
id, value
),
} }
} }
} }

View file

@ -1,6 +1,6 @@
//! Find and query widgets in your applications. //! Find and query widgets in your applications.
pub use iced_selector::{ pub use iced_selector::{
Bounded, Candidate, Selector, Target, Text, id, is_focused, Bounded, Candidate, Selector, Target, Text, focus, id, is_focused,
}; };
use crate::Task; use crate::Task;

View file

@ -1,3 +1,5 @@
use iced_core::window;
use crate::Selector; use crate::Selector;
use crate::core::widget::operation::{ use crate::core::widget::operation::{
Focusable, Outcome, Scrollable, TextInput, Focusable, Outcome, Scrollable, TextInput,
@ -100,6 +102,10 @@ where
fn finish(&self) -> Self::Output { fn finish(&self) -> Self::Output {
self.outputs.clone() self.outputs.clone()
} }
fn set_window_id(&mut self, id: window::Id) {
self.selector.set_window_id(id);
}
} }
pub trait Strategy { pub trait Strategy {
@ -110,6 +116,8 @@ pub trait Strategy {
fn is_done(&self) -> bool; fn is_done(&self) -> bool;
fn finish(&self) -> Self::Output; fn finish(&self) -> Self::Output;
fn set_window_id(&mut self, _id: window::Id) {}
} }
#[derive(Debug)] #[derive(Debug)]
@ -273,4 +281,12 @@ where
fn finish(&self) -> Outcome<S::Output> { fn finish(&self) -> Outcome<S::Output> {
Outcome::Some(self.strategy.finish()) Outcome::Some(self.strategy.finish())
} }
fn set_window_id(&mut self, id: window::Id) {
self.strategy.set_window_id(id);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.strategy.feed(Candidate::PreOperation { id });
}
} }

View file

@ -1,5 +1,6 @@
//! Select data from the widget tree. //! Select data from the widget tree.
use iced_core as core; use iced_core as core;
use iced_core::window;
mod find; mod find;
mod target; mod target;
@ -48,6 +49,8 @@ pub trait Selector {
{ {
FindAll::new(find::All::new(self)) FindAll::new(find::All::new(self))
} }
fn set_window_id(&mut self, _id: window::Id) {}
} }
impl Selector for &str { impl Selector for &str {
@ -171,3 +174,42 @@ pub fn is_focused() -> impl Selector<Output = Target> {
IsFocused IsFocused
} }
/// Returns a [`Selector`] that matches widgets that are focusable.
pub fn focus() -> impl Selector<Output = (bool, Target, window::Id)> {
struct Focus {
cur_window_id: window::Id,
}
impl Selector for Focus {
type Output = (bool, Target, window::Id);
fn select(&mut self, candidate: Candidate<'_>) -> Option<Self::Output> {
if let Candidate::Focusable { state, .. } = candidate {
Some((
state.is_focused(),
Target::from(candidate),
self.cur_window_id,
))
} else if matches!(candidate, Candidate::Scrollable { .. }) {
Some((false, Target::from(candidate), self.cur_window_id))
} else if matches!(candidate, Candidate::PreOperation { .. }) {
Some((false, Target::from(candidate), self.cur_window_id))
} else {
None
}
}
fn description(&self) -> String {
"is focusable and focused".to_owned()
}
fn set_window_id(&mut self, id: iced_core::window::Id) {
self.cur_window_id = id;
}
}
Focus {
cur_window_id: window::Id::NONE,
}
}

View file

@ -42,6 +42,9 @@ pub enum Target {
bounds: Rectangle, bounds: Rectangle,
visible_bounds: Option<Rectangle>, visible_bounds: Option<Rectangle>,
}, },
PreOperation {
id: Option<Id>,
},
} }
impl Target { impl Target {
@ -54,6 +57,7 @@ impl Target {
| Target::TextInput { bounds, .. } | Target::TextInput { bounds, .. }
| Target::Text { bounds, .. } | Target::Text { bounds, .. }
| Target::Custom { bounds, .. } => *bounds, | Target::Custom { bounds, .. } => *bounds,
Target::PreOperation { .. } => Rectangle::default(),
} }
} }
@ -66,6 +70,7 @@ impl Target {
| Target::TextInput { visible_bounds, .. } | Target::TextInput { visible_bounds, .. }
| Target::Text { visible_bounds, .. } | Target::Text { visible_bounds, .. }
| Target::Custom { visible_bounds, .. } => *visible_bounds, | Target::Custom { visible_bounds, .. } => *visible_bounds,
Target::PreOperation { .. } => None,
} }
} }
} }
@ -138,6 +143,9 @@ impl From<Candidate<'_>> for Target {
bounds, bounds,
visible_bounds, visible_bounds,
}, },
Candidate::PreOperation { id } => {
Self::PreOperation { id: id.cloned() }
}
} }
} }
} }
@ -195,6 +203,8 @@ pub enum Candidate<'a> {
visible_bounds: Option<Rectangle>, visible_bounds: Option<Rectangle>,
state: &'a dyn Any, state: &'a dyn Any,
}, },
/// Used to mark the beginning of operations for a widget and its children
PreOperation { id: Option<&'a Id> },
} }
impl<'a> Candidate<'a> { impl<'a> Candidate<'a> {
@ -206,7 +216,8 @@ impl<'a> Candidate<'a> {
| Candidate::Scrollable { id, .. } | Candidate::Scrollable { id, .. }
| Candidate::TextInput { id, .. } | Candidate::TextInput { id, .. }
| Candidate::Text { id, .. } | Candidate::Text { id, .. }
| Candidate::Custom { id, .. } => *id, | Candidate::Custom { id, .. }
| Candidate::PreOperation { id } => *id,
} }
} }
@ -219,6 +230,7 @@ impl<'a> Candidate<'a> {
| Candidate::TextInput { bounds, .. } | Candidate::TextInput { bounds, .. }
| Candidate::Text { bounds, .. } | Candidate::Text { bounds, .. }
| Candidate::Custom { bounds, .. } => *bounds, | Candidate::Custom { bounds, .. } => *bounds,
Candidate::PreOperation { .. } => Rectangle::default(),
} }
} }
@ -231,6 +243,7 @@ impl<'a> Candidate<'a> {
| Candidate::TextInput { visible_bounds, .. } | Candidate::TextInput { visible_bounds, .. }
| Candidate::Text { visible_bounds, .. } | Candidate::Text { visible_bounds, .. }
| Candidate::Custom { visible_bounds, .. } => *visible_bounds, | Candidate::Custom { visible_bounds, .. } => *visible_bounds,
Candidate::PreOperation { .. } => None,
} }
} }
} }

View file

@ -273,6 +273,11 @@ impl<P: Program + 'static> Emulator<P> {
runtime::Action::Reload => { runtime::Action::Reload => {
// TODO // TODO
} }
runtime::Action::Dnd(_)
| runtime::Action::PlatformSpecific(_) => {
// Drag and drop and platform surfaces need a
// windowing system, which an emulator has none of.
}
}, },
} }
} }
@ -474,7 +479,9 @@ impl<P: Program + 'static> Emulator<P> {
&mut self.renderer, &mut self.renderer,
theme, theme,
&renderer::Style { &renderer::Style {
icon_color: style.text_color,
text_color: style.text_color, text_color: style.text_color,
scale_factor: f64::from(scale_factor),
}, },
mouse::Cursor::Unavailable, mouse::Cursor::Unavailable,
); );

View file

@ -209,17 +209,19 @@ where
&mut self.messages, &mut self.messages,
); );
let scale_factor = 2.0_f32;
self.raw.draw( self.raw.draw(
&mut self.renderer, &mut self.renderer,
theme, theme,
&core::renderer::Style { &core::renderer::Style {
icon_color: base.text_color,
text_color: base.text_color, text_color: base.text_color,
scale_factor: f64::from(scale_factor),
}, },
self.cursor, self.cursor,
); );
let scale_factor = 2.0;
let physical_size = Size::new( let physical_size = Size::new(
(self.size.width * scale_factor).round() as u32, (self.size.width * scale_factor).round() as u32,
(self.size.height * scale_factor).round() as u32, (self.size.height * scale_factor).round() as u32,

View file

@ -267,6 +267,7 @@ struct State {
is_hovered: bool, is_hovered: bool,
is_pressed: bool, is_pressed: bool,
is_focused: bool, is_focused: bool,
needs_redraw: bool,
} }
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@ -368,6 +369,12 @@ where
viewport, viewport,
); );
let state = tree.state.downcast_mut::<State>();
if state.needs_redraw {
state.needs_redraw = false;
shell.request_redraw();
}
if shell.is_event_captured() { if shell.is_event_captured() {
return; return;
} }
@ -1000,10 +1007,12 @@ impl operation::Focusable for State {
} }
fn focus(&mut self) { fn focus(&mut self) {
self.needs_redraw |= !self.is_focused;
self.is_focused = true; self.is_focused = true;
} }
fn unfocus(&mut self) { fn unfocus(&mut self) {
self.needs_redraw |= self.is_focused;
self.is_focused = false; self.is_focused = false;
} }
} }

View file

@ -247,64 +247,19 @@ where
self self
} }
/// Sets whether the user should be allowed to auto-scroll the [`Scrollable`] /// Adds padding at the ends of the [`Scrollbar`]s of the [`Scrollable`].
/// with the middle mouse button.
/// ///
/// By default, it is disabled. /// The `padding` provided will be used as space at the top and bottom of a
pub fn auto_scroll(mut self, auto_scroll: bool) -> Self { /// vertical [`Scrollbar`], and at the left and right ends of a horizontal
self.auto_scroll = auto_scroll; /// [`Scrollbar`], when they are visible.
self ///
} /// Unlike [`Self::spacing`], the padding does not affect the layout of the
/// [`Scrollable`].
/// Sets the scrollbar width of the [`Scrollbar`]. pub fn padding(mut self, new_padding: impl Into<Pixels>) -> Self {
pub fn scrollbar_width(mut self, width: impl Into<Pixels>) -> Self { let padding = new_padding.into().0;
let width = width.into().0.max(0.0);
match &mut self.direction { match &mut self.direction {
Direction::Horizontal(scrollbar) Direction::Horizontal(scrollbar) | Direction::Vertical(scrollbar) => {
| Direction::Vertical(scrollbar) => {
scrollbar.width = width;
}
Direction::Both {
horizontal,
vertical,
} => {
horizontal.width = width;
vertical.width = width;
}
}
self
}
/// Sets the scroller width of the [`Scrollbar`].
pub fn scroller_width(mut self, width: impl Into<Pixels>) -> Self {
let width = width.into().0.max(0.0);
match &mut self.direction {
Direction::Horizontal(scrollbar)
| Direction::Vertical(scrollbar) => {
scrollbar.scroller_width = width;
}
Direction::Both {
horizontal,
vertical,
} => {
horizontal.scroller_width = width;
vertical.scroller_width = width;
}
}
self
}
/// Sets the padding at the start and end of the [`Scrollbar`].
pub fn scrollbar_padding(mut self, padding: impl Into<Pixels>) -> Self {
let padding = padding.into().0.max(0.0);
match &mut self.direction {
Direction::Horizontal(scrollbar)
| Direction::Vertical(scrollbar) => {
scrollbar.padding = padding; scrollbar.padding = padding;
} }
Direction::Both { Direction::Both {
@ -319,6 +274,15 @@ where
self self
} }
/// Sets whether the user should be allowed to auto-scroll the [`Scrollable`]
/// with the middle mouse button.
///
/// By default, it is disabled.
pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
self.auto_scroll = auto_scroll;
self
}
/// Sets the style of this [`Scrollable`]. /// Sets the style of this [`Scrollable`].
#[must_use] #[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@ -503,9 +467,15 @@ impl Scrollbar {
self self
} }
/// Sets the padding at the start and end of the [`Scrollbar`]. /// Sets the padding of the [`Scrollbar`].
///
/// The padding is added at the top and bottom of the scrollbar (or at the
/// left and right ends for a horizontal [`Scrollbar`]) when it is visible.
///
/// Unlike [`Self::margin`] and [`Self::spacing`], the padding does not
/// affect the layout of the [`Scrollable`].
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self { pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
self.padding = padding.into().0.max(0.0); self.padding = padding.into().0;
self self
} }
} }
@ -675,13 +645,7 @@ where
let translation = let translation =
state.translation(self.direction, bounds, content_bounds); state.translation(self.direction, bounds, content_bounds);
operation.scrollable( operation.pre_operation(Some(&self.id));
Some(&self.id),
bounds,
content_bounds,
translation,
state,
);
operation.traverse(&mut |operation| { operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate( self.content.as_widget_mut().operate(
@ -695,6 +659,14 @@ where
operation, operation,
); );
}); });
// XXX must be done after traversing to perform DFS
operation.scrollable(
Some(&self.id),
bounds,
content_bounds,
translation,
state,
);
} }
fn update( fn update(
@ -2411,13 +2383,16 @@ impl Scrollbars {
let total_scrollbar_width = let total_scrollbar_width =
width.max(scroller_width) + 2.0 * margin; width.max(scroller_width) + 2.0 * margin;
// The padding is purely visual: it shrinks the top and bottom of the
// scrollbar without affecting the layout
let scrollbar_height = (bounds.height - x_scrollbar_height - 2.0 * padding).max(0.0);
// Total bounds of the scrollbar + margin + scroller width // Total bounds of the scrollbar + margin + scroller width
let total_scrollbar_bounds = Rectangle { let total_scrollbar_bounds = Rectangle {
x: bounds.x + bounds.width - total_scrollbar_width, x: bounds.x + bounds.width - total_scrollbar_width,
y: bounds.y + padding, y: bounds.y + padding,
width: total_scrollbar_width, width: total_scrollbar_width,
height: (bounds.height - x_scrollbar_height - 2.0 * padding) height: scrollbar_height,
.max(0.0),
}; };
// Bounds of just the scrollbar // Bounds of just the scrollbar
@ -2427,8 +2402,7 @@ impl Scrollbars {
- width / 2.0, - width / 2.0,
y: bounds.y + padding, y: bounds.y + padding,
width, width,
height: (bounds.height - x_scrollbar_height - 2.0 * padding) height: scrollbar_height,
.max(0.0),
}; };
let ratio = bounds.height / content_bounds.height; let ratio = bounds.height / content_bounds.height;
@ -2485,12 +2459,15 @@ impl Scrollbars {
let total_scrollbar_height = let total_scrollbar_height =
width.max(scroller_width) + 2.0 * margin; width.max(scroller_width) + 2.0 * margin;
// The padding is purely visual: it shrinks the left and right ends of
// the scrollbar without affecting the layout
let scrollbar_width = (bounds.width - scrollbar_y_width - 2.0 * padding).max(0.0);
// Total bounds of the scrollbar + margin + scroller width // Total bounds of the scrollbar + margin + scroller width
let total_scrollbar_bounds = Rectangle { let total_scrollbar_bounds = Rectangle {
x: bounds.x + padding, x: bounds.x + padding,
y: bounds.y + bounds.height - total_scrollbar_height, y: bounds.y + bounds.height - total_scrollbar_height,
width: (bounds.width - scrollbar_y_width - 2.0 * padding) width: scrollbar_width,
.max(0.0),
height: total_scrollbar_height, height: total_scrollbar_height,
}; };
@ -2500,8 +2477,7 @@ impl Scrollbars {
y: bounds.y + bounds.height y: bounds.y + bounds.height
- total_scrollbar_height / 2.0 - total_scrollbar_height / 2.0
- width / 2.0, - width / 2.0,
width: (bounds.width - scrollbar_y_width - 2.0 * padding) width: scrollbar_width,
.max(0.0),
height: width, height: width,
}; };

View file

@ -545,6 +545,7 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter_settings: Highlighter::Settings, highlighter_settings: Highlighter::Settings,
highlighter_format_address: usize, highlighter_format_address: usize,
context_menu_position: Option<Point>, context_menu_position: Option<Point>,
clipboard_has_text: bool,
pending_edit: Option<Action>, pending_edit: Option<Action>,
} }
@ -632,6 +633,7 @@ where
highlighter_settings: self.highlighter_settings.clone(), highlighter_settings: self.highlighter_settings.clone(),
highlighter_format_address: self.highlighter_format as usize, highlighter_format_address: self.highlighter_format as usize,
context_menu_position: None, context_menu_position: None,
clipboard_has_text: false,
pending_edit: None, pending_edit: None,
}) })
} }
@ -736,6 +738,8 @@ where
state.focus = Some(Focus::now()); state.focus = Some(Focus::now());
} }
state.context_menu_position = Some(pos); state.context_menu_position = Some(pos);
state.clipboard_has_text =
widget::text::clipboard_has_text(clipboard);
shell.capture_event(); shell.capture_event();
return; return;
} }
@ -1649,6 +1653,16 @@ where
self.on_edit.is_some() self.on_edit.is_some()
} }
fn has_text(&self, _tree: &widget::Tree) -> bool {
!self.content.is_empty()
}
fn clipboard_has_text(&self, tree: &widget::Tree) -> bool {
tree.state
.downcast_ref::<State<Highlighter>>()
.clipboard_has_text
}
fn is_focused(&self, tree: &widget::Tree) -> bool { fn is_focused(&self, tree: &widget::Tree) -> bool {
tree.state.downcast_ref::<State<Highlighter>>().is_focused() tree.state.downcast_ref::<State<Highlighter>>().is_focused()
} }

View file

@ -269,6 +269,13 @@ pub fn window_event(
scale_factor, scale_factor,
))) )))
} }
winit::event::PointerSource::TabletTool { .. } => {
let position = position.to_logical::<f64>(scale_factor);
Some(Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(position.x as f32, position.y as f32),
}))
}
_ => None, _ => None,
}, },
// TODO how to handle this for Touch? Is it different than a press? Need to double check winit impl // TODO how to handle this for Touch? Is it different than a press? Need to double check winit impl
@ -306,6 +313,24 @@ pub fn window_event(
} }
})) }))
} }
WindowEvent::PointerButton {
button: ButtonSource::TabletTool { button, .. },
state,
..
} => {
let button: Option<winit::event::MouseButton> = button.into();
button.map(mouse_button).map(|button| {
Event::Mouse(match state {
winit::event::ElementState::Pressed => {
mouse::Event::ButtonPressed(button)
}
winit::event::ElementState::Released => {
mouse::Event::ButtonReleased(button)
}
})
})
}
WindowEvent::PointerButton { WindowEvent::PointerButton {
button: ButtonSource::Touch { finger_id, force }, button: ButtonSource::Touch { finger_id, force },
state, state,

View file

@ -69,6 +69,8 @@ use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Action, Task}; use crate::runtime::{Action, Task};
use program::Program; use program::Program;
#[cfg(feature = "program")]
pub use program::Program;
use window::WindowManager; use window::WindowManager;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -1328,6 +1330,17 @@ async fn run_instance<P>(
continue; continue;
} }
} }
// to avoid race with the reported modifier state from the popup of
// this window, don't empty the modifiers, on focus loss
#[cfg(wayland_platform)]
if let winit::event::WindowEvent::ModifiersChanged(modifiers) =
&event
&& modifiers.state().is_empty()
&& platform_specific_handler.has_popup(id)
{
continue;
}
match event { match event {
winit::event::WindowEvent::SurfaceResized(_) => { winit::event::WindowEvent::SurfaceResized(_) => {
window.raw.request_redraw(); window.raw.request_redraw();
@ -2521,6 +2534,8 @@ where
while let Some(mut operation) = current_operation.take() { while let Some(mut operation) = current_operation.take() {
for (id, ui) in interfaces.iter_mut() { for (id, ui) in interfaces.iter_mut() {
if let Some(window) = window_manager.get_mut(*id) { if let Some(window) = window_manager.get_mut(*id) {
operation.as_mut().set_window_id(*id);
ui.operate(&window.renderer, operation.as_mut()); ui.operate(&window.renderer, operation.as_mut());
} }
} }
@ -2639,10 +2654,10 @@ where
.and_then(|window| theme::Base::palette(window.state.theme())) .and_then(|window| theme::Base::palette(window.state.theme()))
}); });
cached_user_interfaces window_manager
.drain() .iter_mut()
.filter_map(|(id, cache)| { .filter_map(|(id, window)| {
let window = window_manager.get_mut(id)?; let cache = cached_user_interfaces.remove(&id)?;
Some(( Some((
id, id,
@ -2715,7 +2730,5 @@ fn system_information(
graphics_backend: graphics.backend, graphics_backend: graphics.backend,
} }
} }
#[cfg(feature = "program")]
pub use program::Program;
pub use platform_specific::*; pub use platform_specific::*;

View file

@ -74,6 +74,11 @@ impl PlatformSpecific {
} }
} }
#[cfg(wayland_platform)]
pub(crate) fn has_popup(&self, toplevel: window::Id) -> bool {
self.wayland.has_popup(toplevel)
}
pub(crate) fn retain_subsurfaces<F: Fn(window::Id) -> bool>( pub(crate) fn retain_subsurfaces<F: Fn(window::Id) -> bool>(
&mut self, &mut self,
keep: F, keep: F,

View file

@ -138,3 +138,14 @@ pub fn set_input_zone<Message>(
)), )),
)) ))
} }
pub fn set_show_on_lock<Message>(
id: SurfaceId,
value: bool,
) -> Task<Message> {
task::effect(Action::PlatformSpecific(
platform_specific::Action::Wayland(wayland::Action::LayerSurface(
wayland::layer_surface::Action::ShowOnLock { id, value },
)),
))
}

View file

@ -392,6 +392,7 @@ impl SctkEventLoop {
).ok(), ).ok(),
text_input_manager: TextInputManager::try_new(&registry_state, &qh), text_input_manager: TextInputManager::try_new(&registry_state, &qh),
ext_background_effect_manager: ext_background_effect::ExtBackgroundEffectManager::new(&globals, &qh).ok(), ext_background_effect_manager: ext_background_effect::ExtBackgroundEffectManager::new(&globals, &qh).ok(),
cosmic_session_lock_layer_manager: registry_state.bind_one(&qh, 1..=1, ()).ok(),
registry_state, registry_state,
queue_handle: qh, queue_handle: qh,
@ -411,6 +412,7 @@ impl SctkEventLoop {
lock_surfaces: Vec::new(), lock_surfaces: Vec::new(),
subsurfaces: Vec::new(), subsurfaces: Vec::new(),
touch_points: HashMap::new(), touch_points: HashMap::new(),
kbd_leave_to_own_popup: HashSet::new(),
sctk_events: Vec::new(), sctk_events: Vec::new(),
frame_status: HashMap::new(), frame_status: HashMap::new(),
cursor_requests: HashMap::new(), cursor_requests: HashMap::new(),

View file

@ -44,6 +44,7 @@ use cctk::{
cosmic_protocols::{ cosmic_protocols::{
corner_radius::v1::client::cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1, corner_radius::v1::client::cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1,
overlap_notify::v1::client::zcosmic_overlap_notification_v1::ZcosmicOverlapNotificationV1, overlap_notify::v1::client::zcosmic_overlap_notification_v1::ZcosmicOverlapNotificationV1,
session_lock_layer::v1::client::cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1,
}, },
sctk::{ sctk::{
activation::{ActivationState, RequestData}, activation::{ActivationState, RequestData},
@ -434,6 +435,9 @@ pub struct SctkState {
pub(crate) blur_surfaces: HashMap<core::window::Id, ExtBackgroundEffectSurfaceV1>, pub(crate) blur_surfaces: HashMap<core::window::Id, ExtBackgroundEffectSurfaceV1>,
pub(crate) corner_radii: HashMap<core::window::Id, (SctkCornerRadius, Option<CornerRadius>)>, pub(crate) corner_radii: HashMap<core::window::Id, (SctkCornerRadius, Option<CornerRadius>)>,
pub(crate) touch_points: HashMap<touch::Finger, (WlSurface, Point)>, pub(crate) touch_points: HashMap<touch::Finger, (WlSurface, Point)>,
/// List of layer surfaces whose keyboard leave was ignored because focus moved
/// to one of their own grabbing popups. Their next enter is swallowed too.
pub(crate) kbd_leave_to_own_popup: HashSet<ObjectId>,
/// Window updates, which are coming from SCTK or the compositor, which require /// Window updates, which are coming from SCTK or the compositor, which require
/// calling back to the sctk's downstream. They are handled right in the event loop, /// calling back to the sctk's downstream. They are handled right in the event loop,
@ -477,6 +481,7 @@ pub struct SctkState {
pub(crate) toplevel_manager: Option<ToplevelManagerState>, pub(crate) toplevel_manager: Option<ToplevelManagerState>,
pub(crate) subsurface_state: Option<SubsurfaceState>, pub(crate) subsurface_state: Option<SubsurfaceState>,
pub(crate) ext_background_effect_manager: Option<ext_background_effect::ExtBackgroundEffectManager>, pub(crate) ext_background_effect_manager: Option<ext_background_effect::ExtBackgroundEffectManager>,
pub(crate) cosmic_session_lock_layer_manager: Option<CosmicSessionLockLayerManagerV1>,
pub(crate) activation_token_ctr: u32, pub(crate) activation_token_ctr: u32,
pub(crate) token_senders: HashMap<u32, oneshot::Sender<Option<String>>>, pub(crate) token_senders: HashMap<u32, oneshot::Sender<Option<String>>>,
@ -679,6 +684,33 @@ impl SctkState {
// TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well.
} }
/// Popups can only be attached to xdg or layer surfaces. So for subsurfaces, we return the
/// parent, and adjust the offset.
pub(crate) fn subsurface_popup_parent(
&self,
id: core::window::Id,
) -> Option<(WlSurface, (i32, i32))> {
let mut sub = self.subsurfaces.iter().find(|s| s.id == id)?;
let mut offset = (0, 0);
// bounded by the number of subsurfaces so a corrupt chain cannot spin forever
for _ in 0..=self.subsurfaces.len() {
if let Some(b) = sub.instance.bounds {
offset.0 += b.x as i32;
offset.1 += b.y as i32;
}
let parent = &sub.instance.parent;
match self
.subsurfaces
.iter()
.find(|s| s.instance.wl_surface == *parent)
{
Some(s) => sub = s,
None => return Some((parent.clone(), offset)),
}
}
None
}
pub fn get_popup( pub fn get_popup(
&mut self, &mut self,
settings: SctkPopupSettings, settings: SctkPopupSettings,
@ -723,6 +755,27 @@ impl SctkState {
PopupParent::Popup(parent.popup.wl_surface().clone()), PopupParent::Popup(parent.popup.wl_surface().clone()),
parent.data.toplevel.clone(), parent.data.toplevel.clone(),
) )
} else if let Some((surface, _)) =
self.subsurface_popup_parent(settings.parent)
{
if self
.layer_surfaces
.iter()
.any(|l| *l.surface.wl_surface() == surface)
{
(PopupParent::LayerSurface(surface.clone()), surface)
} else if self
.windows
.iter()
.any(|w| w.wl_surface(&self.connection) == surface)
{
(PopupParent::Window(surface.clone()), surface)
} else if let Some(p) = self.popmgr.popup(&surface) {
(PopupParent::Popup(surface), p.data.toplevel.clone())
} else {
// e.g. a session lock surface, which cannot parent xdg popups
return Err(PopupCreationError::ParentMissing);
}
} else { } else {
return Err(PopupCreationError::ParentMissing); return Err(PopupCreationError::ParentMissing);
}; };
@ -1156,6 +1209,7 @@ impl SctkState {
} }
if let Some(i) = self.layer_surfaces.iter().position(|l| l.id == id) { if let Some(i) = self.layer_surfaces.iter().position(|l| l.id == id) {
let l = self.layer_surfaces.remove(i); let l = self.layer_surfaces.remove(i);
_ = self.kbd_leave_to_own_popup.remove(&l.surface.wl_surface().id());
if let Some(blurred) = self.blur_surfaces.remove(&l.id) { if let Some(blurred) = self.blur_surfaces.remove(&l.id) {
blurred.destroy(); blurred.destroy();
@ -1266,16 +1320,36 @@ impl SctkState {
} }
} }
}, },
platform_specific::wayland::layer_surface::Action::ShowOnLock { id, value } => {
if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) {
if let SurfaceKind::Wlr(wlr) = layer_surface.surface.kind() {
if let Some(manager) = &self.cosmic_session_lock_layer_manager {
if value {
manager.set_show_on_lock(wlr);
} else {
manager.unset_show_on_lock(wlr);
}
_ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone());
}
}
}
}
}, },
Action::Popup(action) => { Action::Popup(action) => {
match action { match action {
platform_specific::wayland::popup::Action::Popup { popup: settings } => { platform_specific::wayland::popup::Action::Popup { popup: mut settings } => {
// anchor rects from widgets inside a subsurface are in that subsurface's coordinates
if let Some((_, (dx, dy))) = self.subsurface_popup_parent(settings.parent) {
settings.positioner.anchor_rect.x += dx;
settings.positioner.anchor_rect.y += dy;
}
// first check existing popup // first check existing popup
// if an existing popup with the same id exists, we just update the positioner. // if an existing popup with the same id exists, we just update the positioner.
if let Some((chain, existing)) = self.popmgr.popup_id(settings.id).and_then(|p| if let Some((chain, existing)) = self.popmgr.popup_id(settings.id).and_then(|p|
(p.data.grab == settings.grab && self.popmgr.popups().any(|parent| parent.popup.wl_surface() == p.data.parent.wl_surface() && parent.data.id == settings.parent) (p.data.grab == settings.grab && self.popmgr.popups().any(|parent| parent.popup.wl_surface() == p.data.parent.wl_surface() && parent.data.id == settings.parent)
|| self.windows.iter().any(|w| w.id == settings.parent && *p.data.parent.wl_surface() == w.wl_surface(&self.connection)) || self.windows.iter().any(|w| w.id == settings.parent && *p.data.parent.wl_surface() == w.wl_surface(&self.connection))
|| self.layer_surfaces.iter().any(|l| l.id == settings.parent && p.data.parent.wl_surface() == l.surface.wl_surface())).then(|| || self.layer_surfaces.iter().any(|l| l.id == settings.parent && p.data.parent.wl_surface() == l.surface.wl_surface())
|| (p.data.parent_window == settings.parent && self.subsurfaces.iter().any(|s| s.id == settings.parent))).then(||
{ {
p.popup.wl_surface().clone() p.popup.wl_surface().clone()
})).and_then(|s| self.popmgr.chain_for_popup_mut(&s)) })).and_then(|s| self.popmgr.chain_for_popup_mut(&s))
@ -1646,9 +1720,13 @@ impl SctkState {
} }
}, },
subsurface::Action::Reposition { id, x, y } => { subsurface::Action::Reposition { id, x, y } => {
if let Some(subsurface) = self.subsurfaces.iter().find(|s| s.id == id) { if let Some(subsurface) = self.subsurfaces.iter_mut().find(|s| s.id == id) {
subsurface.instance.wl_subsurface.set_position(x, y); subsurface.instance.wl_subsurface.set_position(x, y);
subsurface.instance.wl_surface.commit(); subsurface.instance.wl_surface.commit();
if let Some(b) = subsurface.instance.bounds.as_mut() {
b.x = x as f32;
b.y = y as f32;
}
} }
}, },
}, },
@ -2165,3 +2243,4 @@ pub(crate) fn send_event(
delegate_noop!(SctkState: ignore WlSubsurface); delegate_noop!(SctkState: ignore WlSubsurface);
delegate_noop!(SctkState: ignore WlRegion); delegate_noop!(SctkState: ignore WlRegion);
delegate_noop!(SctkState: CosmicSessionLockLayerManagerV1);

View file

@ -1,12 +1,16 @@
use crate::platform_specific::wayland::{ use crate::platform_specific::wayland::{
event_loop::state::SctkState, event_loop::state::{PopupParent, SctkState},
sctk_event::{KeyboardEventVariant, SctkEvent}, sctk_event::{KeyboardEventVariant, SctkEvent},
}; };
use cctk::sctk::{ use cctk::sctk::{
delegate_keyboard, delegate_keyboard,
seat::keyboard::{KeyboardHandler, Keysym, Modifiers}, seat::keyboard::{KeyboardHandler, Keysym, Modifiers},
}; };
use cctk::sctk::{reexports::client::Proxy, seat::keyboard::RawModifiers}; use cctk::sctk::{
reexports::client::Proxy,
seat::keyboard::RawModifiers,
shell::{WaylandSurface, wlr_layer::KeyboardInteractivity},
};
fn modifiers_from_keysyms( fn modifiers_from_keysyms(
keysyms: &[Keysym], keysyms: &[Keysym],
@ -90,6 +94,11 @@ impl KeyboardHandler for SctkState {
} }
self.request_redraw(&surface); self.request_redraw(&surface);
// Focus is coming back from our own grabbing popup
if self.kbd_leave_to_own_popup.remove(&surface.id()) {
return;
}
let surfaces = self.subsurfaces.iter().filter_map(|s| { let surfaces = self.subsurfaces.iter().filter_map(|s| {
(s.instance.parent == *surface).then(|| &s.instance.wl_surface) (s.instance.parent == *surface).then(|| &s.instance.wl_surface)
}); });
@ -147,6 +156,32 @@ impl KeyboardHandler for SctkState {
_ = my_seat.kbd_focus.take(); _ = my_seat.kbd_focus.take();
(is_active, seat, kbd) (is_active, seat, kbd)
}; };
// A grabbing popup parented to this layer surface, or opened from one of
// its subsurfaces that holds focus, takes keyboard focus.
// Only swallow the leave when the layer surface has exclusive keyboard interactivity.
let focus_moved_to_own_popup = self.popmgr.popups().any(|p| {
p.data.grab
&& match &p.data.parent {
PopupParent::LayerSurface(s) => {
(s == surface
|| self.subsurfaces.iter().any(|sub| {
sub.id == p.data.parent_window
&& sub.instance.wl_surface == *surface
}))
&& self.layer_surfaces.iter().any(|l| {
l.surface.wl_surface() == s
&& l.keyboard_interactivity
== KeyboardInteractivity::Exclusive
})
}
_ => false,
}
});
if focus_moved_to_own_popup {
_ = self.kbd_leave_to_own_popup.insert(surface.id());
return;
}
let surfaces = self.subsurfaces.iter().filter_map(|s| { let surfaces = self.subsurfaces.iter().filter_map(|s| {
(s.instance.parent == *surface).then(|| &s.instance.wl_surface) (s.instance.parent == *surface).then(|| &s.instance.wl_surface)
}); });
@ -318,6 +353,31 @@ impl KeyboardHandler for SctkState {
surface: surface.clone(), surface: surface.clone(),
}); });
} }
// A popup holds the seat's keyboard on behalf of its toplevel. Report
// the modifiers to the toplevel as well.
let toplevel = self.popmgr.popups().find_map(|p| {
(p.popup.wl_surface() == &surface)
.then(|| p.data.toplevel.clone())
});
if let Some(toplevel) = toplevel {
let id = self
.windows
.iter()
.find(|w| w.wl_surface(&self.connection) == toplevel)
.map(|w| w.id)
.or_else(|| {
self.layer_surfaces
.iter()
.find(|l| *l.surface.wl_surface() == toplevel)
.map(|l| l.id)
});
if let Some(toplevel) = id {
self.sctk_events.push(SctkEvent::PopupModifiers {
toplevel,
modifiers,
});
}
}
} }
} }
} }

View file

@ -20,7 +20,7 @@ use iced_graphics::{Compositor, compositor};
use iced_runtime::core::{Vector, window}; use iced_runtime::core::{Vector, window};
use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle}; use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
use raw_window_handle::{HasRawDisplayHandle, RawWindowHandle}; use raw_window_handle::{HasRawDisplayHandle, RawWindowHandle};
use sctk_event::SctkEvent; use sctk_event::{PopupEventVariant, SctkEvent};
use std::sync::OnceLock; use std::sync::OnceLock;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; use subsurface_widget::{SubsurfaceInstance, SubsurfaceState};
@ -103,6 +103,7 @@ pub(crate) struct WaylandSpecific {
surface_ids: HashMap<ObjectId, SurfaceIdWrapper>, surface_ids: HashMap<ObjectId, SurfaceIdWrapper>,
subsurface_state: Option<SubsurfaceState>, subsurface_state: Option<SubsurfaceState>,
surface_subsurfaces: HashMap<window::Id, Vec<SubsurfaceInstance>>, surface_subsurfaces: HashMap<window::Id, Vec<SubsurfaceInstance>>,
popup_toplevels: HashMap<ObjectId, window::Id>,
} }
impl PlatformSpecific { impl PlatformSpecific {
@ -173,6 +174,11 @@ impl WaylandSpecific {
self.conn.as_ref() self.conn.as_ref()
} }
/// Whether a popup opened from `toplevel` is alive.
pub(crate) fn has_popup(&self, toplevel: window::Id) -> bool {
self.popup_toplevels.values().any(|id| *id == toplevel)
}
pub(crate) async fn handle_event<'a, 'b, P>( pub(crate) async fn handle_event<'a, 'b, P>(
&mut self, &mut self,
e: SctkEvent, e: SctkEvent,
@ -201,8 +207,33 @@ impl WaylandSpecific {
modifiers, modifiers,
subsurface_state, subsurface_state,
surface_subsurfaces, surface_subsurfaces,
popup_toplevels,
} = self; } = self;
if let SctkEvent::PopupEvent {
variant,
parent_id,
parent_window,
id,
..
} = &e
{
match variant {
PopupEventVariant::Created(..) => {
// A nested popup's parent is itself a popup so inhreit its toplevel
let toplevel = popup_toplevels
.get(&parent_id.id())
.copied()
.unwrap_or(*parent_window);
_ = popup_toplevels.insert(id.id(), toplevel);
}
PopupEventVariant::Done => {
_ = popup_toplevels.remove(&id.id());
}
_ => {}
}
}
match e { match e {
sctk_event => { sctk_event => {
let Some(sender) = sender.as_ref() else { let Some(sender) = sender.as_ref() else {

View file

@ -198,6 +198,10 @@ pub enum SctkEvent {
SessionUnlocked, SessionUnlocked,
SurfaceScaleFactorChanged(f64, WlSurface, window::Id), SurfaceScaleFactorChanged(f64, WlSurface, window::Id),
Winit(WindowId, WindowEvent), Winit(WindowId, WindowEvent),
PopupModifiers {
toplevel: window::Id,
modifiers: cctk::sctk::seat::keyboard::Modifiers,
},
Subcompositor(SubsurfaceState), Subcompositor(SubsurfaceState),
ShortcutsInhibited(bool), ShortcutsInhibited(bool),
} }
@ -623,7 +627,12 @@ impl SctkEvent {
keyboard::Event::KeyPressed { keyboard::Event::KeyPressed {
key: key.clone(), key: key.clone(),
location: location, location: location,
text: ke.utf8.map(|s| s.into()), // sctk reports keys without a text representation (e.g. arrows, Home) as
// Some("") but winit reports None. Widgets are written against winit's contract.
text: ke
.utf8
.filter(|s| !s.is_empty())
.map(|s| s.into()),
modifiers: modifiers_to_native(*modifiers), modifiers: modifiers_to_native(*modifiers),
physical_key, physical_key,
repeat: false, repeat: false,
@ -649,7 +658,9 @@ impl SctkEvent {
keyboard::Event::KeyPressed { keyboard::Event::KeyPressed {
key: key.clone(), key: key.clone(),
location: location, location: location,
text: utf8.map(|s| s.into()), text: utf8
.filter(|s| !s.is_empty())
.map(|s| s.into()),
modifiers: modifiers_to_native(*modifiers), modifiers: modifiers_to_native(*modifiers),
physical_key, physical_key,
repeat: true, repeat: true,
@ -1400,6 +1411,19 @@ impl SctkEvent {
), ),
)), )),
SctkEvent::Winit(_, _) => {} SctkEvent::Winit(_, _) => {}
SctkEvent::PopupModifiers {
toplevel,
modifiers,
} => {
events.push((
Some(toplevel),
iced_runtime::core::Event::Keyboard(
keyboard::Event::ModifiersChanged(modifiers_to_native(
modifiers,
)),
),
));
}
SctkEvent::SurfaceScaleFactorChanged(scale, _, id) => { SctkEvent::SurfaceScaleFactorChanged(scale, _, id) => {
if let Some(w) = window_manager.get_mut(id) { if let Some(w) = window_manager.get_mut(id) {
w.state.update_scale_factor(scale); w.state.update_scale_factor(scale);