improv: operation and select for directional navigation

This commit is contained in:
Ashley Wulber 2026-09-01 23:18:45 -04:00 • committed by Ashley Wulber
parent d0f221c48b
commit 880f2c7565
7 changed files with 98 additions and 12 deletions

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,9 @@ 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) {}
} }
impl<T, O> Operation<O> for Box<T> impl<T, O> Operation<O> for Box<T>
@ -140,6 +143,10 @@ 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);
}
} }
/// The result of an [`Operation`]. /// The result of an [`Operation`].
@ -243,6 +250,10 @@ where
fn finish(&self) -> Outcome<O> { fn finish(&self) -> Outcome<O> {
Outcome::None Outcome::None
} }
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
} }
BlackBox { operation } BlackBox { operation }
@ -410,6 +421,10 @@ where
})), })),
} }
} }
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
} }
Map { Map {
@ -515,6 +530,10 @@ where
} }
} }
} }
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
} }
Chain { Chain {
@ -566,6 +585,10 @@ pub fn scoped<T: 'static>(
outcome => outcome, outcome => outcome,
} }
} }
fn set_window_id(&mut self, id: window::Id) {
self.operation.set_window_id(id);
}
} }
ScopedOperation { ScopedOperation {

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,8 @@ 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);
}
} }

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,40 @@ 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 {
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

@ -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;
} }
@ -777,11 +784,11 @@ impl Default for Style {
/// ///
/// impl Catalog for MyTheme { /// impl Catalog for MyTheme {
/// type Class<'a> = ButtonClass; /// type Class<'a> = ButtonClass;
/// ///
/// fn default<'a>() -> Self::Class<'a> { /// fn default<'a>() -> Self::Class<'a> {
/// ButtonClass::default() /// ButtonClass::default()
/// } /// }
/// ///
/// ///
/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { /// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
/// let mut style = Style::default(); /// let mut style = Style::default();
@ -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

@ -676,14 +676,6 @@ where
let translation = let translation =
state.translation(self.direction, bounds, content_bounds); state.translation(self.direction, bounds, content_bounds);
operation.scrollable(
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(
&mut tree.children[0], &mut tree.children[0],
@ -696,6 +688,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(

View file

@ -2519,6 +2519,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());
} }
} }