Merge pull request #1971 from iced-rs/visible-bounds-operation

`visible_bounds` operation for `Container`
This commit is contained in:
Héctor Ramón 2023-07-27 18:48:06 +02:00 committed by GitHub
commit 18f1ab551b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 399 additions and 25 deletions

View file

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Helpers to change viewport alignment of a `Scrollable`. [#1953](https://github.com/iced-rs/iced/pull/1953)
- `scroll_to` widget operation. [#1796](https://github.com/iced-rs/iced/pull/1796)
- `scroll_to` helper. [#1804](https://github.com/iced-rs/iced/pull/1804)
- `visible_bounds` widget operation for `Container`. [#1971](https://github.com/iced-rs/iced/pull/1971)
- Command to fetch window size. [#1927](https://github.com/iced-rs/iced/pull/1927)
- Conversion support from `Fn` trait to custom theme. [#1861](https://github.com/iced-rs/iced/pull/1861)
- Configurable border radii on relevant widgets. [#1869](https://github.com/iced-rs/iced/pull/1869)

View file

@ -5,7 +5,9 @@ use crate::overlay;
use crate::renderer;
use crate::widget;
use crate::widget::tree::{self, Tree};
use crate::{Clipboard, Color, Layout, Length, Rectangle, Shell, Widget};
use crate::{
Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget,
};
use std::any::Any;
use std::borrow::Borrow;
@ -325,11 +327,12 @@ where
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>,
),
) {
self.operation.container(id, &mut |operation| {
self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation });
});
}
@ -346,8 +349,10 @@ where
&mut self,
state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id);
self.operation.scrollable(state, id, bounds, translation);
}
fn text_input(

View file

@ -172,11 +172,12 @@ where
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>,
),
) {
self.operation.container(id, &mut |operation| {
self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation });
});
}
@ -193,8 +194,10 @@ where
&mut self,
state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id);
self.operation.scrollable(state, id, bounds, translation);
}
fn text_input(

View file

@ -138,7 +138,7 @@ where
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child.operate(layout, renderer, operation);

View file

@ -197,3 +197,18 @@ where
}
}
}
impl<T> std::ops::Sub<Vector<T>> for Rectangle<T>
where
T: std::ops::Sub<Output = T>,
{
type Output = Rectangle<T>;
fn sub(self, translation: Vector<T>) -> Self {
Rectangle {
x: self.x - translation.x,
y: self.y - translation.y,
..self
}
}
}

View file

@ -8,6 +8,7 @@ pub use scrollable::Scrollable;
pub use text_input::TextInput;
use crate::widget::Id;
use crate::{Rectangle, Vector};
use std::any::Any;
use std::fmt;
@ -23,6 +24,7 @@ pub trait Operation<T> {
fn container(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
);
@ -30,7 +32,14 @@ pub trait Operation<T> {
fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
/// Operates on a widget that can be scrolled.
fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
fn scrollable(
&mut self,
_state: &mut dyn Scrollable,
_id: Option<&Id>,
_bounds: Rectangle,
_translation: Vector,
) {
}
/// Operates on a widget that has text input.
fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
@ -92,6 +101,7 @@ where
fn container(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
) {
struct MapRef<'a, A> {
@ -102,11 +112,12 @@ where
fn container(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
) {
let Self { operation, .. } = self;
operation.container(id, &mut |operation| {
operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapRef { operation });
});
}
@ -115,8 +126,10 @@ where
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id);
self.operation.scrollable(state, id, bounds, translation);
}
fn focusable(
@ -145,15 +158,21 @@ where
MapRef {
operation: operation.as_mut(),
}
.container(id, operate_on_children);
.container(id, bounds, operate_on_children);
}
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
self.operation.focusable(state, id);
}
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
self.operation.scrollable(state, id);
fn scrollable(
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
}
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@ -197,6 +216,7 @@ pub fn scope<T: 'static>(
fn container(
&mut self,
id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
) {
if id == Some(&self.target) {

View file

@ -1,6 +1,7 @@
//! Operate on widgets that can be focused.
use crate::widget::operation::{Operation, Outcome};
use crate::widget::Id;
use crate::Rectangle;
/// The internal state of a widget that can be focused.
pub trait Focusable {
@ -45,6 +46,7 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -80,6 +82,7 @@ where
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -126,6 +129,7 @@ pub fn focus_previous<T>() -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -159,6 +163,7 @@ pub fn focus_next<T>() -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -185,6 +190,7 @@ pub fn find_focused() -> impl Operation<Id> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
) {
operate_on_children(self)

View file

@ -1,5 +1,6 @@
//! Operate on widgets that can be scrolled.
use crate::widget::{Id, Operation};
use crate::{Rectangle, Vector};
/// The internal state of a widget that can be scrolled.
pub trait Scrollable {
@ -22,12 +23,19 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
}
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
fn scrollable(
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
state.snap_to(self.offset);
}
@ -49,12 +57,19 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
}
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
fn scrollable(
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
state.scroll_to(self.offset);
}

View file

@ -1,6 +1,7 @@
//! Operate on widgets that have text input.
use crate::widget::operation::Operation;
use crate::widget::Id;
use crate::Rectangle;
/// The internal state of a widget that has text input.
pub trait TextInput {
@ -34,6 +35,7 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -63,6 +65,7 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -93,6 +96,7 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)
@ -121,6 +125,7 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self)

View file

@ -381,7 +381,7 @@ mod toast {
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
&mut state.children[0],
layout,
@ -622,7 +622,7 @@ mod toast {
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.toasts
.iter()
.zip(self.state.iter_mut())

View file

@ -0,0 +1,10 @@
[package]
name = "visible_bounds"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
once_cell = "1"

View file

@ -0,0 +1,187 @@
use iced::executor;
use iced::mouse;
use iced::subscription::{self, Subscription};
use iced::theme::{self, Theme};
use iced::widget::{
column, container, horizontal_space, row, scrollable, text, vertical_space,
};
use iced::window;
use iced::{
Alignment, Application, Color, Command, Element, Event, Font, Length,
Point, Rectangle, Settings,
};
pub fn main() -> iced::Result {
Example::run(Settings::default())
}
struct Example {
mouse_position: Option<Point>,
outer_bounds: Option<Rectangle>,
inner_bounds: Option<Rectangle>,
}
#[derive(Debug, Clone, Copy)]
enum Message {
MouseMoved(Point),
WindowResized,
Scrolled(scrollable::Viewport),
OuterBoundsFetched(Option<Rectangle>),
InnerBoundsFetched(Option<Rectangle>),
}
impl Application for Example {
type Message = Message;
type Theme = Theme;
type Flags = ();
type Executor = executor::Default;
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
(
Self {
mouse_position: None,
outer_bounds: None,
inner_bounds: None,
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Visible bounds - Iced")
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::MouseMoved(position) => {
self.mouse_position = Some(position);
Command::none()
}
Message::Scrolled(_) | Message::WindowResized => {
Command::batch(vec![
container::visible_bounds(OUTER_CONTAINER.clone())
.map(Message::OuterBoundsFetched),
container::visible_bounds(INNER_CONTAINER.clone())
.map(Message::InnerBoundsFetched),
])
}
Message::OuterBoundsFetched(outer_bounds) => {
self.outer_bounds = outer_bounds;
Command::none()
}
Message::InnerBoundsFetched(inner_bounds) => {
self.inner_bounds = inner_bounds;
Command::none()
}
}
}
fn view(&self) -> Element<Message> {
let data_row = |label, value, color| {
row![
text(label),
horizontal_space(Length::Fill),
text(value).font(Font::MONOSPACE).size(14).style(color),
]
.height(40)
.align_items(Alignment::Center)
};
let view_bounds = |label, bounds: Option<Rectangle>| {
data_row(
label,
match bounds {
Some(bounds) => format!("{:?}", bounds),
None => "not visible".to_string(),
},
if bounds
.zip(self.mouse_position)
.map(|(bounds, mouse_position)| {
bounds.contains(mouse_position)
})
.unwrap_or_default()
{
Color {
g: 1.0,
..Color::BLACK
}
.into()
} else {
theme::Text::Default
},
)
};
column![
data_row(
"Mouse position",
match self.mouse_position {
Some(Point { x, y }) => format!("({x}, {y})"),
None => "unknown".to_string(),
},
theme::Text::Default,
),
view_bounds("Outer container", self.outer_bounds),
view_bounds("Inner container", self.inner_bounds),
scrollable(
column![
text("Scroll me!"),
vertical_space(400),
container(text("I am the outer container!"))
.id(OUTER_CONTAINER.clone())
.padding(40)
.style(theme::Container::Box),
vertical_space(400),
scrollable(
column![
text("Scroll me!"),
vertical_space(400),
container(text("I am the inner container!"))
.id(INNER_CONTAINER.clone())
.padding(40)
.style(theme::Container::Box),
vertical_space(400)
]
.padding(20)
)
.on_scroll(Message::Scrolled)
.width(Length::Fill)
.height(300),
]
.padding(20)
)
.on_scroll(Message::Scrolled)
.width(Length::Fill)
.height(300),
]
.spacing(10)
.padding(20)
.into()
}
fn subscription(&self) -> Subscription<Message> {
subscription::events_with(|event, _| match event {
Event::Mouse(mouse::Event::CursorMoved { position }) => {
Some(Message::MouseMoved(position))
}
Event::Window(window::Event::Resized { .. }) => {
Some(Message::WindowResized)
}
_ => None,
})
}
fn theme(&self) -> Theme {
Theme::Dark
}
}
use once_cell::sync::Lazy;
static OUTER_CONTAINER: Lazy<container::Id> =
Lazy::new(|| container::Id::new("outer"));
static INNER_CONTAINER: Lazy<container::Id> =
Lazy::new(|| container::Id::new("inner"));

View file

@ -181,7 +181,7 @@ where
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),

View file

@ -148,7 +148,7 @@ where
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)

View file

@ -8,8 +8,9 @@ use crate::core::renderer;
use crate::core::widget::{self, Operation, Tree};
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
Point, Rectangle, Shell, Widget,
Point, Rectangle, Shell, Size, Vector, Widget,
};
use crate::runtime::Command;
pub use iced_style::container::{Appearance, StyleSheet};
@ -180,6 +181,7 @@ where
) {
operation.container(
self.id.as_ref().map(|id| &id.0),
layout.bounds(),
&mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
@ -368,3 +370,92 @@ impl From<Id> for widget::Id {
id.0
}
}
/// Produces a [`Command`] that queries the visible screen bounds of the
/// [`Container`] with the given [`Id`].
pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
struct VisibleBounds {
target: widget::Id,
depth: usize,
scrollables: Vec<(Vector, Rectangle, usize)>,
bounds: Option<Rectangle>,
}
impl Operation<Option<Rectangle>> for VisibleBounds {
fn scrollable(
&mut self,
_state: &mut dyn widget::operation::Scrollable,
_id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) {
match self.scrollables.last() {
Some((last_translation, last_viewport, _depth)) => {
let viewport = last_viewport
.intersection(&(bounds - *last_translation))
.unwrap_or(Rectangle::new(Point::ORIGIN, Size::ZERO));
self.scrollables.push((
translation + *last_translation,
viewport,
self.depth,
));
}
None => {
self.scrollables.push((translation, bounds, self.depth));
}
}
}
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn Operation<Option<Rectangle>>,
),
) {
if self.bounds.is_some() {
return;
}
if id == Some(&self.target) {
match self.scrollables.last() {
Some((translation, viewport, _)) => {
self.bounds =
viewport.intersection(&(bounds - *translation));
}
None => {
self.bounds = Some(bounds);
}
}
return;
}
self.depth += 1;
operate_on_children(self);
self.depth -= 1;
match self.scrollables.last() {
Some((_, _, depth)) if self.depth == *depth => {
let _ = self.scrollables.pop();
}
_ => {}
}
}
fn finish(&self) -> widget::operation::Outcome<Option<Rectangle>> {
widget::operation::Outcome::Some(self.bounds)
}
}
Command::widget(VisibleBounds {
target: id.into(),
depth: 0,
scrollables: Vec::new(),
bounds: None,
})
}

View file

@ -7,7 +7,8 @@ use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
Widget,
};
use crate::runtime::overlay::Nested;
@ -340,11 +341,12 @@ where
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>,
),
) {
self.operation.container(id, &mut |operation| {
self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation });
});
}
@ -369,8 +371,10 @@ where
&mut self,
state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id);
self.operation.scrollable(state, id, bounds, translation);
}
fn custom(

View file

@ -297,7 +297,7 @@ where
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.contents
.iter()
.zip(&mut tree.children)

View file

@ -137,7 +137,7 @@ where
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)

View file

@ -254,10 +254,22 @@ where
) {
let state = tree.state.downcast_mut::<State>();
operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
let translation =
state.translation(self.direction, bounds, content_bounds);
operation.scrollable(
state,
self.id.as_ref().map(|id| &id.0),
bounds,
translation,
);
operation.container(
self.id.as_ref().map(|id| &id.0),
bounds,
&mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],