diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a989f6bb..c68ad2c67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/core/src/element.rs b/core/src/element.rs index b9b76247da..d2c6358b6c 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -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, ), ) { - 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( diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index c2134343db..29b404b83f 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -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, ), ) { - 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( diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index deffaad030..691686cdd5 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -138,7 +138,7 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - 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); diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 7ff324cb89..db56aa18cf 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -197,3 +197,18 @@ where } } } + +impl std::ops::Sub> for Rectangle +where + T: std::ops::Sub, +{ + type Output = Rectangle; + + fn sub(self, translation: Vector) -> Self { + Rectangle { + x: self.x - translation.x, + y: self.y - translation.y, + ..self + } + } +} diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index ad188c364d..b91cf9ac94 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -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 { fn container( &mut self, id: Option<&Id>, + bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ); @@ -30,7 +32,14 @@ pub trait Operation { 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), ) { 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), ) { 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( fn container( &mut self, id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { if id == Some(&self.target) { diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 312e48943d..ab1b677ea4 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -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(target: Id) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { 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), ) { operate_on_children(self) @@ -126,6 +129,7 @@ pub fn focus_previous() -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) @@ -159,6 +163,7 @@ pub fn focus_next() -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) @@ -185,6 +190,7 @@ pub fn find_focused() -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index f947344ded..4f8b2a9819 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -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(target: Id, offset: RelativeOffset) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { 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(target: Id, offset: AbsoluteOffset) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { 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); } diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index 4c773e9984..a9ea2e8137 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -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(target: Id) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) @@ -63,6 +65,7 @@ pub fn move_cursor_to_end(target: Id) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) @@ -93,6 +96,7 @@ pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) @@ -121,6 +125,7 @@ pub fn select_all(target: Id) -> impl Operation { fn container( &mut self, _id: Option<&Id>, + _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 5d29e89544..42f6c34840 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -381,7 +381,7 @@ mod toast { renderer: &Renderer, operation: &mut dyn Operation, ) { - 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, ) { - operation.container(None, &mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.toasts .iter() .zip(self.state.iter_mut()) diff --git a/examples/visible_bounds/Cargo.toml b/examples/visible_bounds/Cargo.toml new file mode 100644 index 0000000000..cfa56dd2e4 --- /dev/null +++ b/examples/visible_bounds/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "visible_bounds" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } +once_cell = "1" diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs new file mode 100644 index 0000000000..8b68451438 --- /dev/null +++ b/examples/visible_bounds/src/main.rs @@ -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, + outer_bounds: Option, + inner_bounds: Option, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + MouseMoved(Point), + WindowResized, + Scrolled(scrollable::Viewport), + OuterBoundsFetched(Option), + InnerBoundsFetched(Option), +} + +impl Application for Example { + type Message = Message; + type Theme = Theme; + type Flags = (); + type Executor = executor::Default; + + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + 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 { + 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 { + 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| { + 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 { + 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 = + Lazy::new(|| container::Id::new("outer")); +static INNER_CONTAINER: Lazy = + Lazy::new(|| container::Id::new("inner")); diff --git a/widget/src/button.rs b/widget/src/button.rs index 1312095f85..5727c63180 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -181,7 +181,7 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - 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(), diff --git a/widget/src/column.rs b/widget/src/column.rs index 9271d5efac..c16477f394 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -148,7 +148,7 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, &mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/container.rs b/widget/src/container.rs index 64cf5cd534..1f1df86171 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -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 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> { + struct VisibleBounds { + target: widget::Id, + depth: usize, + scrollables: Vec<(Vector, Rectangle, usize)>, + bounds: Option, + } + + impl Operation> 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>, + ), + ) { + 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> { + widget::operation::Outcome::Some(self.bounds) + } + } + + Command::widget(VisibleBounds { + target: id.into(), + depth: 0, + scrollables: Vec::new(), + bounds: None, + }) +} diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index bc0e23df56..19df279284 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -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, ), ) { - 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( diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 4f6dfbe83f..0f4ab9eb16 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -297,7 +297,7 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, &mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.contents .iter() .zip(&mut tree.children) diff --git a/widget/src/row.rs b/widget/src/row.rs index 7baaaae31c..99b2a0bf0a 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -137,7 +137,7 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, &mut |operation| { + operation.container(None, layout.bounds(), &mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index f621fb26c4..103e3944e2 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -254,10 +254,22 @@ where ) { let state = tree.state.downcast_mut::(); - 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],