Compare commits

...

15 commits

Author SHA1 Message Date
24da1dc0e4 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
2026-09-24 20:45:11 +02:00
Hojjat
0968a10fc1 fix: arrow keys in text editor on a layer_surface 2026-09-22 01:10:34 -04:00
Hojjat
6f1b3a7170 fix(wayland): share a grabbing popup's modifiers with its toplevel 2026-09-21 17:43:21 -06:00
Hojjat
5716b9a691 fix: build user interfaces in window id order 2026-09-21 17:43:21 -06:00
Ashley Wulber
5e2b580791 chore: update accesskit 2026-09-21 12:13:00 -04:00
Vukašin Vojinović
021cb93fa3 improv(scrollable): match padding impl to upstream 2026-09-18 20:57:23 +02:00
Blake Gardner
ffc3ac317e fix(test): build iced_test against this fork's runtime and renderer style
`iced_test` has not compiled here since it arrived from upstream: it
matches `runtime::Action` exhaustively without the fork's `Dnd` and
`PlatformSpecific` variants, and builds `renderer::Style` without the
fork's `icon_color` and `scale_factor` fields. Nothing else in the
workspace depends on it, so the errors only show up in
`cargo test --workspace` and for anyone adding the crate to an
application.

The emulator ignores the two actions: drag and drop and platform
surfaces need a windowing system, which it has none of. The style
fields follow what the winit runtime passes: the icon color is the
text color, and the scale factor is the one the emulator or simulator
already renders its screenshot at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 23:29:51 -04:00
Hojjat
85d4e4d5c2 chore: update winit to support moving windows with touchscreen 2026-09-16 15:08:25 -04:00
Hojjat
890a53fef1 fix: add tablet support for toplevel windows 2026-09-16 06:57:02 -06:00
Hojjat
5fc4372d87 fix: do not report unfocused if focus moved to surface's own popup 2026-09-15 19:05:32 -04:00
Ian Douglas Scott
87e1787fbb cosmic-session-lock-layer-v1 protocol 2026-09-11 13:06:42 -06:00
Hojjat
9ef36c9604 feat(text): add selected_text_color to the text style 2026-09-11 19:34:51 +02:00
Ashley Wulber
2e12eba5cd improv: PreOperation for tracking children in selections 2026-09-11 11:45:52 -04:00
Ashley Wulber
880f2c7565 improv: operation and select for directional navigation 2026-09-11 11:45:52 -04:00
Hojjat
d0f221c48b feat(text): expose has_text and clipboard_has_text for context menus 2026-09-11 11:34:02 -04:00
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"]
[dependencies]
accesskit = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14" }
accesskit_windows = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true }
accesskit_macos = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true }
accesskit_winit = { git = "https://github.com/wash2/accesskit", tag = "cosmic-0.14", optional = true, default-features = false, features = [
accesskit = { git = "https://github.com/wash2/accesskit", rev = "6c20249" }
accesskit_windows = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true }
accesskit_macos = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true }
accesskit_winit = { git = "https://github.com/wash2/accesskit", rev = "6c20249", optional = true, default-features = false, features = [
"rwh_06",
] }
# accesskit = { path = "../../../accesskit/common" }

View file

@ -9,7 +9,7 @@ pub use scrollable::Scrollable;
pub use text_input::TextInput;
use crate::widget::Id;
use crate::{Rectangle, Vector};
use crate::{Rectangle, Vector, window};
use std::any::Any;
use std::fmt;
@ -75,6 +75,12 @@ pub trait Operation<T = ()>: Send {
fn finish(&self) -> Outcome<T> {
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>
@ -140,6 +146,14 @@ where
fn finish(&self) -> Outcome<O> {
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`].
@ -243,6 +257,14 @@ where
fn finish(&self) -> Outcome<O> {
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 }
@ -341,6 +363,10 @@ where
) {
self.operation.custom(id, bounds, state);
}
fn pre_operation(&mut self, id: Option<&Id>) {
self.operation.pre_operation(id);
}
}
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 {
@ -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 {
@ -566,6 +608,14 @@ pub fn scoped<T: 'static>(
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 {

View file

@ -224,6 +224,7 @@ pub struct State<P: Paragraph> {
focused: bool,
keyboard_focused: bool,
context_menu_position: Option<Point>,
clipboard_has_text: bool,
}
impl<P: Paragraph> Default for State<P> {
@ -234,6 +235,7 @@ impl<P: Paragraph> Default for State<P> {
focused: false,
keyboard_focused: false,
context_menu_position: None,
clipboard_has_text: false,
}
}
}
@ -377,45 +379,65 @@ where
let style = theme.style(&self.class);
let bounds = layout.bounds();
let paragraph = state.paragraph.raw();
if let Some(sel) = &state.selection {
let left = sel.anchor.min(sel.end);
let right = sel.anchor.max(sel.end);
let content: &str = self.fragment.as_ref();
let anchor = bounds.anchor(
paragraph.min_bounds(),
paragraph.align_x(),
paragraph.align_y(),
);
if left != right {
let lo_byte = grapheme_to_byte(content, left);
let hi_byte = grapheme_to_byte(content, right);
let rects: Vec<Rectangle> = state
.selection
.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.min_bounds(),
paragraph.align_x(),
paragraph.align_y(),
paragraph
.highlight(
0,
(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(
0,
(lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before),
);
for r in rects {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: anchor.x + r.x,
y: anchor.y + r.y,
width: r.width,
height: r.height,
},
..renderer::Quad::default()
},
style.selected_fill,
);
}
if style.selected_text_color.is_none() {
fill_selection(renderer);
}
draw(renderer, defaults, bounds, paragraph, style, viewport);
if let Some(color) = style.selected_text_color {
fill_selection(renderer);
for r in &rects {
renderer.with_layer(*r, |renderer| {
renderer
.fill_paragraph(paragraph, anchor, color, *viewport);
});
}
}
draw(renderer, defaults, bounds, paragraph, style, viewport);
}
fn update(
@ -529,6 +551,7 @@ where
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
if let Some(pos) = cursor.position_over(bounds) {
state.context_menu_position = Some(pos);
state.clipboard_has_text = clipboard_has_text(clipboard);
state.focused = true;
state.keyboard_focused = false;
shell.capture_event();
@ -928,6 +951,10 @@ pub struct Style {
pub color: Option<Color>,
/// The fill [`Color`] of the selection highlight.
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 {
@ -935,6 +962,7 @@ impl Default for Style {
Self {
color: None,
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;
/// 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
/// text selection (Copy, Select All, and optionally Cut / Paste) in libcosmic
pub trait HasSelectableText {
@ -1032,11 +1067,23 @@ pub trait HasSelectableText {
/// Selects all text.
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 {
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.
fn is_focused(&self, tree: &WidgetTree) -> bool;
@ -1106,6 +1153,16 @@ impl<Theme: Catalog, Renderer: text::Renderer> HasSelectableText
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 {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
state.is_focused()

View file

@ -157,6 +157,11 @@ pub enum Action {
/// padding of the surface geometry
padding: IcedMargin,
},
ShowOnLock {
/// id of the layer surface
id: Id,
value: bool,
}
}
impl fmt::Debug for Action {
@ -215,6 +220,11 @@ impl fmt::Debug for Action {
"Action::LayerSurfaceAction::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.
pub use iced_selector::{
Bounded, Candidate, Selector, Target, Text, id, is_focused,
Bounded, Candidate, Selector, Target, Text, focus, id, is_focused,
};
use crate::Task;

View file

@ -1,3 +1,5 @@
use iced_core::window;
use crate::Selector;
use crate::core::widget::operation::{
Focusable, Outcome, Scrollable, TextInput,
@ -100,6 +102,10 @@ where
fn finish(&self) -> Self::Output {
self.outputs.clone()
}
fn set_window_id(&mut self, id: window::Id) {
self.selector.set_window_id(id);
}
}
pub trait Strategy {
@ -110,6 +116,8 @@ pub trait Strategy {
fn is_done(&self) -> bool;
fn finish(&self) -> Self::Output;
fn set_window_id(&mut self, _id: window::Id) {}
}
#[derive(Debug)]
@ -273,4 +281,12 @@ where
fn finish(&self) -> Outcome<S::Output> {
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.
use iced_core as core;
use iced_core::window;
mod find;
mod target;
@ -48,6 +49,8 @@ pub trait Selector {
{
FindAll::new(find::All::new(self))
}
fn set_window_id(&mut self, _id: window::Id) {}
}
impl Selector for &str {
@ -171,3 +174,42 @@ pub fn is_focused() -> impl Selector<Output = Target> {
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,
visible_bounds: Option<Rectangle>,
},
PreOperation {
id: Option<Id>,
},
}
impl Target {
@ -54,6 +57,7 @@ impl Target {
| Target::TextInput { bounds, .. }
| Target::Text { bounds, .. }
| Target::Custom { bounds, .. } => *bounds,
Target::PreOperation { .. } => Rectangle::default(),
}
}
@ -66,6 +70,7 @@ impl Target {
| Target::TextInput { visible_bounds, .. }
| Target::Text { visible_bounds, .. }
| Target::Custom { visible_bounds, .. } => *visible_bounds,
Target::PreOperation { .. } => None,
}
}
}
@ -138,6 +143,9 @@ impl From<Candidate<'_>> for Target {
bounds,
visible_bounds,
},
Candidate::PreOperation { id } => {
Self::PreOperation { id: id.cloned() }
}
}
}
}
@ -195,6 +203,8 @@ pub enum Candidate<'a> {
visible_bounds: Option<Rectangle>,
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> {
@ -206,7 +216,8 @@ impl<'a> Candidate<'a> {
| Candidate::Scrollable { id, .. }
| Candidate::TextInput { 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::Text { bounds, .. }
| Candidate::Custom { bounds, .. } => *bounds,
Candidate::PreOperation { .. } => Rectangle::default(),
}
}
@ -231,6 +243,7 @@ impl<'a> Candidate<'a> {
| Candidate::TextInput { visible_bounds, .. }
| Candidate::Text { 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 => {
// 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,
theme,
&renderer::Style {
icon_color: style.text_color,
text_color: style.text_color,
scale_factor: f64::from(scale_factor),
},
mouse::Cursor::Unavailable,
);

View file

@ -209,17 +209,19 @@ where
&mut self.messages,
);
let scale_factor = 2.0_f32;
self.raw.draw(
&mut self.renderer,
theme,
&core::renderer::Style {
icon_color: base.text_color,
text_color: base.text_color,
scale_factor: f64::from(scale_factor),
},
self.cursor,
);
let scale_factor = 2.0;
let physical_size = Size::new(
(self.size.width * 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_pressed: bool,
is_focused: bool,
needs_redraw: bool,
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@ -368,6 +369,12 @@ where
viewport,
);
let state = tree.state.downcast_mut::<State>();
if state.needs_redraw {
state.needs_redraw = false;
shell.request_redraw();
}
if shell.is_event_captured() {
return;
}
@ -777,11 +784,11 @@ impl Default for Style {
///
/// impl Catalog for MyTheme {
/// type Class<'a> = ButtonClass;
///
///
/// fn default<'a>() -> Self::Class<'a> {
/// ButtonClass::default()
/// }
///
///
///
/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
/// let mut style = Style::default();
@ -1000,10 +1007,12 @@ impl operation::Focusable for State {
}
fn focus(&mut self) {
self.needs_redraw |= !self.is_focused;
self.is_focused = true;
}
fn unfocus(&mut self) {
self.needs_redraw |= self.is_focused;
self.is_focused = false;
}
}

View file

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

View file

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

View file

@ -269,6 +269,13 @@ pub fn window_event(
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,
},
// 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 {
button: ButtonSource::Touch { finger_id, force },
state,

View file

@ -69,6 +69,8 @@ use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Action, Task};
use program::Program;
#[cfg(feature = "program")]
pub use program::Program;
use window::WindowManager;
use rustc_hash::FxHashMap;
@ -1328,6 +1330,17 @@ async fn run_instance<P>(
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 {
winit::event::WindowEvent::SurfaceResized(_) => {
window.raw.request_redraw();
@ -2521,6 +2534,8 @@ where
while let Some(mut operation) = current_operation.take() {
for (id, ui) in interfaces.iter_mut() {
if let Some(window) = window_manager.get_mut(*id) {
operation.as_mut().set_window_id(*id);
ui.operate(&window.renderer, operation.as_mut());
}
}
@ -2639,10 +2654,10 @@ where
.and_then(|window| theme::Base::palette(window.state.theme()))
});
cached_user_interfaces
.drain()
.filter_map(|(id, cache)| {
let window = window_manager.get_mut(id)?;
window_manager
.iter_mut()
.filter_map(|(id, window)| {
let cache = cached_user_interfaces.remove(&id)?;
Some((
id,
@ -2715,7 +2730,5 @@ fn system_information(
graphics_backend: graphics.backend,
}
}
#[cfg(feature = "program")]
pub use program::Program;
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>(
&mut self,
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(),
text_input_manager: TextInputManager::try_new(&registry_state, &qh),
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,
queue_handle: qh,
@ -411,6 +412,7 @@ impl SctkEventLoop {
lock_surfaces: Vec::new(),
subsurfaces: Vec::new(),
touch_points: HashMap::new(),
kbd_leave_to_own_popup: HashSet::new(),
sctk_events: Vec::new(),
frame_status: HashMap::new(),
cursor_requests: HashMap::new(),

View file

@ -44,6 +44,7 @@ use cctk::{
cosmic_protocols::{
corner_radius::v1::client::cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1,
overlap_notify::v1::client::zcosmic_overlap_notification_v1::ZcosmicOverlapNotificationV1,
session_lock_layer::v1::client::cosmic_session_lock_layer_manager_v1::CosmicSessionLockLayerManagerV1,
},
sctk::{
activation::{ActivationState, RequestData},
@ -434,6 +435,9 @@ pub struct SctkState {
pub(crate) blur_surfaces: HashMap<core::window::Id, ExtBackgroundEffectSurfaceV1>,
pub(crate) corner_radii: HashMap<core::window::Id, (SctkCornerRadius, Option<CornerRadius>)>,
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
/// 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) subsurface_state: Option<SubsurfaceState>,
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) 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.
}
/// 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(
&mut self,
settings: SctkPopupSettings,
@ -723,6 +755,27 @@ impl SctkState {
PopupParent::Popup(parent.popup.wl_surface().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 {
return Err(PopupCreationError::ParentMissing);
};
@ -1156,6 +1209,7 @@ impl SctkState {
}
if let Some(i) = self.layer_surfaces.iter().position(|l| l.id == id) {
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) {
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) => {
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
// 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|
(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.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()
})).and_then(|s| self.popmgr.chain_for_popup_mut(&s))
@ -1646,9 +1720,13 @@ impl SctkState {
}
},
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_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 WlRegion);
delegate_noop!(SctkState: CosmicSessionLockLayerManagerV1);

View file

@ -1,12 +1,16 @@
use crate::platform_specific::wayland::{
event_loop::state::SctkState,
event_loop::state::{PopupParent, SctkState},
sctk_event::{KeyboardEventVariant, SctkEvent},
};
use cctk::sctk::{
delegate_keyboard,
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(
keysyms: &[Keysym],
@ -90,6 +94,11 @@ impl KeyboardHandler for SctkState {
}
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| {
(s.instance.parent == *surface).then(|| &s.instance.wl_surface)
});
@ -147,6 +156,32 @@ impl KeyboardHandler for SctkState {
_ = my_seat.kbd_focus.take();
(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| {
(s.instance.parent == *surface).then(|| &s.instance.wl_surface)
});
@ -318,6 +353,31 @@ impl KeyboardHandler for SctkState {
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 raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
use raw_window_handle::{HasRawDisplayHandle, RawWindowHandle};
use sctk_event::SctkEvent;
use sctk_event::{PopupEventVariant, SctkEvent};
use std::sync::OnceLock;
use std::{collections::HashMap, sync::Arc};
use subsurface_widget::{SubsurfaceInstance, SubsurfaceState};
@ -103,6 +103,7 @@ pub(crate) struct WaylandSpecific {
surface_ids: HashMap<ObjectId, SurfaceIdWrapper>,
subsurface_state: Option<SubsurfaceState>,
surface_subsurfaces: HashMap<window::Id, Vec<SubsurfaceInstance>>,
popup_toplevels: HashMap<ObjectId, window::Id>,
}
impl PlatformSpecific {
@ -173,6 +174,11 @@ impl WaylandSpecific {
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>(
&mut self,
e: SctkEvent,
@ -201,8 +207,33 @@ impl WaylandSpecific {
modifiers,
subsurface_state,
surface_subsurfaces,
popup_toplevels,
} = 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 {
sctk_event => {
let Some(sender) = sender.as_ref() else {

View file

@ -198,6 +198,10 @@ pub enum SctkEvent {
SessionUnlocked,
SurfaceScaleFactorChanged(f64, WlSurface, window::Id),
Winit(WindowId, WindowEvent),
PopupModifiers {
toplevel: window::Id,
modifiers: cctk::sctk::seat::keyboard::Modifiers,
},
Subcompositor(SubsurfaceState),
ShortcutsInhibited(bool),
}
@ -623,7 +627,12 @@ impl SctkEvent {
keyboard::Event::KeyPressed {
key: key.clone(),
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),
physical_key,
repeat: false,
@ -649,7 +658,9 @@ impl SctkEvent {
keyboard::Event::KeyPressed {
key: key.clone(),
location: location,
text: utf8.map(|s| s.into()),
text: utf8
.filter(|s| !s.is_empty())
.map(|s| s.into()),
modifiers: modifiers_to_native(*modifiers),
physical_key,
repeat: true,
@ -1400,6 +1411,19 @@ impl SctkEvent {
),
)),
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) => {
if let Some(w) = window_manager.get_mut(id) {
w.state.update_scale_factor(scale);