diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 4280a00332..e83a1f7dad 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -3,7 +3,8 @@ use iced::keyboard; use iced::mouse; use iced::widget::{ button, canvas, center, checkbox, column, container, horizontal_rule, - horizontal_space, pick_list, row, scrollable, text, vertical_rule, + horizontal_space, pick_list, pin, row, scrollable, stack, text, + vertical_rule, }; use iced::{ color, Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, @@ -151,6 +152,10 @@ impl Example { title: "Quotes", view: quotes, }, + Self { + title: "Pinning", + view: pinning, + }, ]; fn is_first(self) -> bool { @@ -309,6 +314,23 @@ fn quotes<'a>() -> Element<'a, Message> { .into() } +fn pinning<'a>() -> Element<'a, Message> { + column![ + "The pin widget can be used to position a widget \ + at some fixed coordinates inside some other widget.", + stack![ + container(pin("• (50, 50)").x(50).y(50)) + .width(500) + .height(500) + .style(container::bordered_box), + pin("• (300, 300)").x(300).y(300), + ] + ] + .align_x(Center) + .spacing(10) + .into() +} + fn square<'a>(size: impl Into + Copy) -> Element<'a, Message> { struct Square; diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 33dff647bb..3d85ba7039 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -24,7 +24,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Stack, Themer}; +use crate::{Column, MouseArea, Pin, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -249,6 +249,38 @@ where container(content).center(Length::Fill) } +/// Creates a new [`Pin`] widget with the given content. +/// +/// A [`Pin`] widget positions its contents at some fixed coordinates inside of its boundaries. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::pin; +/// use iced::Fill; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pin("This text is displayed at coordinates (50, 50)!") +/// .x(50) +/// .y(50) +/// .into() +/// } +/// ``` +pub fn pin<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Pin::new(content) +} + /// Creates a new [`Column`] with the given children. /// /// Columns distribute their children vertically. diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 776a04a0fb..38c9929ade 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -11,6 +11,7 @@ pub use iced_runtime::core; mod action; mod column; mod mouse_area; +mod pin; mod row; mod space; mod stack; @@ -63,6 +64,8 @@ pub use pane_grid::PaneGrid; #[doc(no_inline)] pub use pick_list::PickList; #[doc(no_inline)] +pub use pin::Pin; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; diff --git a/widget/src/pin.rs b/widget/src/pin.rs new file mode 100644 index 0000000000..1f16771669 --- /dev/null +++ b/widget/src/pin.rs @@ -0,0 +1,270 @@ +//! A pin widget positions a widget at some fixed coordinates inside its boundaries. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::pin; +//! use iced::Fill; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! pin("This text is displayed at coordinates (50, 50)!") +//! .x(50) +//! .y(50) +//! .into() +//! } +//! ``` +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::{ + self, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, + Shell, Size, Vector, Widget, +}; + +/// A widget that positions its contents at some fixed coordinates inside of its boundaries. +/// +/// By default, a [`Pin`] widget will try to fill its parent. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::pin; +/// use iced::Fill; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pin("This text is displayed at coordinates (50, 50)!") +/// .x(50) +/// .y(50) +/// .into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +where + Renderer: core::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + width: Length, + height: Length, + position: Point, +} + +impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + /// Creates a [`Pin`] widget with the given content. + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + width: Length::Fill, + height: Length::Fill, + position: Point::ORIGIN, + } + } + + /// Sets the width of the [`Pin`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Pin`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the position of the [`Pin`]; where the pinned widget will be displayed. + pub fn position(mut self, position: impl Into) -> Self { + self.position = position.into(); + self + } + + /// Sets the X coordinate of the [`Pin`]. + pub fn x(mut self, x: impl Into) -> Self { + self.position.x = x.into().0; + self + } + + /// Sets the Y coordinate of the [`Pin`]. + pub fn y(mut self, y: impl Into) -> Self { + self.position.y = y.into().0; + self + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> widget::tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> widget::tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut widget::Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + let available = + limits.max() - Size::new(self.position.x, self.position.y); + + let node = self + .content + .as_widget() + .layout(tree, renderer, &layout::Limits::new(Size::ZERO, available)) + .move_to(self.position); + + let size = limits.resolve(self.width, self.height, node.size()); + layout::Node::with_children(size, vec![node]) + } + + fn operate( + &self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + tree, + layout.children().next().unwrap(), + renderer, + operation, + ); + } + + fn update( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + tree, + event, + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + fn mouse_interaction( + &self, + tree: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + tree, + layout.children().next().unwrap(), + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if let Some(clipped_viewport) = bounds.intersection(viewport) { + self.content.as_widget().draw( + tree, + renderer, + theme, + style, + layout.children().next().unwrap(), + cursor, + &clipped_viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + tree, + layout.children().next().unwrap(), + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from( + pin: Pin<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(pin) + } +}