Compare commits
15 commits
d12bb8e056
...
24da1dc0e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 24da1dc0e4 | |||
|
|
0968a10fc1 | ||
|
|
6f1b3a7170 | ||
|
|
5716b9a691 | ||
|
|
5e2b580791 | ||
|
|
021cb93fa3 | ||
|
|
ffc3ac317e | ||
|
|
85d4e4d5c2 | ||
|
|
890a53fef1 | ||
|
|
5fc4372d87 | ||
|
|
87e1787fbb | ||
|
|
9ef36c9604 | ||
|
|
2e12eba5cd | ||
|
|
880f2c7565 | ||
|
|
d0f221c48b |
23 changed files with 1602 additions and 1231 deletions
2125
Cargo.lock
generated
2125
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -392,6 +392,7 @@ impl SctkEventLoop {
|
|||
).ok(),
|
||||
text_input_manager: TextInputManager::try_new(®istry_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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue