diff options
Diffstat (limited to 'widget')
49 files changed, 15670 insertions, 0 deletions
diff --git a/widget/Cargo.toml b/widget/Cargo.toml new file mode 100644 index 00000000..14aae72e --- /dev/null +++ b/widget/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "iced_widget" +version = "0.1.0" +edition = "2021" + +[features] +lazy = ["ouroboros"] +image = ["iced_renderer/image"] +svg = ["iced_renderer/svg"] +canvas = ["iced_renderer/geometry"] +qr_code = ["canvas", "qrcode"] + +[dependencies] +unicode-segmentation = "1.6" +num-traits = "0.2" +thiserror = "1" + +[dependencies.iced_runtime] +version = "0.1" +path = "../runtime" + +[dependencies.iced_renderer] +version = "0.1" +path = "../renderer" + +[dependencies.iced_style] +version = "0.8" +path = "../style" + +[dependencies.ouroboros] +version = "0.17" +optional = true + +[dependencies.qrcode] +version = "0.12" +optional = true +default-features = false diff --git a/widget/src/button.rs b/widget/src/button.rs new file mode 100644 index 00000000..8ebc9657 --- /dev/null +++ b/widget/src/button.rs @@ -0,0 +1,459 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Operation; +use crate::core::{ + Background, Clipboard, Color, Element, Layout, Length, Padding, Point, + Rectangle, Shell, Vector, Widget, +}; + +pub use iced_style::button::{Appearance, StyleSheet}; + +/// A generic widget that produces a message when pressed. +/// +/// ```no_run +/// # type Button<'a, Message> = +/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); +/// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # type Button<'a, Message> = +/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn disabled_button<'a>() -> Button<'a, Message> { +/// Button::new("I'm disabled!") +/// } +/// +/// fn enabled_button<'a>() -> Button<'a, Message> { +/// disabled_button().on_press(Message::ButtonPressed) +/// } +/// ``` +#[allow(missing_debug_implementations)] +pub struct Button<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + content: Element<'a, Message, Renderer>, + on_press: Option<Message>, + width: Length, + height: Length, + padding: Padding, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Button<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Button`] with the given content. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + Button { + content: content.into(), + on_press: None, + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(5.0), + style: <Renderer::Theme as StyleSheet>::Style::default(), + } + } + + /// Sets the width of the [`Button`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Button`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// Unless `on_press` is called, the [`Button`] will be disabled. + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed, + /// if `Some`. + /// + /// If `None`, the [`Button`] will be disabled. + pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { + self.on_press = on_press; + self + } + + /// Sets the style variant of this [`Button`]. + pub fn style( + mut self, + style: <Renderer::Theme as StyleSheet>::Style, + ) -> Self { + self.style = style; + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Button<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + update(event, layout, cursor, shell, &self.on_press, || { + tree.state.downcast_mut::<State>() + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let styling = draw( + renderer, + bounds, + cursor, + self.on_press.is_some(), + theme, + &self.style, + || tree.state.downcast_ref::<State>(), + ); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: styling.text_color, + }, + content_layout, + cursor, + &bounds, + ); + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor, self.on_press.is_some()) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: crate::core::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(button: Button<'a, Message, Renderer>) -> Self { + Self::new(button) + } +} + +/// The local state of a [`Button`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_pressed: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] +/// accordingly. +pub fn update<'a, Message: Clone>( + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + on_press: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if on_press.is_some() { + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + let state = state(); + + state.is_pressed = true; + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = state(); + + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Button`]. +pub fn draw<'a, Renderer: crate::core::Renderer>( + renderer: &mut Renderer, + bounds: Rectangle, + cursor: mouse::Cursor, + is_enabled: bool, + style_sheet: &dyn StyleSheet< + Style = <Renderer::Theme as StyleSheet>::Style, + >, + style: &<Renderer::Theme as StyleSheet>::Style, + state: impl FnOnce() -> &'a State, +) -> Appearance +where + Renderer::Theme: StyleSheet, +{ + let is_mouse_over = cursor.is_over(bounds); + + let styling = if !is_enabled { + style_sheet.disabled(style) + } else if is_mouse_over { + let state = state(); + + if state.is_pressed { + style_sheet.pressed(style) + } else { + style_sheet.hovered(style) + } + } else { + style_sheet.active(style) + }; + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + border_radius: styling.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + styling +} + +/// Computes the layout of a [`Button`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + + let mut content = layout_content(renderer, &limits.pad(padding)); + let padding = padding.fit(content.size(), limits.max()); + let size = limits.pad(padding).resolve(content.size()).pad(padding); + + content.move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(size, vec![content]) +} + +/// Returns the [`mouse::Interaction`] of a [`Button`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor: mouse::Cursor, + is_enabled: bool, +) -> mouse::Interaction { + let is_mouse_over = cursor.is_over(layout.bounds()); + + if is_mouse_over && is_enabled { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs new file mode 100644 index 00000000..96062038 --- /dev/null +++ b/widget/src/canvas.rs @@ -0,0 +1,233 @@ +//! Draw 2D graphics for your users. +pub mod event; + +mod program; + +pub use event::Event; +pub use program::Program; + +pub use crate::graphics::geometry::*; +pub use crate::renderer::geometry::*; + +use crate::core; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{Clipboard, Element, Shell, Widget}; +use crate::core::{Length, Rectangle, Size, Vector}; +use crate::graphics::geometry; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # use iced_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program}; +/// # use iced_widget::core::{Color, Rectangle}; +/// # use iced_widget::core::mouse; +/// # use iced_widget::style::Theme; +/// # +/// # pub type Renderer = iced_widget::renderer::Renderer<Theme>; +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry>{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(renderer, bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas<P, Message, Renderer = crate::Renderer> +where + Renderer: geometry::Renderer, + P: Program<Message, Renderer>, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData<Message>, + theme_: PhantomData<Renderer>, +} + +impl<P, Message, Renderer> Canvas<P, Message, Renderer> +where + Renderer: geometry::Renderer, + P: Program<Message, Renderer>, +{ + const DEFAULT_SIZE: f32 = 100.0; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Fixed(Self::DEFAULT_SIZE), + height: Length::Fixed(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + theme_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } +} + +impl<P, Message, Renderer> Widget<Message, Renderer> + for Canvas<P, Message, Renderer> +where + Renderer: geometry::Renderer, + P: Program<Message, Renderer>, +{ + fn tag(&self) -> tree::Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<P::State>>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: core::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), + core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + core::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::<P::State>(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let state = tree.state.downcast_ref::<P::State>(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( + self.program.draw(state, renderer, theme, bounds, cursor), + ); + }, + ); + } +} + +impl<'a, P, Message, Renderer> From<Canvas<P, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + geometry::Renderer, + P: Program<Message, Renderer> + 'a, +{ + fn from( + canvas: Canvas<P, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs new file mode 100644 index 00000000..4508c184 --- /dev/null +++ b/widget/src/canvas/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a canvas. +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::touch; + +pub use crate::core::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs new file mode 100644 index 00000000..b3f6175e --- /dev/null +++ b/widget/src/canvas/program.rs @@ -0,0 +1,108 @@ +use crate::canvas::event::{self, Event}; +use crate::canvas::mouse; +use crate::core::Rectangle; +use crate::graphics::geometry; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program<Message, Renderer = crate::Renderer> +where + Renderer: geometry::Renderer, +{ + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, None) + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Vec<Renderer::Geometry>; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl<Message, Renderer, T> Program<Message, Renderer> for &T +where + Renderer: geometry::Renderer, + T: Program<Message, Renderer>, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> (event::Status, Option<Message>) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Vec<Renderer::Geometry> { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs new file mode 100644 index 00000000..aa0bff42 --- /dev/null +++ b/widget/src/checkbox.rs @@ -0,0 +1,355 @@ +//! Show toggle controls using checkboxes. +use crate::core::alignment; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text; +use crate::core::touch; +use crate::core::widget::Tree; +use crate::core::{ + Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, + Widget, +}; +use crate::{Row, Text}; + +pub use iced_style::checkbox::{Appearance, StyleSheet}; + +/// A box that can be checked. +/// +/// # Example +/// +/// ```no_run +/// # type Checkbox<'a, Message> = +/// # iced_widget::Checkbox<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// pub enum Message { +/// CheckboxToggled(bool), +/// } +/// +/// let is_checked = true; +/// +/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct Checkbox<'a, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + is_checked: bool, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, + label: String, + width: Length, + size: f32, + spacing: f32, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Option<Renderer::Font>, + icon: Icon<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + /// The default size of a [`Checkbox`]. + const DEFAULT_SIZE: f32 = 20.0; + + /// The default spacing of a [`Checkbox`]. + const DEFAULT_SPACING: f32 = 15.0; + + /// Creates a new [`Checkbox`]. + /// + /// It expects: + /// * a boolean describing whether the [`Checkbox`] is checked or not + /// * the label of the [`Checkbox`] + /// * a function that will be called when the [`Checkbox`] is toggled. It + /// will receive the new state of the [`Checkbox`] and must produce a + /// `Message`. + pub fn new<F>(label: impl Into<String>, is_checked: bool, f: F) -> Self + where + F: 'a + Fn(bool) -> Message, + { + Checkbox { + is_checked, + on_toggle: Box::new(f), + label: label.into(), + width: Length::Shrink, + size: Self::DEFAULT_SIZE, + spacing: Self::DEFAULT_SPACING, + text_size: None, + text_line_height: text::LineHeight::default(), + text_shaping: text::Shaping::Basic, + font: None, + icon: Icon { + font: Renderer::ICON_FONT, + code_point: Renderer::CHECKMARK_ICON, + size: None, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Basic, + }, + style: Default::default(), + } + } + + /// Sets the size of the [`Checkbox`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = size.into().0; + self + } + + /// Sets the width of the [`Checkbox`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the spacing between the [`Checkbox`] and the text. + pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { + self.spacing = spacing.into().0; + self + } + + /// Sets the text size of the [`Checkbox`]. + pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Checkbox`]. + pub fn text_line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Checkbox`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the [`Font`] of the text of the [`Checkbox`]. + /// + /// [`Font`]: crate::text::Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the [`Icon`] of the [`Checkbox`]. + pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self { + self.icon = icon; + self + } + + /// Sets the style of the [`Checkbox`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Checkbox<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + Row::<(), Renderer>::new() + .width(self.width) + .spacing(self.spacing) + .align_items(Alignment::Center) + .push(Row::new().width(self.size).height(self.size)) + .push( + Text::new(&self.label) + .font(self.font.unwrap_or_else(|| renderer.default_font())) + .width(self.width) + .size( + self.text_size + .unwrap_or_else(|| renderer.default_size()), + ) + .line_height(self.text_line_height) + .shaping(self.text_shaping), + ) + .layout(renderer, limits) + } + + fn on_event( + &mut self, + _tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let mouse_over = cursor.is_over(layout.bounds()); + + if mouse_over { + shell.publish((self.on_toggle)(!self.is_checked)); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let is_mouse_over = cursor.is_over(layout.bounds()); + + let mut children = layout.children(); + + let custom_style = if is_mouse_over { + theme.hovered(&self.style, self.is_checked) + } else { + theme.active(&self.style, self.is_checked) + }; + + { + let layout = children.next().unwrap(); + let bounds = layout.bounds(); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: custom_style.border_radius, + border_width: custom_style.border_width, + border_color: custom_style.border_color, + }, + custom_style.background, + ); + + let Icon { + font, + code_point, + size, + line_height, + shaping, + } = &self.icon; + let size = size.unwrap_or(bounds.height * 0.7); + + if self.is_checked { + renderer.fill_text(text::Text { + content: &code_point.to_string(), + font: *font, + size, + line_height: *line_height, + bounds: Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }, + color: custom_style.icon_color, + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: *shaping, + }); + } + } + + { + let label_layout = children.next().unwrap(); + + crate::text::draw( + renderer, + style, + label_layout, + &self.label, + self.text_size, + self.text_line_height, + self.font, + crate::text::Appearance { + color: custom_style.text_color, + }, + alignment::Horizontal::Left, + alignment::Vertical::Center, + self.text_shaping, + ); + } + } +} + +impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + fn from( + checkbox: Checkbox<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(checkbox) + } +} + +/// The icon in a [`Checkbox`]. +#[derive(Debug, Clone, PartialEq)] +pub struct Icon<Font> { + /// Font that will be used to display the `code_point`, + pub font: Font, + /// The unicode code point that will be used as the icon. + pub code_point: char, + /// Font size of the content. + pub size: Option<f32>, + /// The line height of the icon. + pub line_height: text::LineHeight, + /// The shaping strategy of the icon. + pub shaping: text::Shaping, +} diff --git a/widget/src/column.rs b/widget/src/column.rs new file mode 100644 index 00000000..d92d794b --- /dev/null +++ b/widget/src/column.rs @@ -0,0 +1,254 @@ +//! Distribute content vertically. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{Operation, Tree}; +use crate::core::{ + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Widget, +}; + +/// A container that distributes its contents vertically. +#[allow(missing_debug_implementations)] +pub struct Column<'a, Message, Renderer = crate::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align_items: Alignment, + children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Column<'a, Message, Renderer> { + /// Creates an empty [`Column`]. + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: Vec<Element<'a, Message, Renderer>>, + ) -> Self { + Column { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align_items: Alignment::Start, + children, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Column`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Column`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Column`]. + pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Column`] . + pub fn align_items(mut self, align: Alignment) -> Self { + self.align_items = align; + self + } + + /// Adds an element to the [`Column`]. + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.children.push(child.into()); + self + } +} + +impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Column<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .max_width(self.max_width) + .width(self.width) + .height(self.height); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.padding, + self.spacing, + self.align_items, + &self.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }) + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } +} + +impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(column: Column<'a, Message, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/widget/src/container.rs b/widget/src/container.rs new file mode 100644 index 00000000..da9a31d6 --- /dev/null +++ b/widget/src/container.rs @@ -0,0 +1,368 @@ +//! Decorate content and apply alignment. +use crate::core::alignment::{self, Alignment}; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +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, +}; + +pub use iced_style::container::{Appearance, StyleSheet}; + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct Container<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Option<Id>, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + max_height: f32, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + style: <Renderer::Theme as StyleSheet>::Style, + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Container<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates an empty [`Container`]. + pub fn new<T>(content: T) -> Self + where + T: Into<Element<'a, Message, Renderer>>, + { + Container { + id: None, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + max_height: f32::INFINITY, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + style: Default::default(), + content: content.into(), + } + } + + /// Sets the [`Id`] of the [`Container`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Container`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Container`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Container`]. + pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the maximum height of the [`Container`]. + pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self { + self.max_height = max_height.into().0; + self + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { + self.vertical_alignment = alignment; + self + } + + /// Centers the contents in the horizontal axis of the [`Container`]. + pub fn center_x(mut self) -> Self { + self.horizontal_alignment = alignment::Horizontal::Center; + self + } + + /// Centers the contents in the vertical axis of the [`Container`]. + pub fn center_y(mut self) -> Self { + self.vertical_alignment = alignment::Vertical::Center; + self + } + + /// Sets the style of the [`Container`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Container<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.max_width, + self.max_height, + self.padding, + self.horizontal_alignment, + self.vertical_alignment, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container( + self.id.as_ref().map(|id| &id.0), + &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let style = theme.appearance(&self.style); + + draw_background(renderer, &style, layout.bounds()); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: style + .text_color + .unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + column: Container<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} + +/// Computes the layout of a [`Container`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_width: f32, + max_height: f32, + padding: Padding, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_width(max_width) + .max_height(max_height) + .width(width) + .height(height); + + let mut content = layout_content(renderer, &limits.pad(padding).loose()); + let padding = padding.fit(content.size(), limits.max()); + let size = limits.pad(padding).resolve(content.size()); + + content.move_to(Point::new(padding.left, padding.top)); + content.align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + ); + + layout::Node::with_children(size.pad(padding), vec![content]) +} + +/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. +pub fn draw_background<Renderer>( + renderer: &mut Renderer, + appearance: &Appearance, + bounds: Rectangle, +) where + Renderer: crate::core::Renderer, +{ + if appearance.background.is_some() || appearance.border_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } +} + +/// The identifier of a [`Container`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +impl From<Id> for widget::Id { + fn from(id: Id) -> Self { + id.0 + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs new file mode 100644 index 00000000..3f5136f8 --- /dev/null +++ b/widget/src/helpers.rs @@ -0,0 +1,368 @@ +//! Helper functions to create pure widgets. +use crate::button::{self, Button}; +use crate::checkbox::{self, Checkbox}; +use crate::container::{self, Container}; +use crate::core; +use crate::core::widget::operation; +use crate::core::{Element, Length, Pixels}; +use crate::overlay; +use crate::pick_list::{self, PickList}; +use crate::progress_bar::{self, ProgressBar}; +use crate::radio::{self, Radio}; +use crate::rule::{self, Rule}; +use crate::runtime::Command; +use crate::scrollable::{self, Scrollable}; +use crate::slider::{self, Slider}; +use crate::text::{self, Text}; +use crate::text_input::{self, TextInput}; +use crate::toggler::{self, Toggler}; +use crate::tooltip::{self, Tooltip}; +use crate::{Column, MouseArea, Row, Space, VerticalSlider}; + +use std::borrow::Cow; +use std::ops::RangeInclusive; + +/// Creates a [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +#[macro_export] +macro_rules! column { + () => ( + $crate::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + ); +} + +/// Creates a [`Row`] with the given children. +/// +/// [`Row`]: widget::Row +#[macro_export] +macro_rules! row { + () => ( + $crate::Row::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Row::with_children(vec![$($crate::core::Element::from($x)),+]) + ); +} + +/// Creates a new [`Container`] with the provided content. +/// +/// [`Container`]: widget::Container +pub fn container<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> Container<'a, Message, Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + Container::new(content) +} + +/// Creates a new [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +pub fn column<Message, Renderer>( + children: Vec<Element<'_, Message, Renderer>>, +) -> Column<'_, Message, Renderer> { + Column::with_children(children) +} + +/// Creates a new [`Row`] with the given children. +/// +/// [`Row`]: widget::Row +pub fn row<Message, Renderer>( + children: Vec<Element<'_, Message, Renderer>>, +) -> Row<'_, Message, Renderer> { + Row::with_children(children) +} + +/// Creates a new [`Scrollable`] with the provided content. +/// +/// [`Scrollable`]: widget::Scrollable +pub fn scrollable<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> Scrollable<'a, Message, Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: scrollable::StyleSheet, +{ + Scrollable::new(content) +} + +/// Creates a new [`Button`] with the provided content. +/// +/// [`Button`]: widget::Button +pub fn button<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> Button<'a, Message, Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: button::StyleSheet, + <Renderer::Theme as button::StyleSheet>::Style: Default, +{ + Button::new(content) +} + +/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`]. +/// +/// [`Tooltip`]: widget::Tooltip +/// [`tooltip::Position`]: widget::tooltip::Position +pub fn tooltip<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl ToString, + position: tooltip::Position, +) -> crate::Tooltip<'a, Message, Renderer> +where + Renderer: core::text::Renderer, + Renderer::Theme: container::StyleSheet + text::StyleSheet, +{ + Tooltip::new(content, tooltip.to_string(), position) +} + +/// Creates a new [`Text`] widget with the provided content. +/// +/// [`Text`]: widget::Text +pub fn text<'a, Renderer>(text: impl ToString) -> Text<'a, Renderer> +where + Renderer: core::text::Renderer, + Renderer::Theme: text::StyleSheet, +{ + Text::new(text.to_string()) +} + +/// Creates a new [`Checkbox`]. +/// +/// [`Checkbox`]: widget::Checkbox +pub fn checkbox<'a, Message, Renderer>( + label: impl Into<String>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> Checkbox<'a, Message, Renderer> +where + Renderer: core::text::Renderer, + Renderer::Theme: checkbox::StyleSheet + text::StyleSheet, +{ + Checkbox::new(label, is_checked, f) +} + +/// Creates a new [`Radio`]. +/// +/// [`Radio`]: widget::Radio +pub fn radio<Message, Renderer, V>( + label: impl Into<String>, + value: V, + selected: Option<V>, + on_click: impl FnOnce(V) -> Message, +) -> Radio<Message, Renderer> +where + Message: Clone, + Renderer: core::text::Renderer, + Renderer::Theme: radio::StyleSheet, + V: Copy + Eq, +{ + Radio::new(label, value, selected, on_click) +} + +/// Creates a new [`Toggler`]. +/// +/// [`Toggler`]: widget::Toggler +pub fn toggler<'a, Message, Renderer>( + label: impl Into<Option<String>>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> Toggler<'a, Message, Renderer> +where + Renderer: core::text::Renderer, + Renderer::Theme: toggler::StyleSheet, +{ + Toggler::new(label, is_checked, f) +} + +/// Creates a new [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn text_input<'a, Message, Renderer>( + placeholder: &str, + value: &str, +) -> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: core::text::Renderer, + Renderer::Theme: text_input::StyleSheet, +{ + TextInput::new(placeholder, value) +} + +/// Creates a new [`Slider`]. +/// +/// [`Slider`]: widget::Slider +pub fn slider<'a, T, Message, Renderer>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: core::Renderer, + Renderer::Theme: slider::StyleSheet, +{ + Slider::new(range, value, on_change) +} + +/// Creates a new [`VerticalSlider`]. +/// +/// [`VerticalSlider`]: widget::VerticalSlider +pub fn vertical_slider<'a, T, Message, Renderer>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: core::Renderer, + Renderer::Theme: slider::StyleSheet, +{ + VerticalSlider::new(range, value, on_change) +} + +/// Creates a new [`PickList`]. +/// +/// [`PickList`]: widget::PickList +pub fn pick_list<'a, Message, Renderer, T>( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, +) -> PickList<'a, T, Message, Renderer> +where + T: ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: core::text::Renderer, + Renderer::Theme: pick_list::StyleSheet + + scrollable::StyleSheet + + overlay::menu::StyleSheet + + container::StyleSheet, + <Renderer::Theme as overlay::menu::StyleSheet>::Style: + From<<Renderer::Theme as pick_list::StyleSheet>::Style>, +{ + PickList::new(options, selected, on_selected) +} + +/// Creates a new horizontal [`Space`] with the given [`Length`]. +/// +/// [`Space`]: widget::Space +pub fn horizontal_space(width: impl Into<Length>) -> Space { + Space::with_width(width) +} + +/// Creates a new vertical [`Space`] with the given [`Length`]. +/// +/// [`Space`]: widget::Space +pub fn vertical_space(height: impl Into<Length>) -> Space { + Space::with_height(height) +} + +/// Creates a horizontal [`Rule`] with the given height. +/// +/// [`Rule`]: widget::Rule +pub fn horizontal_rule<Renderer>(height: impl Into<Pixels>) -> Rule<Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: rule::StyleSheet, +{ + Rule::horizontal(height) +} + +/// Creates a vertical [`Rule`] with the given width. +/// +/// [`Rule`]: widget::Rule +pub fn vertical_rule<Renderer>(width: impl Into<Pixels>) -> Rule<Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: rule::StyleSheet, +{ + Rule::vertical(width) +} + +/// Creates a new [`ProgressBar`]. +/// +/// It expects: +/// * an inclusive range of possible values, and +/// * the current value of the [`ProgressBar`]. +/// +/// [`ProgressBar`]: widget::ProgressBar +pub fn progress_bar<Renderer>( + range: RangeInclusive<f32>, + value: f32, +) -> ProgressBar<Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: progress_bar::StyleSheet, +{ + ProgressBar::new(range, value) +} + +/// Creates a new [`Image`]. +/// +/// [`Image`]: widget::Image +#[cfg(feature = "image")] +pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { + crate::Image::new(handle.into()) +} + +/// Creates a new [`Svg`] widget from the given [`Handle`]. +/// +/// [`Svg`]: widget::Svg +/// [`Handle`]: widget::svg::Handle +#[cfg(feature = "svg")] +pub fn svg<Renderer>( + handle: impl Into<core::svg::Handle>, +) -> crate::Svg<Renderer> +where + Renderer: core::svg::Renderer, + Renderer::Theme: crate::svg::StyleSheet, +{ + crate::Svg::new(handle) +} + +/// Creates a new [`Canvas`]. +#[cfg(feature = "canvas")] +pub fn canvas<P, Message, Renderer>( + program: P, +) -> crate::Canvas<P, Message, Renderer> +where + Renderer: crate::graphics::geometry::Renderer, + P: crate::canvas::Program<Message, Renderer>, +{ + crate::Canvas::new(program) +} + +/// Focuses the previous focusable widget. +pub fn focus_previous<Message>() -> Command<Message> +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_previous()) +} + +/// Focuses the next focusable widget. +pub fn focus_next<Message>() -> Command<Message> +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_next()) +} + +/// A container intercepting mouse events. +pub fn mouse_area<'a, Message, Renderer>( + widget: impl Into<Element<'a, Message, Renderer>>, +) -> MouseArea<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + MouseArea::new(widget) +} diff --git a/widget/src/image.rs b/widget/src/image.rs new file mode 100644 index 00000000..66bf2156 --- /dev/null +++ b/widget/src/image.rs @@ -0,0 +1,206 @@ +//! Display images in your user interface. +pub mod viewer; +pub use viewer::Viewer; + +use crate::core::image; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{ + ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, +}; + +use std::hash::Hash; + +pub use image::Handle; + +/// Creates a new [`Viewer`] with the given image `Handle`. +pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { + Viewer::new(handle) +} + +/// A frame that displays an image while keeping aspect ratio. +/// +/// # Example +/// +/// ```no_run +/// # use iced_widget::image::{self, Image}; +/// # +/// let image = Image::<image::Handle>::new("resources/ferris.png"); +/// ``` +/// +/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300"> +#[derive(Debug)] +pub struct Image<Handle> { + handle: Handle, + width: Length, + height: Length, + content_fit: ContentFit, +} + +impl<Handle> Image<Handle> { + /// Creates a new [`Image`] with the given path. + pub fn new<T: Into<Handle>>(handle: T) -> Self { + Image { + handle: handle.into(), + width: Length::Shrink, + height: Length::Shrink, + content_fit: ContentFit::Contain, + } + } + + /// Sets the width of the [`Image`] boundaries. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Image`] boundaries. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`ContentFit`] of the [`Image`]. + /// + /// Defaults to [`ContentFit::Contain`] + pub fn content_fit(self, content_fit: ContentFit) -> Self { + Self { + content_fit, + ..self + } + } +} + +/// Computes the layout of an [`Image`]. +pub fn layout<Renderer, Handle>( + renderer: &Renderer, + limits: &layout::Limits, + handle: &Handle, + width: Length, + height: Length, + content_fit: ContentFit, +) -> layout::Node +where + Renderer: image::Renderer<Handle = Handle>, +{ + // The raw w/h of the underlying image + let image_size = { + let Size { width, height } = renderer.dimensions(handle); + + Size::new(width as f32, height as f32) + }; + + // The size to be available to the widget prior to `Shrink`ing + let raw_size = limits.width(width).height(height).resolve(image_size); + + // The uncropped size of the image when fit to the bounds above + let full_size = content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested + let final_size = Size { + width: match width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) +} + +/// Draws an [`Image`] +pub fn draw<Renderer, Handle>( + renderer: &mut Renderer, + layout: Layout<'_>, + handle: &Handle, + content_fit: ContentFit, +) where + Renderer: image::Renderer<Handle = Handle>, + Handle: Clone + Hash, +{ + let Size { width, height } = renderer.dimensions(handle); + let image_size = Size::new(width as f32, height as f32); + + let bounds = layout.bounds(); + let adjusted_fit = content_fit.fit(image_size, bounds.size()); + + let render = |renderer: &mut Renderer| { + let offset = Vector::new( + (bounds.width - adjusted_fit.width).max(0.0) / 2.0, + (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + ); + + let drawing_bounds = Rectangle { + width: adjusted_fit.width, + height: adjusted_fit.height, + ..bounds + }; + + renderer.draw(handle.clone(), drawing_bounds + offset) + }; + + if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height + { + renderer.with_layer(bounds, render); + } else { + render(renderer) + } +} + +impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle> +where + Renderer: image::Renderer<Handle = Handle>, + Handle: Clone + Hash, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + &self.handle, + self.width, + self.height, + self.content_fit, + ) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + draw(renderer, layout, &self.handle, self.content_fit) + } +} + +impl<'a, Message, Renderer, Handle> From<Image<Handle>> + for Element<'a, Message, Renderer> +where + Renderer: image::Renderer<Handle = Handle>, + Handle: Clone + Hash + 'a, +{ + fn from(image: Image<Handle>) -> Element<'a, Message, Renderer> { + Element::new(image) + } +} diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs new file mode 100644 index 00000000..8040d6bd --- /dev/null +++ b/widget/src/image/viewer.rs @@ -0,0 +1,431 @@ +//! Zoom and pan on an image. +use crate::core::event::{self, Event}; +use crate::core::image; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, + Vector, Widget, +}; + +use std::hash::Hash; + +/// A frame that displays an image with the ability to zoom in/out and pan. +#[allow(missing_debug_implementations)] +pub struct Viewer<Handle> { + padding: f32, + width: Length, + height: Length, + min_scale: f32, + max_scale: f32, + scale_step: f32, + handle: Handle, +} + +impl<Handle> Viewer<Handle> { + /// Creates a new [`Viewer`] with the given [`State`]. + pub fn new(handle: Handle) -> Self { + Viewer { + padding: 0.0, + width: Length::Shrink, + height: Length::Shrink, + min_scale: 0.25, + max_scale: 10.0, + scale_step: 0.10, + handle, + } + } + + /// Sets the padding of the [`Viewer`]. + pub fn padding(mut self, padding: impl Into<Pixels>) -> Self { + self.padding = padding.into().0; + self + } + + /// Sets the width of the [`Viewer`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Viewer`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the max scale applied to the image of the [`Viewer`]. + /// + /// Default is `10.0` + pub fn max_scale(mut self, max_scale: f32) -> Self { + self.max_scale = max_scale; + self + } + + /// Sets the min scale applied to the image of the [`Viewer`]. + /// + /// Default is `0.25` + pub fn min_scale(mut self, min_scale: f32) -> Self { + self.min_scale = min_scale; + self + } + + /// Sets the percentage the image of the [`Viewer`] will be scaled by + /// when zoomed in / out. + /// + /// Default is `0.10` + pub fn scale_step(mut self, scale_step: f32) -> Self { + self.scale_step = scale_step; + self + } +} + +impl<Message, Renderer, Handle> Widget<Message, Renderer> for Viewer<Handle> +where + Renderer: image::Renderer<Handle = Handle>, + Handle: Clone + Hash, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let Size { width, height } = renderer.dimensions(&self.handle); + + let mut size = limits + .width(self.width) + .height(self.height) + .resolve(Size::new(width as f32, height as f32)); + + let expansion_size = if height > width { + self.width + } else { + self.height + }; + + // Only calculate viewport sizes if the images are constrained to a limited space. + // If they are Fill|Portion let them expand within their alotted space. + match expansion_size { + Length::Shrink | Length::Fixed(_) => { + let aspect_ratio = width as f32 / height as f32; + let viewport_aspect_ratio = size.width / size.height; + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } + } + Length::Fill | Length::FillPortion(_) => {} + } + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + let state = tree.state.downcast_mut::<State>(); + let previous_scale = state.scale; + + if y < 0.0 && previous_scale > self.min_scale + || y > 0.0 && previous_scale < self.max_scale + { + state.scale = (if y > 0.0 { + state.scale * (1.0 + self.scale_step) + } else { + state.scale / (1.0 + self.scale_step) + }) + .clamp(self.min_scale, self.max_scale); + + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); + + let factor = state.scale / previous_scale - 1.0; + + let cursor_to_center = + cursor_position - bounds.center(); + + let adjustment = cursor_to_center * factor + + state.current_offset * factor; + + state.current_offset = Vector::new( + if image_size.width > bounds.width { + state.current_offset.x + adjustment.x + } else { + 0.0 + }, + if image_size.height > bounds.height { + state.current_offset.y + adjustment.y + } else { + 0.0 + }, + ); + } + } + } + + event::Status::Captured + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + let state = tree.state.downcast_mut::<State>(); + + state.cursor_grabbed_at = Some(cursor_position); + state.starting_offset = state.current_offset; + + event::Status::Captured + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree.state.downcast_mut::<State>(); + + if state.cursor_grabbed_at.is_some() { + state.cursor_grabbed_at = None; + + event::Status::Captured + } else { + event::Status::Ignored + } + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + let state = tree.state.downcast_mut::<State>(); + + if let Some(origin) = state.cursor_grabbed_at { + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); + + let hidden_width = (image_size.width - bounds.width / 2.0) + .max(0.0) + .round(); + + let hidden_height = (image_size.height + - bounds.height / 2.0) + .max(0.0) + .round(); + + let delta = position - origin; + + let x = if bounds.width < image_size.width { + (state.starting_offset.x - delta.x) + .clamp(-hidden_width, hidden_width) + } else { + 0.0 + }; + + let y = if bounds.height < image_size.height { + (state.starting_offset.y - delta.y) + .clamp(-hidden_height, hidden_height) + } else { + 0.0 + }; + + state.current_offset = Vector::new(x, y); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_cursor_grabbed() { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::Idle + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + + let image_size = + image_size(renderer, &self.handle, state, bounds.size()); + + let translation = { + let image_top_left = Vector::new( + bounds.width / 2.0 - image_size.width / 2.0, + bounds.height / 2.0 - image_size.height / 2.0, + ); + + image_top_left - state.offset(bounds, image_size) + }; + + renderer.with_layer(bounds, |renderer| { + renderer.with_translation(translation, |renderer| { + image::Renderer::draw( + renderer, + self.handle.clone(), + Rectangle { + x: bounds.x, + y: bounds.y, + ..Rectangle::with_size(image_size) + }, + ) + }); + }); + } +} + +/// The local state of a [`Viewer`]. +#[derive(Debug, Clone, Copy)] +pub struct State { + scale: f32, + starting_offset: Vector, + current_offset: Vector, + cursor_grabbed_at: Option<Point>, +} + +impl Default for State { + fn default() -> Self { + Self { + scale: 1.0, + starting_offset: Vector::default(), + current_offset: Vector::default(), + cursor_grabbed_at: None, + } + } +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> Self { + State::default() + } + + /// Returns the current offset of the [`State`], given the bounds + /// of the [`Viewer`] and its image. + fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { + let hidden_width = + (image_size.width - bounds.width / 2.0).max(0.0).round(); + + let hidden_height = + (image_size.height - bounds.height / 2.0).max(0.0).round(); + + Vector::new( + self.current_offset.x.clamp(-hidden_width, hidden_width), + self.current_offset.y.clamp(-hidden_height, hidden_height), + ) + } + + /// Returns if the cursor is currently grabbed by the [`Viewer`]. + pub fn is_cursor_grabbed(&self) -> bool { + self.cursor_grabbed_at.is_some() + } +} + +impl<'a, Message, Renderer, Handle> From<Viewer<Handle>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + image::Renderer<Handle = Handle>, + Message: 'a, + Handle: Clone + Hash + 'a, +{ + fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Renderer> { + Element::new(viewer) + } +} + +/// Returns the bounds of the underlying image, given the bounds of +/// the [`Viewer`]. Scaling will be applied and original aspect ratio +/// will be respected. +pub fn image_size<Renderer>( + renderer: &Renderer, + handle: &<Renderer as image::Renderer>::Handle, + state: &State, + bounds: Size, +) -> Size +where + Renderer: image::Renderer, +{ + let Size { width, height } = renderer.dimensions(handle); + + let (width, height) = { + let dimensions = (width as f32, height as f32); + + let width_ratio = bounds.width / dimensions.0; + let height_ratio = bounds.height / dimensions.1; + + let ratio = width_ratio.min(height_ratio); + let scale = state.scale; + + if ratio < 1.0 { + (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) + } else { + (dimensions.0 * scale, dimensions.1 * scale) + } + }; + + Size::new(width, height) +} diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs new file mode 100644 index 00000000..da287f06 --- /dev/null +++ b/widget/src/lazy.rs @@ -0,0 +1,404 @@ +#![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)] +pub(crate) mod helpers; + +pub mod component; +pub mod responsive; + +pub use component::Component; +pub use responsive::Responsive; + +mod cache; + +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Widget}; +use crate::core::Element; +use crate::core::{ + self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, +}; +use crate::runtime::overlay::Nested; + +use ouroboros::self_referencing; +use std::cell::RefCell; +use std::hash::{Hash, Hasher as H}; +use std::rc::Rc; + +/// A widget that only rebuilds its contents when necessary. +#[allow(missing_debug_implementations)] +pub struct Lazy<'a, Message, Renderer, Dependency, View> { + dependency: Dependency, + view: Box<dyn Fn(&Dependency) -> View + 'a>, + element: RefCell< + Option<Rc<RefCell<Option<Element<'static, Message, Renderer>>>>>, + >, +} + +impl<'a, Message, Renderer, Dependency, View> + Lazy<'a, Message, Renderer, Dependency, View> +where + Dependency: Hash + 'a, + View: Into<Element<'static, Message, Renderer>>, +{ + /// Creates a new [`Lazy`] widget with the given data `Dependency` and a + /// closure that can turn this data into a widget tree. + pub fn new( + dependency: Dependency, + view: impl Fn(&Dependency) -> View + 'a, + ) -> Self { + Self { + dependency, + view: Box::new(view), + element: RefCell::new(None), + } + } + + fn with_element<T>( + &self, + f: impl FnOnce(&Element<'_, Message, Renderer>) -> T, + ) -> T { + f(self + .element + .borrow() + .as_ref() + .unwrap() + .borrow() + .as_ref() + .unwrap()) + } + + fn with_element_mut<T>( + &self, + f: impl FnOnce(&mut Element<'_, Message, Renderer>) -> T, + ) -> T { + f(self + .element + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .as_mut() + .unwrap()) + } +} + +struct Internal<Message, Renderer> { + element: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>, + hash: u64, +} + +impl<'a, Message, Renderer, Dependency, View> Widget<Message, Renderer> + for Lazy<'a, Message, Renderer, Dependency, View> +where + View: Into<Element<'static, Message, Renderer>> + 'static, + Dependency: Hash + 'a, + Message: 'static, + Renderer: core::Renderer + 'static, +{ + fn tag(&self) -> tree::Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<View>>() + } + + fn state(&self) -> tree::State { + let mut hasher = Hasher::default(); + self.dependency.hash(&mut hasher); + let hash = hasher.finish(); + + let element = + Rc::new(RefCell::new(Some((self.view)(&self.dependency).into()))); + + (*self.element.borrow_mut()) = Some(element.clone()); + + tree::State::new(Internal { element, hash }) + } + + fn children(&self) -> Vec<Tree> { + self.with_element(|element| vec![Tree::new(element.as_widget())]) + } + + fn diff(&self, tree: &mut Tree) { + let current = tree.state.downcast_mut::<Internal<Message, Renderer>>(); + + let mut hasher = Hasher::default(); + self.dependency.hash(&mut hasher); + let new_hash = hasher.finish(); + + if current.hash != new_hash { + current.hash = new_hash; + + let element = (self.view)(&self.dependency).into(); + current.element = Rc::new(RefCell::new(Some(element))); + + (*self.element.borrow_mut()) = Some(current.element.clone()); + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element.as_widget())) + }); + } else { + (*self.element.borrow_mut()) = Some(current.element.clone()); + } + } + + fn width(&self) -> Length { + self.with_element(|element| element.as_widget().width()) + } + + fn height(&self) -> Length { + self.with_element(|element| element.as_widget().height()) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.with_element(|element| { + element.as_widget().layout(renderer, limits) + }) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + self.with_element(|element| { + element.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_element_mut(|element| { + element.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + ) + }) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_element(|element| { + element.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.with_element(|element| { + element.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ) + }) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let overlay = Overlay(Some( + InnerBuilder { + cell: self.element.borrow().as_ref().unwrap().clone(), + element: self + .element + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .take() + .unwrap(), + tree: &mut tree.children[0], + overlay_builder: |element, tree| { + element + .as_widget_mut() + .overlay(tree, layout, renderer) + .map(|overlay| RefCell::new(Nested::new(overlay))) + }, + } + .build(), + )); + + let has_overlay = + overlay.with_overlay_maybe(|overlay| overlay.position()); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) + } +} + +#[self_referencing] +struct Inner<'a, Message: 'a, Renderer: 'a> { + cell: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>, + element: Element<'static, Message, Renderer>, + tree: &'a mut Tree, + + #[borrows(mut element, mut tree)] + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Message, Renderer>>>, +} + +struct Overlay<'a, Message, Renderer>(Option<Inner<'a, Message, Renderer>>); + +impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> { + fn drop(&mut self) { + let heads = self.0.take().unwrap().into_heads(); + (*heads.cell.borrow_mut()) = Some(heads.element); + } +} + +impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> { + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.0.as_ref().unwrap().with_overlay(|overlay| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.0.as_mut().unwrap().with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) + } +} + +impl<'a, Message, Renderer> overlay::Overlay<Message, Renderer> + for Overlay<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + overlay.layout(renderer, bounds, position) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction(layout, cursor, viewport, renderer) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event(event, layout, cursor, renderer, clipboard, shell) + }) + .unwrap_or(event::Status::Ignored) + } + + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, renderer, cursor_position) + }) + .unwrap_or_default() + } +} + +impl<'a, Message, Renderer, Dependency, View> + From<Lazy<'a, Message, Renderer, Dependency, View>> + for Element<'a, Message, Renderer> +where + View: Into<Element<'static, Message, Renderer>> + 'static, + Renderer: core::Renderer + 'static, + Message: 'static, + Dependency: Hash + 'a, +{ + fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self { + Self::new(lazy) + } +} diff --git a/widget/src/lazy/cache.rs b/widget/src/lazy/cache.rs new file mode 100644 index 00000000..e7b87614 --- /dev/null +++ b/widget/src/lazy/cache.rs @@ -0,0 +1,13 @@ +use crate::core::overlay; +use crate::core::Element; + +use ouroboros::self_referencing; + +#[self_referencing(pub_extras)] +pub struct Cache<'a, Message: 'a, Renderer: 'a> { + pub element: Element<'a, Message, Renderer>, + + #[borrows(mut element)] + #[covariant] + overlay: Option<overlay::Element<'this, Message, Renderer>>, +} diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs new file mode 100644 index 00000000..c7814966 --- /dev/null +++ b/widget/src/lazy/component.rs @@ -0,0 +1,681 @@ +//! Build and reuse custom widgets using The Elm Architecture. +use crate::core::event; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +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, +}; +use crate::runtime::overlay::Nested; + +use ouroboros::self_referencing; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; + +/// A reusable, custom widget that uses The Elm Architecture. +/// +/// A [`Component`] allows you to implement custom widgets as if they were +/// `iced` applications with encapsulated state. +/// +/// In other words, a [`Component`] allows you to turn `iced` applications into +/// custom widgets and embed them without cumbersome wiring. +/// +/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) +/// and update the internal state of the [`Component`]. +/// +/// Additionally, a [`Component`] is capable of producing a `Message` to notify +/// the parent application of any relevant interactions. +pub trait Component<Message, Renderer> { + /// The internal state of this [`Component`]. + type State: Default; + + /// The type of event this [`Component`] handles internally. + type Event; + + /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. + /// + /// It can produce a `Message` for the parent application. + fn update( + &mut self, + state: &mut Self::State, + event: Self::Event, + ) -> Option<Message>; + + /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) + /// on user interaction. + fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>; + + /// Update the [`Component`] state based on the provided [`Operation`](widget::Operation) + /// + /// By default, it does nothing. + fn operate( + &self, + _state: &mut Self::State, + _operation: &mut dyn widget::Operation<Message>, + ) { + } +} + +struct Tag<T>(T); + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn view<'a, C, Message, Renderer>( + component: C, +) -> Element<'a, Message, Renderer> +where + C: Component<Message, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Renderer: core::Renderer + 'a, +{ + Element::new(Instance { + state: RefCell::new(Some( + StateBuilder { + component: Box::new(component), + message: PhantomData, + state: PhantomData, + element_builder: |_| None, + } + .build(), + )), + tree: RefCell::new(Rc::new(RefCell::new(None))), + }) +} + +struct Instance<'a, Message, Renderer, Event, S> { + state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, + tree: RefCell<Rc<RefCell<Option<Tree>>>>, +} + +#[self_referencing] +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { + component: + Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>, + message: PhantomData<Message>, + state: PhantomData<S>, + + #[borrows(component)] + #[covariant] + element: Option<Element<'this, Event, Renderer>>, +} + +impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> +where + S: Default + 'static, + Renderer: renderer::Renderer, +{ + fn diff_self(&self) { + self.with_element(|element| { + self.tree + .borrow_mut() + .borrow_mut() + .as_mut() + .unwrap() + .diff_children(std::slice::from_ref(&element)); + }); + } + + fn rebuild_element_if_necessary(&self) { + let inner = self.state.borrow_mut().take().unwrap(); + if inner.borrow_element().is_none() { + let heads = inner.into_heads(); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| { + Some( + component.view( + self.tree + .borrow() + .borrow() + .as_ref() + .unwrap() + .state + .downcast_ref::<S>(), + ), + ) + }, + } + .build(), + ); + self.diff_self(); + } else { + *self.state.borrow_mut() = Some(inner); + } + } + + fn rebuild_element_with_operation( + &self, + operation: &mut dyn widget::Operation<Message>, + ) { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + heads.component.operate( + self.tree + .borrow_mut() + .borrow_mut() + .as_mut() + .unwrap() + .state + .downcast_mut(), + operation, + ); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| { + Some( + component.view( + self.tree + .borrow() + .borrow() + .as_ref() + .unwrap() + .state + .downcast_ref(), + ), + ) + }, + } + .build(), + ); + self.diff_self(); + } + + fn with_element<T>( + &self, + f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, + ) -> T { + self.with_element_mut(|element| f(element)) + } + + fn with_element_mut<T>( + &self, + f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, + ) -> T { + self.rebuild_element_if_necessary(); + self.state + .borrow_mut() + .as_mut() + .unwrap() + .with_element_mut(|element| f(element.as_mut().unwrap())) + } +} + +impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer> + for Instance<'a, Message, Renderer, Event, S> +where + S: 'static + Default, + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<Tag<S>>() + } + + fn state(&self) -> tree::State { + let state = Rc::new(RefCell::new(Some(Tree { + tag: tree::Tag::of::<Tag<S>>(), + state: tree::State::new(S::default()), + children: vec![Tree::empty()], + }))); + *self.tree.borrow_mut() = state.clone(); + tree::State::new(state) + } + + fn children(&self) -> Vec<Tree> { + vec![] + } + + fn diff(&self, tree: &mut Tree) { + let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); + *self.tree.borrow_mut() = tree.clone(); + self.rebuild_element_if_necessary(); + } + + fn width(&self) -> Length { + self.with_element(|element| element.as_widget().width()) + } + + fn height(&self) -> Length { + self.with_element(|element| element.as_widget().height()) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.with_element(|element| { + element.as_widget().layout(renderer, limits) + }) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: core::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); + let event_status = self.with_element_mut(|element| { + element.as_widget_mut().on_event( + &mut t.borrow_mut().as_mut().unwrap().children[0], + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + ) + }); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if let Some(redraw_request) = local_shell.redraw_request() { + shell.request_redraw(redraw_request); + } + + if !local_messages.is_empty() { + let mut heads = self.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads.component.update( + t.borrow_mut().as_mut().unwrap().state.downcast_mut(), + message, + ) + }) { + shell.publish(message); + } + + self.state = RefCell::new(Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |_| None, + } + .build(), + )); + + shell.invalidate_layout(); + } + + event_status + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + self.rebuild_element_with_operation(operation); + + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation<B>, + } + + impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<T>, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn custom( + &mut self, + state: &mut dyn std::any::Any, + id: Option<&widget::Id>, + ) { + self.operation.custom(state, id); + } + } + + let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); + self.with_element(|element| { + element.as_widget().operate( + &mut tree.borrow_mut().as_mut().unwrap().children[0], + layout, + renderer, + &mut MapOperation { operation }, + ); + }); + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); + self.with_element(|element| { + element.as_widget().draw( + &tree.borrow().as_ref().unwrap().children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + }); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); + self.with_element(|element| { + element.as_widget().mouse_interaction( + &tree.borrow().as_ref().unwrap().children[0], + layout, + cursor, + viewport, + renderer, + ) + }) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.rebuild_element_if_necessary(); + let tree = tree + .state + .downcast_mut::<Rc<RefCell<Option<Tree>>>>() + .borrow_mut() + .take() + .unwrap(); + let overlay = Overlay(Some( + InnerBuilder { + instance: self, + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance.state.get_mut().as_mut().unwrap().with_element_mut( + move |element| { + element + .as_mut() + .unwrap() + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout, + renderer, + ) + .map(|overlay| { + RefCell::new(Nested::new(overlay)) + }) + }, + ) + }, + } + .build(), + )); + + let has_overlay = overlay.0.as_ref().unwrap().with_overlay(|overlay| { + overlay.as_ref().map(|nested| nested.borrow().position()) + }); + + has_overlay.map(|position| { + overlay::Element::new( + position, + Box::new(OverlayInstance { + overlay: Some(overlay), + }), + ) + }) + } +} + +struct Overlay<'a, 'b, Message, Renderer, Event, S>( + Option<Inner<'a, 'b, Message, Renderer, Event, S>>, +); + +impl<'a, 'b, Message, Renderer, Event, S> Drop + for Overlay<'a, 'b, Message, Renderer, Event, S> +{ + fn drop(&mut self) { + if let Some(heads) = self.0.take().map(|inner| inner.into_heads()) { + *heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree); + } + } +} + +#[self_referencing] +struct Inner<'a, 'b, Message, Renderer, Event, S> { + instance: &'a mut Instance<'b, Message, Renderer, Event, S>, + tree: Tree, + types: PhantomData<(Message, Event, S)>, + + #[borrows(mut instance, mut tree)] + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Event, Renderer>>>, +} + +struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { + overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>, +} + +impl<'a, 'b, Message, Renderer, Event, S> + OverlayInstance<'a, 'b, Message, Renderer, Event, S> +{ + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_ref() + .unwrap() + .0 + .as_ref() + .unwrap() + .with_overlay(|overlay| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_mut() + .unwrap() + .0 + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) + } +} + +impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer> + for OverlayInstance<'a, 'b, Message, Renderer, Event, S> +where + Renderer: core::Renderer, + S: 'static + Default, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + overlay.layout(renderer, bounds, position) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction(layout, cursor, viewport, renderer) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: core::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self + .with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + ) + }) + .unwrap_or(event::Status::Ignored); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if let Some(redraw_request) = local_shell.redraw_request() { + shell.request_redraw(redraw_request); + } + + if !local_messages.is_empty() { + let mut inner = + self.overlay.take().unwrap().0.take().unwrap().into_heads(); + let mut heads = inner.instance.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(inner.tree.state.downcast_mut(), message) + }) { + shell.publish(message); + } + + *inner.instance.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |_| None, + } + .build(), + ); + + self.overlay = Some(Overlay(Some( + InnerBuilder { + instance: inner.instance, + tree: inner.tree, + types: PhantomData, + overlay_builder: |_, _| None, + } + .build(), + ))); + + shell.invalidate_layout(); + } + + event_status + } + + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, renderer, cursor_position) + }) + .unwrap_or_default() + } +} diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs new file mode 100644 index 00000000..8ca9cb86 --- /dev/null +++ b/widget/src/lazy/helpers.rs @@ -0,0 +1,47 @@ +use crate::core::{self, Element, Size}; +use crate::lazy::component::{self, Component}; +use crate::lazy::{Lazy, Responsive}; + +use std::hash::Hash; + +/// Creates a new [`Lazy`] widget with the given data `Dependency` and a +/// closure that can turn this data into a widget tree. +pub fn lazy<'a, Message, Renderer, Dependency, View>( + dependency: Dependency, + view: impl Fn(&Dependency) -> View + 'a, +) -> Lazy<'a, Message, Renderer, Dependency, View> +where + Dependency: Hash + 'a, + View: Into<Element<'static, Message, Renderer>>, +{ + Lazy::new(dependency, view) +} + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn component<'a, C, Message, Renderer>( + component: C, +) -> Element<'a, Message, Renderer> +where + C: Component<Message, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Renderer: core::Renderer + 'a, +{ + component::view(component) +} + +/// Creates a new [`Responsive`] widget with a closure that produces its +/// contents. +/// +/// The `view` closure will be provided with the current [`Size`] of +/// the [`Responsive`] widget and, therefore, can be used to build the +/// contents of the widget in a responsive way. +pub fn responsive<'a, Message, Renderer>( + f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, +) -> Responsive<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + Responsive::new(f) +} diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs new file mode 100644 index 00000000..07300857 --- /dev/null +++ b/widget/src/lazy/responsive.rs @@ -0,0 +1,426 @@ +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +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, +}; +use crate::horizontal_space; +use crate::runtime::overlay::Nested; + +use ouroboros::self_referencing; +use std::cell::{RefCell, RefMut}; +use std::marker::PhantomData; +use std::ops::Deref; + +/// A widget that is aware of its dimensions. +/// +/// A [`Responsive`] widget will always try to fill all the available space of +/// its parent. +#[allow(missing_debug_implementations)] +pub struct Responsive<'a, Message, Renderer = crate::Renderer> { + view: Box<dyn Fn(Size) -> Element<'a, Message, Renderer> + 'a>, + content: RefCell<Content<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + /// Creates a new [`Responsive`] widget with a closure that produces its + /// contents. + /// + /// The `view` closure will be provided with the current [`Size`] of + /// the [`Responsive`] widget and, therefore, can be used to build the + /// contents of the widget in a responsive way. + pub fn new( + view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, + ) -> Self { + Self { + view: Box::new(view), + content: RefCell::new(Content { + size: Size::ZERO, + layout: None, + element: Element::new(horizontal_space(0)), + }), + } + } +} + +struct Content<'a, Message, Renderer> { + size: Size, + layout: Option<layout::Node>, + element: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + fn layout(&mut self, renderer: &Renderer) { + if self.layout.is_none() { + self.layout = + Some(self.element.as_widget().layout( + renderer, + &layout::Limits::new(Size::ZERO, self.size), + )); + } + } + + fn update( + &mut self, + tree: &mut Tree, + new_size: Size, + view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, + ) { + if self.size == new_size { + return; + } + + self.element = view(new_size); + self.size = new_size; + self.layout = None; + + tree.diff(&self.element); + } + + fn resolve<R, T>( + &mut self, + tree: &mut Tree, + renderer: R, + layout: Layout<'_>, + view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, + f: impl FnOnce( + &mut Tree, + R, + Layout<'_>, + &mut Element<'a, Message, Renderer>, + ) -> T, + ) -> T + where + R: Deref<Target = Renderer>, + { + self.update(tree, layout.bounds().size(), view); + self.layout(renderer.deref()); + + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + self.layout.as_ref().unwrap(), + ); + + f(tree, renderer, content_layout, &mut self.element) + } +} + +struct State { + tree: RefCell<Tree>, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Responsive<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + tree: RefCell::new(Tree::empty()), + }) + } + + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Fill + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(limits.max()) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element + .as_widget() + .operate(tree, layout, renderer, operation); + }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::<State>(); + let mut content = self.content.borrow_mut(); + + let mut local_messages = vec![]; + let mut local_shell = Shell::new(&mut local_messages); + + let status = content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget_mut().on_event( + tree, + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + ) + }, + ); + + if local_shell.is_layout_invalid() { + content.layout = None; + } + + shell.merge(local_shell, std::convert::identity); + + status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget().draw( + tree, renderer, theme, style, layout, cursor, viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element + .as_widget() + .mouse_interaction(tree, layout, cursor, viewport, renderer) + }, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + use std::ops::DerefMut; + + let state = tree.state.downcast_ref::<State>(); + + let overlay = OverlayBuilder { + content: self.content.borrow_mut(), + tree: state.tree.borrow_mut(), + types: PhantomData, + overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>, + tree| { + content.update(tree, layout.bounds().size(), &self.view); + content.layout(renderer); + + let Content { + element, + layout: content_layout_node, + .. + } = content.deref_mut(); + + let content_layout = Layout::with_offset( + layout.bounds().position() - Point::ORIGIN, + content_layout_node.as_ref().unwrap(), + ); + + element + .as_widget_mut() + .overlay(tree, content_layout, renderer) + .map(|overlay| RefCell::new(Nested::new(overlay))) + }, + } + .build(); + + let has_overlay = + overlay.with_overlay_maybe(|overlay| overlay.position()); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) + } +} + +impl<'a, Message, Renderer> From<Responsive<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: core::Renderer + 'a, + Message: 'a, +{ + fn from(responsive: Responsive<'a, Message, Renderer>) -> Self { + Self::new(responsive) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer> { + content: RefMut<'a, Content<'b, Message, Renderer>>, + tree: RefMut<'a, Tree>, + types: PhantomData<Message>, + + #[borrows(mut content, mut tree)] + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Message, Renderer>>>, +} + +impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.with_overlay(|overlay| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) + } +} + +impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer> + for Overlay<'a, 'b, Message, Renderer> +where + Renderer: core::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + overlay.layout(renderer, bounds, position) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction(layout, cursor, viewport, renderer) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event(event, layout, cursor, renderer, clipboard, shell) + }) + .unwrap_or(event::Status::Ignored) + } + + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, renderer, cursor_position) + }) + .unwrap_or_default() + } +} diff --git a/widget/src/lib.rs b/widget/src/lib.rs new file mode 100644 index 00000000..9da13f9b --- /dev/null +++ b/widget/src/lib.rs @@ -0,0 +1,126 @@ +//! Use the built-in widgets or create your own. +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code, rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +pub use iced_renderer as renderer; +pub use iced_renderer::graphics; +pub use iced_runtime as runtime; +pub use iced_runtime::core; +pub use iced_style as style; + +mod column; +mod mouse_area; +mod row; + +pub mod button; +pub mod checkbox; +pub mod container; +pub mod overlay; +pub mod pane_grid; +pub mod pick_list; +pub mod progress_bar; +pub mod radio; +pub mod rule; +pub mod scrollable; +pub mod slider; +pub mod space; +pub mod text; +pub mod text_input; +pub mod toggler; +pub mod tooltip; +pub mod vertical_slider; + +mod helpers; + +pub use helpers::*; + +#[cfg(feature = "lazy")] +mod lazy; + +#[cfg(feature = "lazy")] +pub use crate::lazy::{Component, Lazy, Responsive}; + +#[cfg(feature = "lazy")] +pub use crate::lazy::helpers::*; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use column::Column; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use mouse_area::MouseArea; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use row::Row; +#[doc(no_inline)] +pub use rule::Rule; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use space::Space; +#[doc(no_inline)] +pub use text::Text; +#[doc(no_inline)] +pub use text_input::TextInput; +#[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] +pub use tooltip::Tooltip; +#[doc(no_inline)] +pub use vertical_slider::VerticalSlider; + +#[cfg(feature = "svg")] +pub mod svg; + +#[cfg(feature = "svg")] +#[doc(no_inline)] +pub use svg::Svg; + +#[cfg(feature = "image")] +pub mod image; + +#[cfg(feature = "image")] +#[doc(no_inline)] +pub use image::Image; + +#[cfg(feature = "canvas")] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + +type Renderer<Theme = style::Theme> = renderer::Renderer<Theme>; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs new file mode 100644 index 00000000..da7dc88f --- /dev/null +++ b/widget/src/mouse_area.rs @@ -0,0 +1,311 @@ +//! A container for capturing mouse events. + +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget::{tree, Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Widget, +}; + +/// Emit messages on mouse events. +#[allow(missing_debug_implementations)] +pub struct MouseArea<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, + on_press: Option<Message>, + on_release: Option<Message>, + on_right_press: Option<Message>, + on_right_release: Option<Message>, + on_middle_press: Option<Message>, + on_middle_release: Option<Message>, +} + +impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { + /// The message to emit on a left button press. + #[must_use] + pub fn on_press(mut self, message: Message) -> Self { + self.on_press = Some(message); + self + } + + /// The message to emit on a left button release. + #[must_use] + pub fn on_release(mut self, message: Message) -> Self { + self.on_release = Some(message); + self + } + + /// The message to emit on a right button press. + #[must_use] + pub fn on_right_press(mut self, message: Message) -> Self { + self.on_right_press = Some(message); + self + } + + /// The message to emit on a right button release. + #[must_use] + pub fn on_right_release(mut self, message: Message) -> Self { + self.on_right_release = Some(message); + self + } + + /// The message to emit on a middle button press. + #[must_use] + pub fn on_middle_press(mut self, message: Message) -> Self { + self.on_middle_press = Some(message); + self + } + + /// The message to emit on a middle button release. + #[must_use] + pub fn on_middle_release(mut self, message: Message) -> Self { + self.on_middle_release = Some(message); + self + } +} + +/// Local state of the [`MouseArea`]. +#[derive(Default)] +struct State { + // TODO: Support on_mouse_enter and on_mouse_exit +} + +impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { + /// Creates a [`MouseArea`] with the given content. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + MouseArea { + content: content.into(), + on_press: None, + on_release: None, + on_right_press: None, + on_right_release: None, + on_middle_press: None, + on_middle_release: None, + } + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for MouseArea<'a, Message, Renderer> +where + Renderer: renderer::Renderer, + Message: Clone, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(renderer, limits) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + update(self, &event, layout, cursor, shell) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<MouseArea<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + renderer::Renderer, +{ + fn from( + area: MouseArea<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(area) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`] +/// accordingly. +fn update<Message: Clone, Renderer>( + widget: &mut MouseArea<'_, Message, Renderer>, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, +) -> event::Status { + if !cursor.is_over(layout.bounds()) { + return event::Status::Ignored; + } + + if let Some(message) = widget.on_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_release.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_right_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = + event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_right_release.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Right, + )) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_middle_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Middle, + )) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_middle_release.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Middle, + )) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + event::Status::Ignored +} diff --git a/widget/src/overlay.rs b/widget/src/overlay.rs new file mode 100644 index 00000000..bc0ed744 --- /dev/null +++ b/widget/src/overlay.rs @@ -0,0 +1,2 @@ +//! Display interactive elements on top of other widgets. +pub mod menu; diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs new file mode 100644 index 00000000..ccf4dfb5 --- /dev/null +++ b/widget/src/overlay/menu.rs @@ -0,0 +1,541 @@ +//! Build and show dropdown menus. +use crate::container::{self, Container}; +use crate::core::alignment; +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::text::{self, Text}; +use crate::core::touch; +use crate::core::widget::Tree; +use crate::core::{ + Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, Size, Vector, +}; +use crate::core::{Element, Shell, Widget}; +use crate::scrollable::{self, Scrollable}; + +pub use iced_style::menu::{Appearance, StyleSheet}; + +/// A list of selectable options. +#[allow(missing_debug_implementations)] +pub struct Menu<'a, T, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + on_selected: Box<dyn FnMut(T) -> Message + 'a>, + width: f32, + padding: Padding, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Option<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Message, Renderer> Menu<'a, T, Message, Renderer> +where + T: ToString + Clone, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: + StyleSheet + container::StyleSheet + scrollable::StyleSheet, +{ + /// Creates a new [`Menu`] with the given [`State`], a list of options, and + /// the message to produced when an option is selected. + pub fn new( + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + on_selected: impl FnMut(T) -> Message + 'a, + ) -> Self { + Menu { + state, + options, + hovered_option, + on_selected: Box::new(on_selected), + width: 0.0, + padding: Padding::ZERO, + text_size: None, + text_line_height: text::LineHeight::default(), + text_shaping: text::Shaping::Basic, + font: None, + style: Default::default(), + } + } + + /// Sets the width of the [`Menu`]. + pub fn width(mut self, width: f32) -> Self { + self.width = width; + self + } + + /// Sets the [`Padding`] of the [`Menu`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`Menu`]. + pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Menu`]. + pub fn text_line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Menu`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the font of the [`Menu`]. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the style of the [`Menu`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Turns the [`Menu`] into an overlay [`Element`] at the given target + /// position. + /// + /// The `target_height` will be used to display the menu either on top + /// of the target or under it, depending on the screen position and the + /// dimensions of the [`Menu`]. + pub fn overlay( + self, + position: Point, + target_height: f32, + ) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new( + position, + Box::new(Overlay::new(self, target_height)), + ) + } +} + +/// The local state of a [`Menu`]. +#[derive(Debug)] +pub struct State { + tree: Tree, +} + +impl State { + /// Creates a new [`State`] for a [`Menu`]. + pub fn new() -> Self { + Self { + tree: Tree::empty(), + } + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +struct Overlay<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + state: &'a mut Tree, + container: Container<'a, Message, Renderer>, + width: f32, + target_height: f32, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a, + Renderer: text::Renderer, + Renderer::Theme: + StyleSheet + container::StyleSheet + scrollable::StyleSheet, +{ + pub fn new<T>( + menu: Menu<'a, T, Message, Renderer>, + target_height: f32, + ) -> Self + where + T: Clone + ToString, + { + let Menu { + state, + options, + hovered_option, + on_selected, + width, + padding, + font, + text_size, + text_line_height, + text_shaping, + style, + } = menu; + + let container = Container::new(Scrollable::new(List { + options, + hovered_option, + on_selected, + font, + text_size, + text_line_height, + text_shaping, + padding, + style: style.clone(), + })); + + state.tree.diff(&container as &dyn Widget<_, _>); + + Self { + state: &mut state.tree, + container, + width, + target_height, + style, + } + } +} + +impl<'a, Message, Renderer> crate::core::Overlay<Message, Renderer> + for Overlay<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let space_below = bounds.height - (position.y + self.target_height); + let space_above = position.y; + + let limits = layout::Limits::new( + Size::ZERO, + Size::new( + bounds.width - position.x, + if space_below > space_above { + space_below + } else { + space_above + }, + ), + ) + .width(self.width); + + let mut node = self.container.layout(renderer, &limits); + + node.move_to(if space_below > space_above { + position + Vector::new(0.0, self.target_height) + } else { + position - Vector::new(0.0, node.size().height) + }); + + node + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.container.on_event( + self.state, event, layout, cursor, renderer, clipboard, shell, + ) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.container + .mouse_interaction(self.state, layout, cursor, viewport, renderer) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let appearance = theme.appearance(&self.style); + let bounds = layout.bounds(); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: appearance.border_color, + border_width: appearance.border_width, + border_radius: appearance.border_radius, + }, + appearance.background, + ); + + self.container + .draw(self.state, renderer, theme, style, layout, cursor, &bounds); + } +} + +struct List<'a, T, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + options: &'a [T], + hovered_option: &'a mut Option<usize>, + on_selected: Box<dyn FnMut(T) -> Message + 'a>, + padding: Padding, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Option<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for List<'a, T, Message, Renderer> +where + T: Clone + ToString, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits.width(Length::Fill).height(Length::Shrink); + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); + + let text_line_height = + self.text_line_height.to_absolute(Pixels(text_size)); + + let size = { + let intrinsic = Size::new( + 0.0, + (f32::from(text_line_height) + self.padding.vertical()) + * self.options.len() as f32, + ); + + limits.resolve(intrinsic) + }; + + layout::Node::new(size) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if cursor.is_over(layout.bounds()) { + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + shell.publish((self.on_selected)(option.clone())); + return event::Status::Captured; + } + } + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some(cursor_position) = + cursor.position_in(layout.bounds()) + { + let text_size = self + .text_size + .unwrap_or_else(|| renderer.default_size()); + + let option_height = f32::from( + self.text_line_height.to_absolute(Pixels(text_size)), + ) + self.padding.vertical(); + + *self.hovered_option = + Some((cursor_position.y / option_height) as usize); + } + } + Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = + cursor.position_in(layout.bounds()) + { + let text_size = self + .text_size + .unwrap_or_else(|| renderer.default_size()); + + let option_height = f32::from( + self.text_line_height.to_absolute(Pixels(text_size)), + ) + self.padding.vertical(); + + *self.hovered_option = + Some((cursor_position.y / option_height) as usize); + + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + shell.publish((self.on_selected)(option.clone())); + return event::Status::Captured; + } + } + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let is_mouse_over = cursor.is_over(layout.bounds()); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let appearance = theme.appearance(&self.style); + let bounds = layout.bounds(); + + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); + let option_height = + f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + + self.padding.vertical(); + + let offset = viewport.y - bounds.y; + let start = (offset / option_height) as usize; + let end = ((offset + viewport.height) / option_height).ceil() as usize; + + let visible_options = &self.options[start..end.min(self.options.len())]; + + for (i, option) in visible_options.iter().enumerate() { + let i = start + i; + let is_selected = *self.hovered_option == Some(i); + + let bounds = Rectangle { + x: bounds.x, + y: bounds.y + (option_height * i as f32), + width: bounds.width, + height: option_height, + }; + + if is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + appearance.border_width, + width: bounds.width - appearance.border_width * 2.0, + ..bounds + }, + border_color: Color::TRANSPARENT, + border_width: 0.0, + border_radius: appearance.border_radius, + }, + appearance.selected_background, + ); + } + + renderer.fill_text(Text { + content: &option.to_string(), + bounds: Rectangle { + x: bounds.x + self.padding.left, + y: bounds.center_y(), + width: f32::INFINITY, + ..bounds + }, + size: text_size, + line_height: self.text_line_height, + font: self.font.unwrap_or_else(|| renderer.default_font()), + color: if is_selected { + appearance.selected_text_color + } else { + appearance.text_color + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: self.text_shaping, + }); + } + } +} + +impl<'a, T, Message, Renderer> From<List<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: ToString + Clone, + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(list: List<'a, T, Message, Renderer>) -> Self { + Element::new(list) + } +} diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs new file mode 100644 index 00000000..31bb0e86 --- /dev/null +++ b/widget/src/pane_grid.rs @@ -0,0 +1,1201 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid +mod axis; +mod configuration; +mod content; +mod direction; +mod draggable; +mod node; +mod pane; +mod split; +mod title_bar; + +pub mod state; + +pub use axis::Axis; +pub use configuration::Configuration; +pub use content::Content; +pub use direction::Direction; +pub use draggable::Draggable; +pub use node::Node; +pub use pane::Pane; +pub use split::Split; +pub use state::State; +pub use title_bar::TitleBar; + +pub use crate::style::pane_grid::{Appearance, Line, StyleSheet}; + +use crate::container; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay::{self, Group}; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, + Size, Vector, Widget, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/frailfreshairedaleterrier) +/// +/// This distribution of space is common in tiling window managers (like +/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +/// [`tmux`](https://github.com/tmux/tmux)). +/// +/// A [`PaneGrid`] supports: +/// +/// * Vertical and horizontal splits +/// * Tracking of the last active pane +/// * Mouse-based resizing +/// * Drag and drop to reorganize panes +/// * Hotkey support +/// * Configurable modifier keys +/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +/// +/// ## Example +/// +/// ```no_run +/// # use iced_widget::{pane_grid, text}; +/// # +/// # type PaneGrid<'a, Message> = +/// # iced_widget::PaneGrid<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// enum PaneState { +/// SomePane, +/// AnotherKindOfPane, +/// } +/// +/// enum Message { +/// PaneDragged(pane_grid::DragEvent), +/// PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); +/// +/// let pane_grid = +/// PaneGrid::new(&state, |pane, state, is_maximized| { +/// pane_grid::Content::new(match state { +/// PaneState::SomePane => text("This is some pane"), +/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), +/// }) +/// }) +/// .on_drag(Message::PaneDragged) +/// .on_resize(10, Message::PaneResized); +/// ``` +#[allow(missing_debug_implementations)] +pub struct PaneGrid<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + contents: Contents<'a, Content<'a, Message, Renderer>>, + width: Length, + height: Length, + spacing: f32, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + /// Creates a [`PaneGrid`] with the given [`State`] and view function. + /// + /// The view function will be called to display each [`Pane`] present in the + /// [`State`]. [`bool`] is set if the pane is maximized. + pub fn new<T>( + state: &'a State<T>, + view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>, + ) -> Self { + let contents = if let Some((pane, pane_state)) = + state.maximized.and_then(|pane| { + state.panes.get(&pane).map(|pane_state| (pane, pane_state)) + }) { + Contents::Maximized( + pane, + view(pane, pane_state, true), + Node::Pane(pane), + ) + } else { + Contents::All( + state + .panes + .iter() + .map(|(pane, pane_state)| { + (*pane, view(*pane, pane_state, false)) + }) + .collect(), + &state.internal, + ) + }; + + Self { + contents, + width: Length::Fill, + height: Length::Fill, + spacing: 0.0, + on_click: None, + on_drag: None, + on_resize: None, + style: Default::default(), + } + } + + /// Sets the width of the [`PaneGrid`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`PaneGrid`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the spacing _between_ the panes of the [`PaneGrid`]. + pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); + self + } + + /// Enables the drag and drop interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + pub fn on_drag<F>(mut self, f: F) -> Self + where + F: 'a + Fn(DragEvent) -> Message, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// Enables the resize interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + /// + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. + /// + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self + where + F: 'a + Fn(ResizeEvent) -> Message, + { + self.on_resize = Some((leeway.into().0, Box::new(f))); + self + } + + /// Sets the style of the [`PaneGrid`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + fn drag_enabled(&self) -> bool { + (!self.contents.is_maximized()) + .then(|| self.on_drag.is_some()) + .unwrap_or_default() + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for PaneGrid<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<state::Action>() + } + + fn state(&self) -> tree::State { + tree::State::new(state::Action::Idle) + } + + fn children(&self) -> Vec<Tree> { + self.contents + .iter() + .map(|(_, content)| content.state()) + .collect() + } + + fn diff(&self, tree: &mut Tree) { + match &self.contents { + Contents::All(contents, _) => tree.diff_children_custom( + contents, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ), + Contents::Maximized(_, content, _) => tree.diff_children_custom( + &[content], + |state, content| content.diff(state), + |content| content.state(), + ), + } + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.contents.layout(), + self.width, + self.height, + self.spacing, + self.contents.iter(), + |content, renderer, limits| content.layout(renderer, limits), + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.contents + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|(((_pane, content), state), layout)| { + content.operate(state, layout, renderer, operation); + }) + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let action = tree.state.downcast_mut::<state::Action>(); + + let on_drag = if self.drag_enabled() { + &self.on_drag + } else { + &None + }; + + let event_status = update( + action, + self.contents.layout(), + &event, + layout, + cursor, + shell, + self.spacing, + self.contents.iter(), + &self.on_click, + on_drag, + &self.on_resize, + ); + + let picked_pane = action.picked_pane().map(|(pane, _)| pane); + + self.contents + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|(((pane, content), tree), layout)| { + let is_picked = picked_pane == Some(pane); + + content.on_event( + tree, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref(), + self.contents.layout(), + layout, + cursor, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.contents + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor, + viewport, + renderer, + self.drag_enabled(), + ) + }) + .max() + .unwrap_or_default() + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref(), + self.contents.layout(), + layout, + cursor, + renderer, + theme, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + &self.style, + self.contents + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (pane, (content, tree))), + |(content, tree), renderer, style, layout, cursor, rectangle| { + content.draw( + tree, renderer, theme, style, layout, cursor, rectangle, + ); + }, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let children = self + .contents + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|(((_, content), state), layout)| { + content.overlay(state, layout, renderer) + }) + .collect::<Vec<_>>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) + } +} + +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} + +/// Calculates the [`Layout`] of a [`PaneGrid`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + node: &Node, + width: Length, + height: Length, + spacing: f32, + contents: impl Iterator<Item = (Pane, T)>, + layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + let size = limits.resolve(Size::ZERO); + + let regions = node.pane_regions(spacing, size); + let children = contents + .filter_map(|(pane, content)| { + let region = regions.get(&pane)?; + let size = Size::new(region.width, region.height); + + let mut node = layout_content( + content, + renderer, + &layout::Limits::new(size, size), + ); + + node.move_to(Point::new(region.x, region.y)); + + Some(node) + }) + .collect(); + + layout::Node::with_children(size, children) +} + +/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] +/// accordingly. +pub fn update<'a, Message, T: Draggable>( + action: &mut state::Action, + node: &Node, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + spacing: f32, + contents: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, +) -> event::Status { + let mut event_status = event::Status::Ignored; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if let Some(cursor_position) = cursor.position_over(bounds) { + event_status = event::Status::Captured; + + match on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = node.split_regions( + spacing, + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + spacing + leeway, + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + if action.picked_pane().is_none() { + *action = + state::Action::Resizing { split, axis }; + } + } else { + click_pane( + action, + layout, + cursor_position, + shell, + contents, + on_click, + on_drag, + ); + } + } + None => { + click_pane( + action, + layout, + cursor_position, + shell, + contents, + on_click, + on_drag, + ); + } + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, _)) = action.picked_pane() { + if let Some(on_drag) = on_drag { + if let Some(cursor_position) = cursor.position() { + let event = if let Some(edge) = + in_edge(layout, cursor_position) + { + DragEvent::Dropped { + pane, + target: Target::Edge(edge), + } + } else { + let dropped_region = contents + .zip(layout.children()) + .filter_map(|(target, layout)| { + layout_region(layout, cursor_position) + .map(|region| (target, region)) + }) + .next(); + + match dropped_region { + Some(((target, _), region)) + if pane != target => + { + DragEvent::Dropped { + pane, + target: Target::Pane(target, region), + } + } + _ => DragEvent::Canceled { pane }, + } + }; + + shell.publish(on_drag(event)); + } + } + + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + event_status = event::Status::Captured; + } + + *action = state::Action::Idle; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some((_, on_resize)) = on_resize { + if let Some((split, _)) = action.picked_split() { + let bounds = layout.bounds(); + + let splits = node.split_regions( + spacing, + Size::new(bounds.width, bounds.height), + ); + + if let Some((axis, rectangle, _)) = splits.get(&split) { + if let Some(cursor_position) = cursor.position() { + let ratio = match axis { + Axis::Horizontal => { + let position = cursor_position.y + - bounds.y + - rectangle.y; + + (position / rectangle.height) + .clamp(0.1, 0.9) + } + Axis::Vertical => { + let position = cursor_position.x + - bounds.x + - rectangle.x; + + (position / rectangle.width).clamp(0.1, 0.9) + } + }; + + shell.publish(on_resize(ResizeEvent { + split, + ratio, + })); + + event_status = event::Status::Captured; + } + } + } + } + } + _ => {} + } + + event_status +} + +fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> { + let bounds = layout.bounds(); + + if !bounds.contains(cursor_position) { + return None; + } + + let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) { + Region::Edge(Edge::Left) + } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) { + Region::Edge(Edge::Right) + } else if cursor_position.y < (bounds.y + bounds.height / 3.0) { + Region::Edge(Edge::Top) + } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) { + Region::Edge(Edge::Bottom) + } else { + Region::Center + }; + + Some(region) +} + +fn click_pane<'a, Message, T>( + action: &mut state::Action, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + contents: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, +) where + T: Draggable, +{ + let mut clicked_region = contents + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().contains(cursor_position)); + + if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(on_click) = &on_click { + shell.publish(on_click(pane)); + } + + if let Some(on_drag) = &on_drag { + if content.can_be_dragged_at(layout, cursor_position) { + let pane_position = layout.position(); + + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); + + *action = state::Action::Dragging { pane, origin }; + + shell.publish(on_drag(DragEvent::Picked { pane })); + } + } + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. +pub fn mouse_interaction( + action: &state::Action, + node: &Node, + layout: Layout<'_>, + cursor: mouse::Cursor, + spacing: f32, + resize_leeway: Option<f32>, +) -> Option<mouse::Interaction> { + if action.picked_pane().is_some() { + return Some(mouse::Interaction::Grabbing); + } + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let splits = node.split_regions(spacing, bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + hovered_split(splits.iter(), spacing + leeway, relative_cursor) + .map(|(_, axis, _)| axis) + }) + }); + + if let Some(resize_axis) = resize_axis { + return Some(match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }); + } + + None +} + +/// Draws a [`PaneGrid`]. +pub fn draw<Renderer, T>( + action: &state::Action, + node: &Node, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &mut Renderer, + theme: &Renderer::Theme, + default_style: &renderer::Style, + viewport: &Rectangle, + spacing: f32, + resize_leeway: Option<f32>, + style: &<Renderer::Theme as StyleSheet>::Style, + contents: impl Iterator<Item = (Pane, T)>, + draw_pane: impl Fn( + T, + &mut Renderer, + &renderer::Style, + Layout<'_>, + mouse::Cursor, + &Rectangle, + ), +) where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + let picked_pane = action.picked_pane(); + + let picked_split = action + .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); + + let splits = node.split_regions(spacing, bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = axis.split_line_bounds(*region, *ratio, spacing); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) + .or_else(|| match resize_leeway { + Some(leeway) => { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = node.split_regions(spacing, bounds.size()); + + let (_split, axis, region) = hovered_split( + splits.iter(), + spacing + leeway, + relative_cursor, + )?; + + Some((axis, region + Vector::new(bounds.x, bounds.y), false)) + } + None => None, + }); + + let pane_cursor = if picked_pane.is_some() { + mouse::Cursor::Unavailable + } else { + cursor + }; + + let mut render_picked_pane = None; + + let pane_in_edge = if picked_pane.is_some() { + cursor + .position() + .and_then(|cursor_position| in_edge(layout, cursor_position)) + } else { + None + }; + + for ((id, pane), pane_layout) in contents.zip(layout.children()) { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + render_picked_pane = Some((pane, origin, pane_layout)); + } + Some((dragging, _)) if id != dragging => { + draw_pane( + pane, + renderer, + default_style, + pane_layout, + pane_cursor, + viewport, + ); + + if picked_pane.is_some() && pane_in_edge.is_none() { + if let Some(region) = + cursor.position().and_then(|cursor_position| { + layout_region(pane_layout, cursor_position) + }) + { + let bounds = layout_region_bounds(pane_layout, region); + let hovered_region_style = theme.hovered_region(style); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: hovered_region_style + .border_radius, + border_width: hovered_region_style.border_width, + border_color: hovered_region_style.border_color, + }, + theme.hovered_region(style).background, + ); + } + } + } + _ => { + draw_pane( + pane, + renderer, + default_style, + pane_layout, + pane_cursor, + viewport, + ); + } + } + } + + if let Some(edge) = pane_in_edge { + let hovered_region_style = theme.hovered_region(style); + let bounds = edge_bounds(layout, edge); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: hovered_region_style.border_radius, + border_width: hovered_region_style.border_width, + border_color: hovered_region_style.border_color, + }, + theme.hovered_region(style).background, + ); + } + + // Render picked pane last + if let Some((pane, origin, layout)) = render_picked_pane { + if let Some(cursor_position) = cursor.position() { + let bounds = layout.bounds(); + + renderer.with_translation( + cursor_position + - Point::new(bounds.x + origin.x, bounds.y + origin.y), + |renderer| { + renderer.with_layer(bounds, |renderer| { + draw_pane( + pane, + renderer, + default_style, + layout, + pane_cursor, + viewport, + ); + }); + }, + ); + } + } + + if picked_pane.is_none() { + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + theme.picked_split(style) + } else { + theme.hovered_split(style) + }; + + if let Some(highlight) = highlight { + renderer.fill_quad( + renderer::Quad { + bounds: match axis { + Axis::Horizontal => Rectangle { + x: split_region.x, + y: (split_region.y + + (split_region.height - highlight.width) + / 2.0) + .round(), + width: split_region.width, + height: highlight.width, + }, + Axis::Vertical => Rectangle { + x: (split_region.x + + (split_region.width - highlight.width) + / 2.0) + .round(), + y: split_region.y, + width: highlight.width, + height: split_region.height, + }, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + highlight.color, + ); + } + } + } +} + +const THICKNESS_RATIO: f32 = 25.0; + +fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> { + let bounds = layout.bounds(); + + let height_thickness = bounds.height / THICKNESS_RATIO; + let width_thickness = bounds.width / THICKNESS_RATIO; + let thickness = height_thickness.min(width_thickness); + + if cursor.x > bounds.x && cursor.x < bounds.x + thickness { + Some(Edge::Left) + } else if cursor.x > bounds.x + bounds.width - thickness + && cursor.x < bounds.x + bounds.width + { + Some(Edge::Right) + } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness { + Some(Edge::Top) + } else if cursor.y > bounds.y + bounds.height - thickness + && cursor.y < bounds.y + bounds.height + { + Some(Edge::Bottom) + } else { + None + } +} + +fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle { + let bounds = layout.bounds(); + + let height_thickness = bounds.height / THICKNESS_RATIO; + let width_thickness = bounds.width / THICKNESS_RATIO; + let thickness = height_thickness.min(width_thickness); + + match edge { + Edge::Top => Rectangle { + height: thickness, + ..bounds + }, + Edge::Left => Rectangle { + width: thickness, + ..bounds + }, + Edge::Right => Rectangle { + x: bounds.x + bounds.width - thickness, + width: thickness, + ..bounds + }, + Edge::Bottom => Rectangle { + y: bounds.y + bounds.height - thickness, + height: thickness, + ..bounds + }, + } +} + +fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle { + let bounds = layout.bounds(); + + match region { + Region::Center => bounds, + Region::Edge(edge) => match edge { + Edge::Top => Rectangle { + height: bounds.height / 2.0, + ..bounds + }, + Edge::Left => Rectangle { + width: bounds.width / 2.0, + ..bounds + }, + Edge::Right => Rectangle { + x: bounds.x + bounds.width / 2.0, + width: bounds.width / 2.0, + ..bounds + }, + Edge::Bottom => Rectangle { + y: bounds.y + bounds.height / 2.0, + height: bounds.height / 2.0, + ..bounds + }, + }, + } +} + +/// An event produced during a drag and drop interaction of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy)] +pub enum DragEvent { + /// A [`Pane`] was picked for dragging. + Picked { + /// The picked [`Pane`]. + pane: Pane, + }, + + /// A [`Pane`] was dropped on top of another [`Pane`]. + Dropped { + /// The picked [`Pane`]. + pane: Pane, + + /// The [`Target`] where the picked [`Pane`] was dropped on. + target: Target, + }, + + /// A [`Pane`] was picked and then dropped outside of other [`Pane`] + /// boundaries. + Canceled { + /// The picked [`Pane`]. + pane: Pane, + }, +} + +/// The [`Target`] area a pane can be dropped on. +#[derive(Debug, Clone, Copy)] +pub enum Target { + /// An [`Edge`] of the full [`PaneGrid`]. + Edge(Edge), + /// A single [`Pane`] of the [`PaneGrid`]. + Pane(Pane, Region), +} + +/// The region of a [`Pane`]. +#[derive(Debug, Clone, Copy, Default)] +pub enum Region { + /// Center region. + #[default] + Center, + /// Edge region. + Edge(Edge), +} + +/// The edges of an area. +#[derive(Debug, Clone, Copy)] +pub enum Edge { + /// Top edge. + Top, + /// Left edge. + Left, + /// Right edge. + Right, + /// Bottom edge. + Bottom, +} + +/// An event produced during a resize interaction of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy)] +pub struct ResizeEvent { + /// The [`Split`] that is being dragged for resizing. + pub split: Split, + + /// The new ratio of the [`Split`]. + /// + /// The ratio is a value in [0, 1], representing the exact position of a + /// [`Split`] between two panes. + pub ratio: f32, +} + +/* + * Helpers + */ +fn hovered_split<'a>( + splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, + spacing: f32, + cursor_position: Point, +) -> Option<(Split, Axis, Rectangle)> { + splits + .filter_map(|(split, (axis, region, ratio))| { + let bounds = axis.split_line_bounds(*region, *ratio, spacing); + + if bounds.contains(cursor_position) { + Some((*split, *axis, bounds)) + } else { + None + } + }) + .next() +} + +/// The visible contents of the [`PaneGrid`] +#[derive(Debug)] +pub enum Contents<'a, T> { + /// All panes are visible + All(Vec<(Pane, T)>, &'a state::Internal), + /// A maximized pane is visible + Maximized(Pane, T, Node), +} + +impl<'a, T> Contents<'a, T> { + /// Returns the layout [`Node`] of the [`Contents`] + pub fn layout(&self) -> &Node { + match self { + Contents::All(_, state) => state.layout(), + Contents::Maximized(_, _, layout) => layout, + } + } + + /// Returns an iterator over the values of the [`Contents`] + pub fn iter(&self) -> Box<dyn Iterator<Item = (Pane, &T)> + '_> { + match self { + Contents::All(contents, _) => Box::new( + contents.iter().map(|(pane, content)| (*pane, content)), + ), + Contents::Maximized(pane, content, _) => { + Box::new(std::iter::once((*pane, content))) + } + } + } + + fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (Pane, &mut T)> + '_> { + match self { + Contents::All(contents, _) => Box::new( + contents.iter_mut().map(|(pane, content)| (*pane, content)), + ), + Contents::Maximized(pane, content, _) => { + Box::new(std::iter::once((*pane, content))) + } + } + } + + fn is_maximized(&self) -> bool { + matches!(self, Self::Maximized(..)) + } +} diff --git a/widget/src/pane_grid/axis.rs b/widget/src/pane_grid/axis.rs new file mode 100644 index 00000000..a3049230 --- /dev/null +++ b/widget/src/pane_grid/axis.rs @@ -0,0 +1,241 @@ +use crate::core::Rectangle; + +/// A fixed reference line for the measurement of coordinates. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Axis { + /// The horizontal axis: — + Horizontal, + /// The vertical axis: | + Vertical, +} + +impl Axis { + /// Splits the provided [`Rectangle`] on the current [`Axis`] with the + /// given `ratio` and `spacing`. + pub fn split( + &self, + rectangle: &Rectangle, + ratio: f32, + spacing: f32, + ) -> (Rectangle, Rectangle) { + match self { + Axis::Horizontal => { + let height_top = + (rectangle.height * ratio - spacing / 2.0).round(); + let height_bottom = rectangle.height - height_top - spacing; + + ( + Rectangle { + height: height_top, + ..*rectangle + }, + Rectangle { + y: rectangle.y + height_top + spacing, + height: height_bottom, + ..*rectangle + }, + ) + } + Axis::Vertical => { + let width_left = + (rectangle.width * ratio - spacing / 2.0).round(); + let width_right = rectangle.width - width_left - spacing; + + ( + Rectangle { + width: width_left, + ..*rectangle + }, + Rectangle { + x: rectangle.x + width_left + spacing, + width: width_right, + ..*rectangle + }, + ) + } + } + } + + /// Calculates the bounds of the split line in a [`Rectangle`] region. + pub fn split_line_bounds( + &self, + rectangle: Rectangle, + ratio: f32, + spacing: f32, + ) -> Rectangle { + match self { + Axis::Horizontal => Rectangle { + x: rectangle.x, + y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) + .round(), + width: rectangle.width, + height: spacing, + }, + Axis::Vertical => Rectangle { + x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) + .round(), + y: rectangle.y, + width: spacing, + height: rectangle.height, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + enum Case { + Horizontal { + overall_height: f32, + spacing: f32, + top_height: f32, + bottom_y: f32, + bottom_height: f32, + }, + Vertical { + overall_width: f32, + spacing: f32, + left_width: f32, + right_x: f32, + right_width: f32, + }, + } + + #[test] + fn split() { + let cases = vec![ + // Even height, even spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, even spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 3.0, + }, + // Even height, odd spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 1.0, + top_height: 5.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, odd spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 1.0, + top_height: 4.0, + bottom_y: 5.0, + bottom_height: 4.0, + }, + // Even width, even spacing + Case::Vertical { + overall_width: 10.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, even spacing + Case::Vertical { + overall_width: 9.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 3.0, + }, + // Even width, odd spacing + Case::Vertical { + overall_width: 10.0, + spacing: 1.0, + left_width: 5.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, odd spacing + Case::Vertical { + overall_width: 9.0, + spacing: 1.0, + left_width: 4.0, + right_x: 5.0, + right_width: 4.0, + }, + ]; + for case in cases { + match case { + Case::Horizontal { + overall_height, + spacing, + top_height, + bottom_y, + bottom_height, + } => { + let a = Axis::Horizontal; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: 10.0, + height: overall_height, + }; + let (top, bottom) = a.split(&r, 0.5, spacing); + assert_eq!( + top, + Rectangle { + height: top_height, + ..r + } + ); + assert_eq!( + bottom, + Rectangle { + y: bottom_y, + height: bottom_height, + ..r + } + ); + } + Case::Vertical { + overall_width, + spacing, + left_width, + right_x, + right_width, + } => { + let a = Axis::Vertical; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: overall_width, + height: 10.0, + }; + let (left, right) = a.split(&r, 0.5, spacing); + assert_eq!( + left, + Rectangle { + width: left_width, + ..r + } + ); + assert_eq!( + right, + Rectangle { + x: right_x, + width: right_width, + ..r + } + ); + } + } + } + } +} diff --git a/widget/src/pane_grid/configuration.rs b/widget/src/pane_grid/configuration.rs new file mode 100644 index 00000000..ddbc3bc2 --- /dev/null +++ b/widget/src/pane_grid/configuration.rs @@ -0,0 +1,26 @@ +use crate::pane_grid::Axis; + +/// The arrangement of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub enum Configuration<T> { + /// A split of the available space. + Split { + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Configuration`] of the split. + a: Box<Configuration<T>>, + + /// The right/bottom [`Configuration`] of the split. + b: Box<Configuration<T>>, + }, + /// A [`Pane`]. + /// + /// [`Pane`]: crate::widget::pane_grid::Pane + Pane(T), +} diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs new file mode 100644 index 00000000..c28ae6e3 --- /dev/null +++ b/widget/src/pane_grid/content.rs @@ -0,0 +1,379 @@ +use crate::container; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{self, Tree}; +use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::pane_grid::{Draggable, TitleBar}; + +/// The content of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + title_bar: Option<TitleBar<'a, Message, Renderer>>, + body: Element<'a, Message, Renderer>, + style: <Renderer::Theme as container::StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + /// Creates a new [`Content`] with the provided body. + pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { + Self { + title_bar: None, + body: body.into(), + style: Default::default(), + } + } + + /// Sets the [`TitleBar`] of this [`Content`]. + pub fn title_bar( + mut self, + title_bar: TitleBar<'a, Message, Renderer>, + ) -> Self { + self.title_bar = Some(title_bar); + self + } + + /// Sets the style of the [`Content`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + pub(super) fn state(&self) -> Tree { + let children = if let Some(title_bar) = self.title_bar.as_ref() { + vec![Tree::new(&self.body), title_bar.state()] + } else { + vec![Tree::new(&self.body), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub(super) fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(title_bar) = self.title_bar.as_ref() { + title_bar.diff(&mut tree.children[1]); + } + + tree.children[0].diff(&self.body); + } else { + *tree = self.state(); + } + } + + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + use container::StyleSheet; + + let bounds = layout.bounds(); + + { + let style = theme.appearance(&self.style); + + container::draw_background(renderer, &style, bounds); + } + + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + let body_layout = children.next().unwrap(); + + let show_controls = cursor.is_over(bounds); + + self.body.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + body_layout, + cursor, + viewport, + ); + + title_bar.draw( + &tree.children[1], + renderer, + theme, + style, + title_bar_layout, + cursor, + viewport, + show_controls, + ); + } else { + self.body.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if let Some(title_bar) = &self.title_bar { + let max_size = limits.max(); + + let title_bar_layout = title_bar + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_bar_size = title_bar_layout.size(); + + let mut body_layout = self.body.as_widget().layout( + renderer, + &layout::Limits::new( + Size::ZERO, + Size::new( + max_size.width, + max_size.height - title_bar_size.height, + ), + ), + ); + + body_layout.move_to(Point::new(0.0, title_bar_size.height)); + + layout::Node::with_children( + max_size, + vec![title_bar_layout, body_layout], + ) + } else { + self.body.as_widget().layout(renderer, limits) + } + } + + pub(crate) fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + let body_layout = if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + + title_bar.operate( + &mut tree.children[1], + children.next().unwrap(), + renderer, + operation, + ); + + children.next().unwrap() + } else { + layout + }; + + self.body.as_widget().operate( + &mut tree.children[0], + body_layout, + renderer, + operation, + ); + } + + pub(crate) fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + is_picked: bool, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + let body_layout = if let Some(title_bar) = &mut self.title_bar { + let mut children = layout.children(); + + event_status = title_bar.on_event( + &mut tree.children[1], + event.clone(), + children.next().unwrap(), + cursor, + renderer, + clipboard, + shell, + ); + + children.next().unwrap() + } else { + layout + }; + + let body_status = if is_picked { + event::Status::Ignored + } else { + self.body.as_widget_mut().on_event( + &mut tree.children[0], + event, + body_layout, + cursor, + renderer, + clipboard, + shell, + ) + }; + + event_status.merge(body_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + drag_enabled: bool, + ) -> mouse::Interaction { + let (body_layout, title_bar_interaction) = if let Some(title_bar) = + &self.title_bar + { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + let is_over_pick_area = cursor + .position() + .map(|cursor_position| { + title_bar + .is_over_pick_area(title_bar_layout, cursor_position) + }) + .unwrap_or_default(); + + if is_over_pick_area && drag_enabled { + return mouse::Interaction::Grab; + } + + let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], + title_bar_layout, + cursor, + viewport, + renderer, + ); + + (children.next().unwrap(), mouse_interaction) + } else { + (layout, mouse::Interaction::default()) + }; + + self.body + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor, + viewport, + renderer, + ) + .max(title_bar_interaction) + } + + pub(crate) fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + if let Some(title_bar) = self.title_bar.as_mut() { + let mut children = layout.children(); + let title_bar_layout = children.next()?; + + let mut states = tree.children.iter_mut(); + let body_state = states.next().unwrap(); + let title_bar_state = states.next().unwrap(); + + match title_bar.overlay(title_bar_state, title_bar_layout, renderer) + { + Some(overlay) => Some(overlay), + None => self.body.as_widget_mut().overlay( + body_state, + children.next()?, + renderer, + ), + } + } else { + self.body.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } + } +} + +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where + T: Into<Element<'a, Message, Renderer>>, + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + fn from(element: T) -> Self { + Self::new(element) + } +} diff --git a/widget/src/pane_grid/direction.rs b/widget/src/pane_grid/direction.rs new file mode 100644 index 00000000..b31a8737 --- /dev/null +++ b/widget/src/pane_grid/direction.rs @@ -0,0 +1,12 @@ +/// A four cardinal direction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// ↑ + Up, + /// ↓ + Down, + /// ← + Left, + /// → + Right, +} diff --git a/widget/src/pane_grid/draggable.rs b/widget/src/pane_grid/draggable.rs new file mode 100644 index 00000000..9d31feb5 --- /dev/null +++ b/widget/src/pane_grid/draggable.rs @@ -0,0 +1,8 @@ +use crate::core::{Layout, Point}; + +/// A pane that can be dragged. +pub trait Draggable { + /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked + /// at the provided cursor position. + fn can_be_dragged_at(&self, layout: Layout<'_>, cursor: Point) -> bool; +} diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs new file mode 100644 index 00000000..6de5920f --- /dev/null +++ b/widget/src/pane_grid/node.rs @@ -0,0 +1,260 @@ +use crate::core::{Rectangle, Size}; +use crate::pane_grid::{Axis, Pane, Split}; + +use std::collections::BTreeMap; + +/// A layout node of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub enum Node { + /// The region of this [`Node`] is split into two. + Split { + /// The [`Split`] of this [`Node`]. + id: Split, + + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Node`] of the split. + a: Box<Node>, + + /// The right/bottom [`Node`] of the split. + b: Box<Node>, + }, + /// The region of this [`Node`] is taken by a [`Pane`]. + Pane(Pane), +} + +impl Node { + /// Returns an iterator over each [`Split`] in this [`Node`]. + pub fn splits(&self) -> impl Iterator<Item = &Split> { + let mut unvisited_nodes = vec![self]; + + std::iter::from_fn(move || { + while let Some(node) = unvisited_nodes.pop() { + if let Node::Split { id, a, b, .. } = node { + unvisited_nodes.push(a); + unvisited_nodes.push(b); + + return Some(id); + } + } + + None + }) + } + + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given + /// the spacing between panes and the total available space. + pub fn pane_regions( + &self, + spacing: f32, + size: Size, + ) -> BTreeMap<Pane, Rectangle> { + let mut regions = BTreeMap::new(); + + self.compute_regions( + spacing, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut regions, + ); + + regions + } + + /// Returns the axis, rectangular region, and ratio for each [`Split`] in + /// the [`Node`] given the spacing between panes and the total available + /// space. + pub fn split_regions( + &self, + spacing: f32, + size: Size, + ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { + let mut splits = BTreeMap::new(); + + self.compute_splits( + spacing, + &Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + &mut splits, + ); + + splits + } + + pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> { + match self { + Node::Split { a, b, .. } => { + a.find(pane).or_else(move || b.find(pane)) + } + Node::Pane(p) => { + if p == pane { + Some(self) + } else { + None + } + } + } + } + + pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { + *self = Node::Split { + id, + axis, + ratio: 0.5, + a: Box::new(self.clone()), + b: Box::new(Node::Pane(new_pane)), + }; + } + + pub(crate) fn split_inverse(&mut self, id: Split, axis: Axis, pane: Pane) { + *self = Node::Split { + id, + axis, + ratio: 0.5, + a: Box::new(Node::Pane(pane)), + b: Box::new(self.clone()), + }; + } + + pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { + if let Node::Split { a, b, .. } = self { + a.update(f); + b.update(f); + } + + f(self); + } + + pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool { + match self { + Node::Split { + id, ratio, a, b, .. + } => { + if id == split { + *ratio = percentage; + + true + } else if a.resize(split, percentage) { + true + } else { + b.resize(split, percentage) + } + } + Node::Pane(_) => false, + } + } + + pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> { + match self { + Node::Split { a, b, .. } => { + if a.pane() == Some(*pane) { + *self = *b.clone(); + Some(self.first_pane()) + } else if b.pane() == Some(*pane) { + *self = *a.clone(); + Some(self.first_pane()) + } else { + a.remove(pane).or_else(|| b.remove(pane)) + } + } + Node::Pane(_) => None, + } + } + + fn pane(&self) -> Option<Pane> { + match self { + Node::Split { .. } => None, + Node::Pane(pane) => Some(*pane), + } + } + + fn first_pane(&self) -> Pane { + match self { + Node::Split { a, .. } => a.first_pane(), + Node::Pane(pane) => *pane, + } + } + + fn compute_regions( + &self, + spacing: f32, + current: &Rectangle, + regions: &mut BTreeMap<Pane, Rectangle>, + ) { + match self { + Node::Split { + axis, ratio, a, b, .. + } => { + let (region_a, region_b) = axis.split(current, *ratio, spacing); + + a.compute_regions(spacing, ®ion_a, regions); + b.compute_regions(spacing, ®ion_b, regions); + } + Node::Pane(pane) => { + let _ = regions.insert(*pane, *current); + } + } + } + + fn compute_splits( + &self, + spacing: f32, + current: &Rectangle, + splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>, + ) { + match self { + Node::Split { + axis, + ratio, + a, + b, + id, + } => { + let (region_a, region_b) = axis.split(current, *ratio, spacing); + + let _ = splits.insert(*id, (*axis, *current, *ratio)); + + a.compute_splits(spacing, ®ion_a, splits); + b.compute_splits(spacing, ®ion_b, splits); + } + Node::Pane(_) => {} + } + } +} + +impl std::hash::Hash for Node { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + match self { + Node::Split { + id, + axis, + ratio, + a, + b, + } => { + id.hash(state); + axis.hash(state); + ((ratio * 100_000.0) as u32).hash(state); + a.hash(state); + b.hash(state); + } + Node::Pane(pane) => { + pane.hash(state); + } + } + } +} diff --git a/widget/src/pane_grid/pane.rs b/widget/src/pane_grid/pane.rs new file mode 100644 index 00000000..d6fbab83 --- /dev/null +++ b/widget/src/pane_grid/pane.rs @@ -0,0 +1,5 @@ +/// A rectangular region in a [`PaneGrid`] used to display widgets. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Pane(pub(super) usize); diff --git a/widget/src/pane_grid/split.rs b/widget/src/pane_grid/split.rs new file mode 100644 index 00000000..8132272a --- /dev/null +++ b/widget/src/pane_grid/split.rs @@ -0,0 +1,5 @@ +/// A divider that splits a region in a [`PaneGrid`] into two different panes. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Split(pub(super) usize); diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs new file mode 100644 index 00000000..6fd15890 --- /dev/null +++ b/widget/src/pane_grid/state.rs @@ -0,0 +1,447 @@ +//! The state of a [`PaneGrid`]. +//! +//! [`PaneGrid`]: crate::widget::PaneGrid +use crate::core::{Point, Size}; +use crate::pane_grid::{ + Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, +}; + +use std::collections::HashMap; + +/// The state of a [`PaneGrid`]. +/// +/// It keeps track of the state of each [`Pane`] and the position of each +/// [`Split`]. +/// +/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is +/// why this struct is generic over the type `T`. Values of this type are +/// provided to the view function of [`PaneGrid::new`] for displaying each +/// [`Pane`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +/// [`PaneGrid::new`]: crate::widget::PaneGrid::new +#[derive(Debug, Clone)] +pub struct State<T> { + /// The panes of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub panes: HashMap<Pane, T>, + + /// The internal state of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub internal: Internal, + + /// The maximized [`Pane`] of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub(super) maximized: Option<Pane>, +} + +impl<T> State<T> { + /// Creates a new [`State`], initializing the first pane with the provided + /// state. + /// + /// Alongside the [`State`], it returns the first [`Pane`] identifier. + pub fn new(first_pane_state: T) -> (Self, Pane) { + ( + Self::with_configuration(Configuration::Pane(first_pane_state)), + Pane(0), + ) + } + + /// Creates a new [`State`] with the given [`Configuration`]. + pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { + let mut panes = HashMap::new(); + + let internal = + Internal::from_configuration(&mut panes, config.into(), 0); + + State { + panes, + internal, + maximized: None, + } + } + + /// Returns the total amount of panes in the [`State`]. + pub fn len(&self) -> usize { + self.panes.len() + } + + /// Returns `true` if the amount of panes in the [`State`] is 0. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the internal state of the given [`Pane`], if it exists. + pub fn get(&self, pane: &Pane) -> Option<&T> { + self.panes.get(pane) + } + + /// Returns the internal state of the given [`Pane`] with mutability, if it + /// exists. + pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { + self.panes.get_mut(pane) + } + + /// Returns an iterator over all the panes of the [`State`], alongside its + /// internal state. + pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { + self.panes.iter() + } + + /// Returns a mutable iterator over all the panes of the [`State`], + /// alongside its internal state. + pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { + self.panes.iter_mut() + } + + /// Returns the layout of the [`State`]. + pub fn layout(&self) -> &Node { + &self.internal.layout + } + + /// Returns the adjacent [`Pane`] of another [`Pane`] in the given + /// direction, if there is one. + pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { + let regions = self + .internal + .layout + .pane_regions(0.0, Size::new(4096.0, 4096.0)); + + let current_region = regions.get(pane)?; + + let target = match direction { + Direction::Left => { + Point::new(current_region.x - 1.0, current_region.y + 1.0) + } + Direction::Right => Point::new( + current_region.x + current_region.width + 1.0, + current_region.y + 1.0, + ), + Direction::Up => { + Point::new(current_region.x + 1.0, current_region.y - 1.0) + } + Direction::Down => Point::new( + current_region.x + 1.0, + current_region.y + current_region.height + 1.0, + ), + }; + + let mut colliding_regions = + regions.iter().filter(|(_, region)| region.contains(target)); + + let (pane, _) = colliding_regions.next()?; + + Some(*pane) + } + + /// Splits the given [`Pane`] into two in the given [`Axis`] and + /// initializing the new [`Pane`] with the provided internal state. + pub fn split( + &mut self, + axis: Axis, + pane: &Pane, + state: T, + ) -> Option<(Pane, Split)> { + self.split_node(axis, Some(pane), state, false) + } + + /// Split a target [`Pane`] with a given [`Pane`] on a given [`Region`]. + /// + /// Panes will be swapped by default for [`Region::Center`]. + pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) { + match region { + Region::Center => self.swap(pane, target), + Region::Edge(edge) => match edge { + Edge::Top => { + self.split_and_swap(Axis::Horizontal, target, pane, true) + } + Edge::Bottom => { + self.split_and_swap(Axis::Horizontal, target, pane, false) + } + Edge::Left => { + self.split_and_swap(Axis::Vertical, target, pane, true) + } + Edge::Right => { + self.split_and_swap(Axis::Vertical, target, pane, false) + } + }, + } + } + + /// Drops the given [`Pane`] into the provided [`Target`]. + pub fn drop(&mut self, pane: &Pane, target: Target) { + match target { + Target::Edge(edge) => self.move_to_edge(pane, edge), + Target::Pane(target, region) => { + self.split_with(&target, pane, region) + } + } + } + + fn split_node( + &mut self, + axis: Axis, + pane: Option<&Pane>, + state: T, + inverse: bool, + ) -> Option<(Pane, Split)> { + let node = if let Some(pane) = pane { + self.internal.layout.find(pane)? + } else { + // Major node + &mut self.internal.layout + }; + + let new_pane = { + self.internal.last_id = self.internal.last_id.checked_add(1)?; + + Pane(self.internal.last_id) + }; + + let new_split = { + self.internal.last_id = self.internal.last_id.checked_add(1)?; + + Split(self.internal.last_id) + }; + + if inverse { + node.split_inverse(new_split, axis, new_pane); + } else { + node.split(new_split, axis, new_pane); + } + + let _ = self.panes.insert(new_pane, state); + let _ = self.maximized.take(); + + Some((new_pane, new_split)) + } + + fn split_and_swap( + &mut self, + axis: Axis, + target: &Pane, + pane: &Pane, + swap: bool, + ) { + if let Some((state, _)) = self.close(pane) { + if let Some((new_pane, _)) = self.split(axis, target, state) { + if swap { + self.swap(target, &new_pane); + } + } + } + } + + /// Move [`Pane`] to an [`Edge`] of the [`PaneGrid`]. + pub fn move_to_edge(&mut self, pane: &Pane, edge: Edge) { + match edge { + Edge::Top => { + self.split_major_node_and_swap(Axis::Horizontal, pane, true) + } + Edge::Bottom => { + self.split_major_node_and_swap(Axis::Horizontal, pane, false) + } + Edge::Left => { + self.split_major_node_and_swap(Axis::Vertical, pane, true) + } + Edge::Right => { + self.split_major_node_and_swap(Axis::Vertical, pane, false) + } + } + } + + fn split_major_node_and_swap( + &mut self, + axis: Axis, + pane: &Pane, + swap: bool, + ) { + if let Some((state, _)) = self.close(pane) { + let _ = self.split_node(axis, None, state, swap); + } + } + + /// Swaps the position of the provided panes in the [`State`]. + /// + /// If you want to swap panes on drag and drop in your [`PaneGrid`], you + /// will need to call this method when handling a [`DragEvent`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`DragEvent`]: crate::widget::pane_grid::DragEvent + pub fn swap(&mut self, a: &Pane, b: &Pane) { + self.internal.layout.update(&|node| match node { + Node::Split { .. } => {} + Node::Pane(pane) => { + if pane == a { + *node = Node::Pane(*b); + } else if pane == b { + *node = Node::Pane(*a); + } + } + }); + } + + /// Resizes two panes by setting the position of the provided [`Split`]. + /// + /// The ratio is a value in [0, 1], representing the exact position of a + /// [`Split`] between two panes. + /// + /// If you want to enable resize interactions in your [`PaneGrid`], you will + /// need to call this method when handling a [`ResizeEvent`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent + pub fn resize(&mut self, split: &Split, ratio: f32) { + let _ = self.internal.layout.resize(split, ratio); + } + + /// Closes the given [`Pane`] and returns its internal state and its closest + /// sibling, if it exists. + pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { + if self.maximized == Some(*pane) { + let _ = self.maximized.take(); + } + + if let Some(sibling) = self.internal.layout.remove(pane) { + self.panes.remove(pane).map(|state| (state, sibling)) + } else { + None + } + } + + /// Maximize the given [`Pane`]. Only this pane will be rendered by the + /// [`PaneGrid`] until [`Self::restore()`] is called. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn maximize(&mut self, pane: &Pane) { + self.maximized = Some(*pane); + } + + /// Restore the currently maximized [`Pane`] to it's normal size. All panes + /// will be rendered by the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn restore(&mut self) { + let _ = self.maximized.take(); + } + + /// Returns the maximized [`Pane`] of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn maximized(&self) -> Option<Pane> { + self.maximized + } +} + +/// The internal state of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub struct Internal { + layout: Node, + last_id: usize, +} + +impl Internal { + /// Initializes the [`Internal`] state of a [`PaneGrid`] from a + /// [`Configuration`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn from_configuration<T>( + panes: &mut HashMap<Pane, T>, + content: Configuration<T>, + next_id: usize, + ) -> Self { + let (layout, last_id) = match content { + Configuration::Split { axis, ratio, a, b } => { + let Internal { + layout: a, + last_id: next_id, + .. + } = Self::from_configuration(panes, *a, next_id); + + let Internal { + layout: b, + last_id: next_id, + .. + } = Self::from_configuration(panes, *b, next_id); + + ( + Node::Split { + id: Split(next_id), + axis, + ratio, + a: Box::new(a), + b: Box::new(b), + }, + next_id + 1, + ) + } + Configuration::Pane(state) => { + let id = Pane(next_id); + let _ = panes.insert(id, state); + + (Node::Pane(id), next_id + 1) + } + }; + + Self { layout, last_id } + } +} + +/// The current action of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { + /// The [`PaneGrid`] is idle. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + Idle, + /// A [`Pane`] in the [`PaneGrid`] is being dragged. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + Dragging { + /// The [`Pane`] being dragged. + pane: Pane, + /// The starting [`Point`] of the drag interaction. + origin: Point, + }, + /// A [`Split`] in the [`PaneGrid`] is being dragged. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + Resizing { + /// The [`Split`] being dragged. + split: Split, + /// The [`Axis`] of the [`Split`]. + axis: Axis, + }, +} + +impl Action { + /// Returns the current [`Pane`] that is being dragged, if any. + pub fn picked_pane(&self) -> Option<(Pane, Point)> { + match *self { + Action::Dragging { pane, origin, .. } => Some((pane, origin)), + _ => None, + } + } + + /// Returns the current [`Split`] that is being dragged, if any. + pub fn picked_split(&self) -> Option<(Split, Axis)> { + match *self { + Action::Resizing { split, axis, .. } => Some((split, axis)), + _ => None, + } + } +} + +impl Internal { + /// The layout [`Node`] of the [`Internal`] state + pub fn layout(&self) -> &Node { + &self.layout + } +} diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs new file mode 100644 index 00000000..2fe79f80 --- /dev/null +++ b/widget/src/pane_grid/title_bar.rs @@ -0,0 +1,432 @@ +use crate::container; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{self, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, +}; + +/// The title bar of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct TitleBar<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + content: Element<'a, Message, Renderer>, + controls: Option<Element<'a, Message, Renderer>>, + padding: Padding, + always_show_controls: bool, + style: <Renderer::Theme as container::StyleSheet>::Style, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + /// Creates a new [`TitleBar`] with the given content. + pub fn new<E>(content: E) -> Self + where + E: Into<Element<'a, Message, Renderer>>, + { + Self { + content: content.into(), + controls: None, + padding: Padding::ZERO, + always_show_controls: false, + style: Default::default(), + } + } + + /// Sets the controls of the [`TitleBar`]. + pub fn controls( + mut self, + controls: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the style of the [`TitleBar`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are + /// always visible. + /// + /// By default, the controls are only visible when the [`Pane`] of this + /// [`TitleBar`] is hovered. + /// + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane + pub fn always_show_controls(mut self) -> Self { + self.always_show_controls = true; + self + } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + pub(super) fn state(&self) -> Tree { + let children = if let Some(controls) = self.controls.as_ref() { + vec![Tree::new(&self.content), Tree::new(controls)] + } else { + vec![Tree::new(&self.content), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub(super) fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(controls) = self.controls.as_ref() { + tree.children[1].diff(controls); + } + + tree.children[0].diff(&self.content); + } else { + *tree = self.state(); + } + } + + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + show_controls: bool, + ) { + use container::StyleSheet; + + let bounds = layout.bounds(); + let style = theme.appearance(&self.style); + let inherited_style = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + container::draw_background(renderer, &style, bounds); + + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + let mut show_title = true; + + if let Some(controls) = &self.controls { + if show_controls || self.always_show_controls { + let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } + + controls.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); + } + } + + if show_title { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &inherited_style, + title_layout, + cursor, + viewport, + ); + } + } + + /// Returns whether the mouse cursor is over the pick area of the + /// [`TitleBar`] or not. + /// + /// The whole [`TitleBar`] is a pick area, except its controls. + pub fn is_over_pick_area( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if layout.bounds().contains(cursor_position) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + if self.controls.is_some() { + let controls_layout = children.next().unwrap(); + + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + !controls_layout.bounds().contains(cursor_position) + } else { + !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } + } else { + !title_layout.bounds().contains(cursor_position) + } + } else { + false + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.pad(self.padding); + let max_size = limits.max(); + + let title_layout = self + .content + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_size = title_layout.size(); + + let mut node = if let Some(controls) = &self.controls { + let mut controls_layout = controls + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let controls_size = controls_layout.size(); + let space_before_controls = max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + controls_layout.move_to(Point::new(space_before_controls, 0.0)); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![title_layout, controls_layout], + ) + } else { + layout::Node::with_children( + Size::new(max_size.width, title_size.height), + vec![title_layout], + ) + }; + + node.move_to(Point::new(self.padding.left, self.padding.top)); + + layout::Node::with_children(node.size().pad(self.padding), vec![node]) + } + + pub(crate) fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + let mut show_title = true; + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } + + controls.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ) + }; + + if show_title { + self.content.as_widget().operate( + &mut tree.children[0], + title_layout, + renderer, + operation, + ) + } + } + + pub(crate) fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + let mut show_title = true; + + let control_status = if let Some(controls) = &mut self.controls { + let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } + + controls.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; + + let title_status = if show_title { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + title_layout, + cursor, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; + + control_status.merge(title_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let title_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], + title_layout, + cursor, + viewport, + renderer, + ); + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + let controls_interaction = controls.as_widget().mouse_interaction( + &tree.children[1], + controls_layout, + cursor, + viewport, + renderer, + ); + + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + controls_interaction + } else { + controls_interaction.max(title_interaction) + } + } else { + title_interaction + } + } + + pub(crate) fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let mut children = layout.children(); + let padded = children.next()?; + + let mut children = padded.children(); + let title_layout = children.next()?; + + let Self { + content, controls, .. + } = self; + + let mut states = tree.children.iter_mut(); + let title_state = states.next().unwrap(); + let controls_state = states.next().unwrap(); + + content + .as_widget_mut() + .overlay(title_state, title_layout, renderer) + .or_else(move || { + controls.as_mut().and_then(|controls| { + let controls_layout = children.next()?; + + controls.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + ) + }) + }) + } +} diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs new file mode 100644 index 00000000..832aae6b --- /dev/null +++ b/widget/src/pick_list.rs @@ -0,0 +1,712 @@ +//! Display a dropdown list of selectable values. +use crate::container; +use crate::core::alignment; +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::text::{self, Text}; +use crate::core::touch; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, + Size, Widget, +}; +use crate::overlay::menu::{self, Menu}; +use crate::scrollable; + +use std::borrow::Cow; + +pub use crate::style::pick_list::{Appearance, StyleSheet}; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer = crate::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + on_selected: Box<dyn Fn(T) -> Message + 'a>, + options: Cow<'a, [T]>, + placeholder: Option<String>, + selected: Option<T>, + width: Length, + padding: Padding, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Option<Renderer::Font>, + handle: Handle<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + <Renderer::Theme as menu::StyleSheet>::Style: + From<<Renderer::Theme as StyleSheet>::Style>, +{ + /// The default padding of a [`PickList`]. + pub const DEFAULT_PADDING: Padding = Padding::new(5.0); + + /// Creates a new [`PickList`] with the given list of options, the current + /// selected value, and the message to produce when an option is selected. + pub fn new( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, + ) -> Self { + Self { + on_selected: Box::new(on_selected), + options: options.into(), + placeholder: None, + selected, + width: Length::Shrink, + padding: Self::DEFAULT_PADDING, + text_size: None, + text_line_height: text::LineHeight::default(), + text_shaping: text::Shaping::Basic, + font: None, + handle: Default::default(), + style: Default::default(), + } + } + + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { + self.placeholder = Some(placeholder.into()); + self + } + + /// Sets the width of the [`PickList`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the [`Padding`] of the [`PickList`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`PickList`]. + pub fn text_size(mut self, size: impl Into<Pixels>) -> Self { + self.text_size = Some(size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`PickList`]. + pub fn text_line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the [`text::Shaping`] strategy of the [`PickList`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the font of the [`PickList`]. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the [`Handle`] of the [`PickList`]. + pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self { + self.handle = handle; + self + } + + /// Sets the style of the [`PickList`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + <Renderer::Theme as menu::StyleSheet>::Style: + From<<Renderer::Theme as StyleSheet>::Style>, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + self.text_line_height, + self.text_shaping, + self.font, + self.placeholder.as_deref(), + &self.options, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::<State>(), + ) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let font = self.font.unwrap_or_else(|| renderer.default_font()); + draw( + renderer, + theme, + layout, + cursor, + self.padding, + self.text_size, + self.text_line_height, + self.text_shaping, + font, + self.placeholder.as_deref(), + self.selected.as_ref(), + &self.handle, + &self.style, + || tree.state.downcast_ref::<State>(), + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_mut::<State>(); + + overlay( + layout, + state, + self.padding, + self.text_size, + self.text_shaping, + self.font.unwrap_or_else(|| renderer.default_font()), + &self.options, + &self.on_selected, + self.style.clone(), + ) + } +} + +impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + <Renderer::Theme as menu::StyleSheet>::Style: + From<<Renderer::Theme as StyleSheet>::Style>, +{ + fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { + Self::new(pick_list) + } +} + +/// The local state of a [`PickList`]. +#[derive(Debug)] +pub struct State { + menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, + is_open: bool, + hovered_option: Option<usize>, +} + +impl State { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { + Self { + menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), + is_open: bool::default(), + hovered_option: Option::default(), + } + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +/// The handle to the right side of the [`PickList`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Handle<Font> { + /// Displays an arrow icon (▼). + /// + /// This is the default. + Arrow { + /// Font size of the content. + size: Option<f32>, + }, + /// A custom static handle. + Static(Icon<Font>), + /// A custom dynamic handle. + Dynamic { + /// The [`Icon`] used when [`PickList`] is closed. + closed: Icon<Font>, + /// The [`Icon`] used when [`PickList`] is open. + open: Icon<Font>, + }, + /// No handle will be shown. + None, +} + +impl<Font> Default for Handle<Font> { + fn default() -> Self { + Self::Arrow { size: None } + } +} + +/// The icon of a [`Handle`]. +#[derive(Debug, Clone, PartialEq)] +pub struct Icon<Font> { + /// Font that will be used to display the `code_point`, + pub font: Font, + /// The unicode code point that will be used as the icon. + pub code_point: char, + /// Font size of the content. + pub size: Option<f32>, + /// Line height of the content. + pub line_height: text::LineHeight, + /// The shaping strategy of the icon. + pub shaping: text::Shaping, +} + +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Option<Renderer::Font>, + placeholder: Option<&str>, + options: &[T], +) -> layout::Node +where + Renderer: text::Renderer, + T: ToString, +{ + use std::f32; + + let limits = limits.width(width).height(Length::Shrink).pad(padding); + let text_size = text_size.unwrap_or_else(|| renderer.default_size()); + + let max_width = match width { + Length::Shrink => { + let measure = |label: &str| -> f32 { + let width = renderer.measure_width( + label, + text_size, + font.unwrap_or_else(|| renderer.default_font()), + text_shaping, + ); + + width.round() + }; + + let labels = options.iter().map(ToString::to_string); + + let labels_width = labels + .map(|label| measure(&label)) + .fold(100.0, |candidate, current| current.max(candidate)); + + let placeholder_width = placeholder.map(measure).unwrap_or(100.0); + + labels_width.max(placeholder_width) + } + _ => 0.0, + }; + + let size = { + let intrinsic = Size::new( + max_width + text_size + padding.left, + f32::from(text_line_height.to_absolute(Pixels(text_size))), + ); + + limits.resolve(intrinsic).pad(padding) + }; + + layout::Node::new(size) +} + +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State, +) -> event::Status +where + T: PartialEq + Clone + 'a, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + + if state.is_open { + // Event wasn't processed by overlay, so cursor was clicked either outside it's + // bounds or on the drop-down, either way we close the overlay. + state.is_open = false; + + event::Status::Captured + } else if cursor.is_over(layout.bounds()) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() + && cursor.is_over(layout.bounds()) + && !state.is_open + { + fn find_next<'a, T: PartialEq>( + selected: &'a T, + mut options: impl Iterator<Item = &'a T>, + ) -> Option<&'a T> { + let _ = options.find(|&option| option == selected); + + options.next() + } + + let next_option = if y < 0.0 { + if let Some(selected) = selected { + find_next(selected, options.iter()) + } else { + options.first() + } + } else if y > 0.0 { + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) + } else { + options.last() + } + } else { + None + }; + + if let Some(next_option) = next_option { + shell.publish((on_selected)(next_option.clone())); + } + + event::Status::Captured + } else { + event::Status::Ignored + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + + event::Status::Ignored + } + _ => event::Status::Ignored, + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor: mouse::Cursor, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( + layout: Layout<'_>, + state: &'a mut State, + padding: Padding, + text_size: Option<f32>, + text_shaping: text::Shaping, + font: Renderer::Font, + options: &'a [T], + on_selected: &'a dyn Fn(T) -> Message, + style: <Renderer::Theme as StyleSheet>::Style, +) -> Option<overlay::Element<'a, Message, Renderer>> +where + T: Clone + ToString, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet + + container::StyleSheet, + <Renderer::Theme as menu::StyleSheet>::Style: + From<<Renderer::Theme as StyleSheet>::Style>, +{ + if state.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut state.menu, + options, + &mut state.hovered_option, + |option| { + state.is_open = false; + + (on_selected)(option) + }, + ) + .width(bounds.width) + .padding(padding) + .font(font) + .text_shaping(text_shaping) + .style(style); + + if let Some(text_size) = text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } +} + +/// Draws a [`PickList`]. +pub fn draw<'a, T, Renderer>( + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor: mouse::Cursor, + padding: Padding, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Renderer::Font, + placeholder: Option<&str>, + selected: Option<&T>, + handle: &Handle<Renderer::Font>, + style: &<Renderer::Theme as StyleSheet>::Style, + state: impl FnOnce() -> &'a State, +) where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, + T: ToString + 'a, +{ + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + let is_selected = selected.is_some(); + + let style = if is_mouse_over { + theme.hovered(style) + } else { + theme.active(style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }, + style.background, + ); + + let handle = match handle { + Handle::Arrow { size } => Some(( + Renderer::ICON_FONT, + Renderer::ARROW_DOWN_ICON, + *size, + text::LineHeight::default(), + text::Shaping::Basic, + )), + Handle::Static(Icon { + font, + code_point, + size, + line_height, + shaping, + }) => Some((*font, *code_point, *size, *line_height, *shaping)), + Handle::Dynamic { open, closed } => { + if state().is_open { + Some(( + open.font, + open.code_point, + open.size, + open.line_height, + open.shaping, + )) + } else { + Some(( + closed.font, + closed.code_point, + closed.size, + closed.line_height, + closed.shaping, + )) + } + } + Handle::None => None, + }; + + if let Some((font, code_point, size, line_height, shaping)) = handle { + let size = size.unwrap_or_else(|| renderer.default_size()); + + renderer.fill_text(Text { + content: &code_point.to_string(), + size, + line_height, + font, + color: style.handle_color, + bounds: Rectangle { + x: bounds.x + bounds.width - padding.horizontal(), + y: bounds.center_y(), + height: f32::from(line_height.to_absolute(Pixels(size))), + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + shaping, + }); + } + + let label = selected.map(ToString::to_string); + + if let Some(label) = label.as_deref().or(placeholder) { + let text_size = text_size.unwrap_or_else(|| renderer.default_size()); + + renderer.fill_text(Text { + content: label, + size: text_size, + line_height: text_line_height, + font, + color: if is_selected { + style.text_color + } else { + style.placeholder_color + }, + bounds: Rectangle { + x: bounds.x + padding.left, + y: bounds.center_y(), + width: bounds.width - padding.horizontal(), + height: f32::from( + text_line_height.to_absolute(Pixels(text_size)), + ), + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text_shaping, + }); + } +} diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs new file mode 100644 index 00000000..37c6bc72 --- /dev/null +++ b/widget/src/progress_bar.rs @@ -0,0 +1,171 @@ +//! Provide progress feedback to your users. +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{Color, Element, Layout, Length, Rectangle, Size, Widget}; + +use std::ops::RangeInclusive; + +pub use iced_style::progress_bar::{Appearance, StyleSheet}; + +/// A bar that displays progress. +/// +/// # Example +/// ```no_run +/// # type ProgressBar = +/// # iced_widget::ProgressBar<iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// let value = 50.0; +/// +/// ProgressBar::new(0.0..=100.0, value); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct ProgressBar<Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + range: RangeInclusive<f32>, + value: f32, + width: Length, + height: Option<Length>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<Renderer> ProgressBar<Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default height of a [`ProgressBar`]. + pub const DEFAULT_HEIGHT: f32 = 30.0; + + /// Creates a new [`ProgressBar`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`ProgressBar`] + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { + ProgressBar { + value: value.clamp(*range.start(), *range.end()), + range, + width: Length::Fill, + height: None, + style: Default::default(), + } + } + + /// Sets the width of the [`ProgressBar`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`ProgressBar`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = Some(height.into()); + self + } + + /// Sets the style of the [`ProgressBar`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)) + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .width(self.width) + .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let (range_start, range_end) = self.range.clone().into_inner(); + + let active_progress_width = if range_start >= range_end { + 0.0 + } else { + bounds.width * (self.value - range_start) + / (range_end - range_start) + }; + + let style = theme.appearance(&self.style); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { ..bounds }, + border_radius: style.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.background, + ); + + if active_progress_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + border_radius: style.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.bar, + ); + } + } +} + +impl<'a, Message, Renderer> From<ProgressBar<Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + progress_bar: ProgressBar<Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(progress_bar) + } +} diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs new file mode 100644 index 00000000..51a541fd --- /dev/null +++ b/widget/src/qr_code.rs @@ -0,0 +1,299 @@ +//! Encode and display information in a QR code. +use crate::canvas; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer::{self, Renderer as _}; +use crate::core::widget::Tree; +use crate::core::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, +}; +use crate::graphics::geometry::Renderer as _; +use crate::Renderer; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer<Theme>, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new(side_length, side_length)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer<Theme>, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = + self.state.cache.draw(renderer, bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new( + QUIET_ZONE as f32, + QUIET_ZONE as f32, + )); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + let translation = Vector::new(bounds.x, bounds.y); + + renderer.with_translation(translation, |renderer| { + renderer.draw(vec![geometry]); + }); + } +} + +impl<'a, Message, Theme> From<QRCode<'a>> + for Element<'a, Message, Renderer<Theme>> +{ + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec<qrcode::Color>, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result<Self, Error> { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result<Self, Error> { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From<Version> for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From<ErrorCorrection> for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From<qrcode::types::QrError> for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} diff --git a/widget/src/radio.rs b/widget/src/radio.rs new file mode 100644 index 00000000..5b883147 --- /dev/null +++ b/widget/src/radio.rs @@ -0,0 +1,354 @@ +//! Create choices using radio buttons. +use crate::core::alignment; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text; +use crate::core::touch; +use crate::core::widget::Tree; +use crate::core::{ + Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, + Shell, Widget, +}; +use crate::{Row, Text}; + +pub use iced_style::radio::{Appearance, StyleSheet}; + +/// A circular button representing a choice. +/// +/// # Example +/// ```no_run +/// # type Radio<Message> = +/// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// # use iced_widget::column; +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// pub enum Choice { +/// A, +/// B, +/// C, +/// All, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// pub enum Message { +/// RadioSelected(Choice), +/// } +/// +/// let selected_choice = Some(Choice::A); +/// +/// let a = Radio::new( +/// "A", +/// Choice::A, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let b = Radio::new( +/// "B", +/// Choice::B, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let c = Radio::new( +/// "C", +/// Choice::C, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let all = Radio::new( +/// "All of the above", +/// Choice::All, +/// selected_choice, +/// Message::RadioSelected +/// ); +/// +/// let content = column![a, b, c, all]; +/// ``` +#[allow(missing_debug_implementations)] +pub struct Radio<Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + is_selected: bool, + on_click: Message, + label: String, + width: Length, + size: f32, + spacing: f32, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_shaping: text::Shaping, + font: Option<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<Message, Renderer> Radio<Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default size of a [`Radio`] button. + pub const DEFAULT_SIZE: f32 = 28.0; + + /// The default spacing of a [`Radio`] button. + pub const DEFAULT_SPACING: f32 = 15.0; + + /// Creates a new [`Radio`] button. + /// + /// It expects: + /// * the value related to the [`Radio`] button + /// * the label of the [`Radio`] button + /// * the current selected value + /// * a function that will be called when the [`Radio`] is selected. It + /// receives the value of the radio and must produce a `Message`. + pub fn new<F, V>( + label: impl Into<String>, + value: V, + selected: Option<V>, + f: F, + ) -> Self + where + V: Eq + Copy, + F: FnOnce(V) -> Message, + { + Radio { + is_selected: Some(value) == selected, + on_click: f(value), + label: label.into(), + width: Length::Shrink, + size: Self::DEFAULT_SIZE, + spacing: Self::DEFAULT_SPACING, //15 + text_size: None, + text_line_height: text::LineHeight::default(), + text_shaping: text::Shaping::Basic, + font: None, + style: Default::default(), + } + } + + /// Sets the size of the [`Radio`] button. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = size.into().0; + self + } + + /// Sets the width of the [`Radio`] button. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the spacing between the [`Radio`] button and the text. + pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { + self.spacing = spacing.into().0; + self + } + + /// Sets the text size of the [`Radio`] button. + pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Radio`] button. + pub fn text_line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Radio`] button. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the text font of the [`Radio`] button. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the style of the [`Radio`] button. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + Row::<(), Renderer>::new() + .width(self.width) + .spacing(self.spacing) + .align_items(Alignment::Center) + .push(Row::new().width(self.size).height(self.size)) + .push( + Text::new(&self.label) + .width(self.width) + .size( + self.text_size + .unwrap_or_else(|| renderer.default_size()), + ) + .line_height(self.text_line_height) + .shaping(self.text_shaping), + ) + .layout(renderer, limits) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if cursor.is_over(layout.bounds()) { + shell.publish(self.on_click.clone()); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let is_mouse_over = cursor.is_over(layout.bounds()); + + let mut children = layout.children(); + + let custom_style = if is_mouse_over { + theme.hovered(&self.style, self.is_selected) + } else { + theme.active(&self.style, self.is_selected) + }; + + { + let layout = children.next().unwrap(); + let bounds = layout.bounds(); + + let size = bounds.width; + let dot_size = size / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: (size / 2.0).into(), + border_width: custom_style.border_width, + border_color: custom_style.border_color, + }, + custom_style.background, + ); + + if self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + dot_size / 2.0, + y: bounds.y + dot_size / 2.0, + width: bounds.width - dot_size, + height: bounds.height - dot_size, + }, + border_radius: (dot_size / 2.0).into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + custom_style.dot_color, + ); + } + } + + { + let label_layout = children.next().unwrap(); + + crate::text::draw( + renderer, + style, + label_layout, + &self.label, + self.text_size, + self.text_line_height, + self.font, + crate::text::Appearance { + color: custom_style.text_color, + }, + alignment::Horizontal::Left, + alignment::Vertical::Center, + self.text_shaping, + ); + } + } +} + +impl<'a, Message, Renderer> From<Radio<Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { + Element::new(radio) + } +} diff --git a/widget/src/row.rs b/widget/src/row.rs new file mode 100644 index 00000000..1db22416 --- /dev/null +++ b/widget/src/row.rs @@ -0,0 +1,243 @@ +//! Distribute content horizontally. +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{Operation, Tree}; +use crate::core::{ + Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, + Widget, +}; + +/// A container that distributes its contents horizontally. +#[allow(missing_debug_implementations)] +pub struct Row<'a, Message, Renderer = crate::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + align_items: Alignment, + children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Row<'a, Message, Renderer> { + /// Creates an empty [`Row`]. + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + /// Creates a [`Row`] with the given elements. + pub fn with_children( + children: Vec<Element<'a, Message, Renderer>>, + ) -> Self { + Row { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + align_items: Alignment::Start, + children, + } + } + + /// Sets the horizontal spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Row`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Row`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the vertical alignment of the contents of the [`Row`] . + pub fn align_items(mut self, align: Alignment) -> Self { + self.align_items = align; + self + } + + /// Adds an [`Element`] to the [`Row`]. + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.children.push(child.into()); + self + } +} + +impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Row<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + layout::flex::resolve( + layout::flex::Axis::Horizontal, + renderer, + &limits, + self.padding, + self.spacing, + self.align_items, + &self.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }) + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } +} + +impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(row: Row<'a, Message, Renderer>) -> Self { + Self::new(row) + } +} diff --git a/widget/src/rule.rs b/widget/src/rule.rs new file mode 100644 index 00000000..d703e6ae --- /dev/null +++ b/widget/src/rule.rs @@ -0,0 +1,148 @@ +//! Display a horizontal or vertical rule for dividing content. +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{ + Color, Element, Layout, Length, Pixels, Rectangle, Size, Widget, +}; + +pub use crate::style::rule::{Appearance, FillMode, StyleSheet}; + +/// Display a horizontal or vertical rule for dividing content. +#[allow(missing_debug_implementations)] +pub struct Rule<Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + width: Length, + height: Length, + is_horizontal: bool, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<Renderer> Rule<Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a horizontal [`Rule`] with the given height. + pub fn horizontal(height: impl Into<Pixels>) -> Self { + Rule { + width: Length::Fill, + height: Length::Fixed(height.into().0), + is_horizontal: true, + style: Default::default(), + } + } + + /// Creates a vertical [`Rule`] with the given width. + pub fn vertical(width: impl Into<Pixels>) -> Self { + Rule { + width: Length::Fixed(width.into().0), + height: Length::Fill, + is_horizontal: false, + style: Default::default(), + } + } + + /// Sets the style of the [`Rule`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + layout::Node::new(limits.resolve(Size::ZERO)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let style = theme.appearance(&self.style); + + let bounds = if self.is_horizontal { + let line_y = (bounds.y + (bounds.height / 2.0) + - (style.width as f32 / 2.0)) + .round(); + + let (offset, line_width) = style.fill_mode.fill(bounds.width); + let line_x = bounds.x + offset; + + Rectangle { + x: line_x, + y: line_y, + width: line_width, + height: style.width as f32, + } + } else { + let line_x = (bounds.x + (bounds.width / 2.0) + - (style.width as f32 / 2.0)) + .round(); + + let (offset, line_height) = style.fill_mode.fill(bounds.height); + let line_y = bounds.y + offset; + + Rectangle { + x: line_x, + y: line_y, + width: style.width as f32, + height: line_height, + } + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: style.radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.color, + ); + } +} + +impl<'a, Message, Renderer> From<Rule<Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> { + Element::new(rule) + } +} diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs new file mode 100644 index 00000000..88746ac4 --- /dev/null +++ b/widget/src/scrollable.rs @@ -0,0 +1,1514 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget; +use crate::core::widget::operation::{self, Operation}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Vector, Widget, +}; +use crate::runtime::Command; + +pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; +pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; + +/// A widget that can vertically display an infinite amount of content with a +/// scrollbar. +#[allow(missing_debug_implementations)] +pub struct Scrollable<'a, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Option<Id>, + width: Length, + height: Length, + direction: Direction, + content: Element<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + Scrollable { + id: None, + width: Length::Shrink, + height: Length::Shrink, + direction: Default::default(), + content: content.into(), + on_scroll: None, + style: Default::default(), + } + } + + /// Sets the [`Id`] of the [`Scrollable`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Sets the width of the [`Scrollable`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Scrollable`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`Direction`] of the [`Scrollable`] . + pub fn direction(mut self, direction: Direction) -> Self { + self.direction = direction; + self + } + + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the [`Viewport`] of the [`Scrollable`] + pub fn on_scroll(mut self, f: impl Fn(Viewport) -> Message + 'a) -> Self { + self.on_scroll = Some(Box::new(f)); + self + } + + /// Sets the style of the [`Scrollable`] . + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// The direction of [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Direction { + /// Vertical scrolling + Vertical(Properties), + /// Horizontal scrolling + Horizontal(Properties), + /// Both vertical and horizontal scrolling + Both { + /// The properties of the vertical scrollbar. + vertical: Properties, + /// The properties of the horizontal scrollbar. + horizontal: Properties, + }, +} + +impl Direction { + /// Returns the [`Properties`] of the horizontal scrollbar, if any. + pub fn horizontal(&self) -> Option<&Properties> { + match self { + Self::Horizontal(properties) => Some(properties), + Self::Both { horizontal, .. } => Some(horizontal), + _ => None, + } + } + + /// Returns the [`Properties`] of the vertical scrollbar, if any. + pub fn vertical(&self) -> Option<&Properties> { + match self { + Self::Vertical(properties) => Some(properties), + Self::Both { vertical, .. } => Some(vertical), + _ => None, + } + } +} + +impl Default for Direction { + fn default() -> Self { + Self::Vertical(Properties::default()) + } +} + +/// Properties of a scrollbar within a [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Properties { + width: f32, + margin: f32, + scroller_width: f32, + alignment: Alignment, +} + +impl Default for Properties { + fn default() -> Self { + Self { + width: 10.0, + margin: 0.0, + scroller_width: 10.0, + alignment: Alignment::Start, + } + } +} + +impl Properties { + /// Creates new [`Properties`] for use in a [`Scrollable`]. + pub fn new() -> Self { + Self::default() + } + + /// Sets the scrollbar width of the [`Scrollable`] . + pub fn width(mut self, width: impl Into<Pixels>) -> Self { + self.width = width.into().0.max(0.0); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn margin(mut self, margin: impl Into<Pixels>) -> Self { + self.margin = margin.into().0; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { + self.scroller_width = scroller_width.into().0.max(0.0); + self + } + + /// Sets the alignment of the [`Scrollable`] . + pub fn alignment(mut self, alignment: Alignment) -> Self { + self.alignment = alignment; + self + } +} + +/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum Alignment { + /// Content is aligned to the start of the [`Viewport`]. + #[default] + Start, + /// Content is aligned to the end of the [`Viewport`] + End, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + &self.direction, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + + operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); + + operation.container( + self.id.as_ref().map(|id| &id.0), + &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + tree.state.downcast_mut::<State>(), + event, + layout, + cursor, + clipboard, + shell, + self.direction, + &self.on_scroll, + |event, layout, cursor, clipboard, shell| { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::<State>(), + renderer, + theme, + layout, + cursor, + self.direction, + &self.style, + |renderer, layout, cursor, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref::<State>(), + layout, + cursor, + self.direction, + |layout, cursor, viewport| { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let translation = tree + .state + .downcast_ref::<State>() + .translation(self.direction, bounds, content_bounds); + + overlay.translate(Vector::new(-translation.x, -translation.y)) + }) + } +} + +impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: Scrollable<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`Scrollable`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +impl From<Id> for widget::Id { + fn from(id: Id) -> Self { + id.0 + } +} + +/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided `percentage` along the x & y axis. +pub fn snap_to<Message: 'static>( + id: Id, + offset: RelativeOffset, +) -> Command<Message> { + Command::widget(operation::scrollable::snap_to(id.0, offset)) +} + +/// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`] +/// to the provided [`AbsoluteOffset`] along the x & y axis. +pub fn scroll_to<Message: 'static>( + id: Id, + offset: AbsoluteOffset, +) -> Command<Message> { + Command::widget(operation::scrollable::scroll_to(id.0, offset)) +} + +/// Computes the layout of a [`Scrollable`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + direction: &Direction, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); + + let content = layout_content(renderer, &child_limits); + let size = limits.resolve(content.size()); + + layout::Node::with_children(size, vec![content]) +} + +/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] +/// accordingly. +pub fn update<Message>( + state: &mut State, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + direction: Direction, + on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, + update_content: impl FnOnce( + Event, + Layout<'_>, + mouse::Cursor, + &mut dyn Clipboard, + &mut Shell<'_, Message>, + ) -> event::Status, +) -> event::Status { + let bounds = layout.bounds(); + let cursor_over_scrollable = cursor.position_over(bounds); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + let event_status = { + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + + state.translation(direction, bounds, content_bounds), + ) + } + _ => mouse::Cursor::Unavailable, + }; + + update_content(event.clone(), content, cursor, clipboard, shell) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event + { + state.keyboard_modifiers = modifiers; + + return event::Status::Ignored; + } + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return event::Status::Ignored; + } + + let delta = match delta { + mouse::ScrollDelta::Lines { x, y } => { + // TODO: Configurable speed/friction (?) + let movement = if state.keyboard_modifiers.shift() { + Vector::new(y, x) + } else { + Vector::new(x, y) + }; + + movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; + + state.scroll(delta, direction, bounds, content_bounds); + + notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); + + return event::Status::Captured; + } + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => + { + match event { + touch::Event::FingerPressed { .. } => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + state.scroll_area_touched_at = Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); + + state.scroll(delta, direction, bounds, content_bounds); + + state.scroll_area_touched_at = Some(cursor_position); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_area_touched_at = None; + } + } + + return event::Status::Captured; + } + _ => {} + } + + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.y_scroller_grabbed_at = None; + + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = + (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) + { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.x_scroller_grabbed_at = None; + + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = + (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) + { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } + + event::Status::Ignored +} + +/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. +pub fn mouse_interaction( + state: &State, + layout: Layout<'_>, + cursor: mouse::Cursor, + direction: Direction, + content_interaction: impl FnOnce( + Layout<'_>, + mouse::Cursor, + &Rectangle, + ) -> mouse::Interaction, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor_over_scrollable = cursor.position_over(bounds); + + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) + || state.scrollers_grabbed() + { + mouse::Interaction::Idle + } else { + let translation = state.translation(direction, bounds, content_bounds); + + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + translation) + } + _ => mouse::Cursor::Unavailable, + }; + + content_interaction( + content_layout, + cursor, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ) + } +} + +/// Draws a [`Scrollable`]. +pub fn draw<Renderer>( + state: &State, + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor: mouse::Cursor, + direction: Direction, + style: &<Renderer::Theme as StyleSheet>::Style, + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), +) where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); + + let cursor_over_scrollable = cursor.position_over(bounds); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + let translation = state.translation(direction, bounds, content_bounds); + + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + translation) + } + _ => mouse::Cursor::Unavailable, + }; + + // Draw inner content + if scrollbars.active() { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(-translation.x, -translation.y), + |renderer| { + draw_content( + renderer, + content_layout, + cursor, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ); + }, + ); + }); + + let draw_scrollbar = + |renderer: &mut Renderer, + style: Scrollbar, + scrollbar: &internals::Scrollbar| { + //track + if scrollbar.bounds.width > 0.0 + && scrollbar.bounds.height > 0.0 + && (style.background.is_some() + || (style.border_color != Color::TRANSPARENT + && style.border_width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + //thumb + if scrollbar.scroller.bounds.width > 0.0 + && scrollbar.scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border_color != Color::TRANSPARENT + && style.scroller.border_width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); + } + }; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + //draw y scrollbar + if let Some(scrollbar) = scrollbars.y { + let style = if state.y_scroller_grabbed_at.is_some() { + theme.dragging(style) + } else if cursor_over_scrollable.is_some() { + theme.hovered(style, mouse_over_y_scrollbar) + } else { + theme.active(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } + + //draw x scrollbar + if let Some(scrollbar) = scrollbars.x { + let style = if state.x_scroller_grabbed_at.is_some() { + theme.dragging_horizontal(style) + } else if cursor_over_scrollable.is_some() { + theme.hovered_horizontal(style, mouse_over_x_scrollbar) + } else { + theme.active_horizontal(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } + }, + ); + } else { + draw_content( + renderer, + content_layout, + cursor, + &Rectangle { + x: bounds.x + translation.x, + y: bounds.y + translation.y, + ..bounds + }, + ); + } +} + +fn notify_on_scroll<Message>( + state: &mut State, + on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) { + if let Some(on_scroll) = on_scroll { + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return; + } + + let viewport = Viewport { + offset_x: state.offset_x, + offset_y: state.offset_y, + bounds, + content_bounds, + }; + + // Don't publish redundant viewports to shell + if let Some(last_notified) = state.last_notified { + let last_relative_offset = last_notified.relative_offset(); + let current_relative_offset = viewport.relative_offset(); + + let last_absolute_offset = last_notified.absolute_offset(); + let current_absolute_offset = viewport.absolute_offset(); + + let unchanged = |a: f32, b: f32| { + (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) + }; + + if unchanged(last_relative_offset.x, current_relative_offset.x) + && unchanged(last_relative_offset.y, current_relative_offset.y) + && unchanged(last_absolute_offset.x, current_absolute_offset.x) + && unchanged(last_absolute_offset.y, current_absolute_offset.y) + { + return; + } + } + + shell.publish(on_scroll(viewport)); + state.last_notified = Some(viewport); + } +} + +/// The local state of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +pub struct State { + scroll_area_touched_at: Option<Point>, + offset_y: Offset, + y_scroller_grabbed_at: Option<f32>, + offset_x: Offset, + x_scroller_grabbed_at: Option<f32>, + keyboard_modifiers: keyboard::Modifiers, + last_notified: Option<Viewport>, +} + +impl Default for State { + fn default() -> Self { + Self { + scroll_area_touched_at: None, + offset_y: Offset::Absolute(0.0), + y_scroller_grabbed_at: None, + offset_x: Offset::Absolute(0.0), + x_scroller_grabbed_at: None, + keyboard_modifiers: keyboard::Modifiers::default(), + last_notified: None, + } + } +} + +impl operation::Scrollable for State { + fn snap_to(&mut self, offset: RelativeOffset) { + State::snap_to(self, offset); + } + + fn scroll_to(&mut self, offset: AbsoluteOffset) { + State::scroll_to(self, offset) + } +} + +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, viewport: f32, content: f32) -> f32 { + match self { + Offset::Absolute(absolute) => { + absolute.min((content - viewport).max(0.0)) + } + Offset::Relative(percentage) => { + ((content - viewport) * percentage).max(0.0) + } + } + } + + fn translation( + self, + viewport: f32, + content: f32, + alignment: Alignment, + ) -> f32 { + let offset = self.absolute(viewport, content); + + match alignment { + Alignment::Start => offset, + Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + } + } +} + +/// The current [`Viewport`] of the [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +pub struct Viewport { + offset_x: Offset, + offset_y: Offset, + bounds: Rectangle, + content_bounds: Rectangle, +} + +impl Viewport { + /// Returns the [`AbsoluteOffset`] of the current [`Viewport`]. + pub fn absolute_offset(&self) -> AbsoluteOffset { + let x = self + .offset_x + .absolute(self.bounds.width, self.content_bounds.width); + let y = self + .offset_y + .absolute(self.bounds.height, self.content_bounds.height); + + AbsoluteOffset { x, y } + } + + /// Returns the [`RelativeOffset`] of the current [`Viewport`]. + pub fn relative_offset(&self) -> RelativeOffset { + let AbsoluteOffset { x, y } = self.absolute_offset(); + + let x = x / (self.content_bounds.width - self.bounds.width); + let y = y / (self.content_bounds.height - self.bounds.height); + + RelativeOffset { x, y } + } +} + +impl State { + /// Creates a new [`State`] with the scrollbar(s) at the beginning. + pub fn new() -> Self { + State::default() + } + + /// Apply a scrolling offset to the current [`State`], given the bounds of + /// the [`Scrollable`] and its contents. + pub fn scroll( + &mut self, + delta: Vector<f32>, + direction: Direction, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + let horizontal_alignment = direction + .horizontal() + .map(|p| p.alignment) + .unwrap_or_default(); + + let vertical_alignment = direction + .vertical() + .map(|p| p.alignment) + .unwrap_or_default(); + + let align = |alignment: Alignment, delta: f32| match alignment { + Alignment::Start => delta, + Alignment::End => -delta, + }; + + let delta = Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ); + + if bounds.height < content_bounds.height { + self.offset_y = Offset::Absolute( + (self.offset_y.absolute(bounds.height, content_bounds.height) + - delta.y) + .clamp(0.0, content_bounds.height - bounds.height), + ) + } + + if bounds.width < content_bounds.width { + self.offset_x = Offset::Absolute( + (self.offset_x.absolute(bounds.width, content_bounds.width) + - delta.x) + .clamp(0.0, content_bounds.width - bounds.width), + ); + } + } + + /// Scrolls the [`Scrollable`] to a relative amount along the y axis. + /// + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_y_to( + &mut self, + percentage: f32, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)); + self.unsnap(bounds, content_bounds); + } + + /// Scrolls the [`Scrollable`] to a relative amount along the x axis. + /// + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_x_to( + &mut self, + percentage: f32, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)); + self.unsnap(bounds, content_bounds); + } + + /// Snaps the scroll position to a [`RelativeOffset`]. + pub fn snap_to(&mut self, offset: RelativeOffset) { + self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0)); + self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0)); + } + + /// Scroll to the provided [`AbsoluteOffset`]. + pub fn scroll_to(&mut self, offset: AbsoluteOffset) { + self.offset_x = Offset::Absolute(offset.x.max(0.0)); + self.offset_y = Offset::Absolute(offset.y.max(0.0)); + } + + /// Unsnaps the current scroll position, if snapped, given the bounds of the + /// [`Scrollable`] and its contents. + pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { + self.offset_x = Offset::Absolute( + self.offset_x.absolute(bounds.width, content_bounds.width), + ); + self.offset_y = Offset::Absolute( + self.offset_y.absolute(bounds.height, content_bounds.height), + ); + } + + /// Returns the scrolling translation of the [`State`], given a [`Direction`], + /// the bounds of the [`Scrollable`] and its contents. + fn translation( + &self, + direction: Direction, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Vector { + Vector::new( + if let Some(horizontal) = direction.horizontal() { + self.offset_x.translation( + bounds.width, + content_bounds.width, + horizontal.alignment, + ) + } else { + 0.0 + }, + if let Some(vertical) = direction.vertical() { + self.offset_y.translation( + bounds.height, + content_bounds.height, + vertical.alignment, + ) + } else { + 0.0 + }, + ) + } + + /// Returns whether any scroller is currently grabbed or not. + pub fn scrollers_grabbed(&self) -> bool { + self.x_scroller_grabbed_at.is_some() + || self.y_scroller_grabbed_at.is_some() + } +} + +#[derive(Debug)] +/// State of both [`Scrollbar`]s. +struct Scrollbars { + y: Option<internals::Scrollbar>, + x: Option<internals::Scrollbar>, +} + +impl Scrollbars { + /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. + fn new( + state: &State, + direction: Direction, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Self { + let translation = state.translation(direction, bounds, content_bounds); + + let show_scrollbar_x = direction + .horizontal() + .filter(|_| content_bounds.width > bounds.width); + + let show_scrollbar_y = direction + .vertical() + .filter(|_| content_bounds.height > bounds.height); + + let y_scrollbar = if let Some(vertical) = show_scrollbar_y { + let Properties { + width, + margin, + scroller_width, + .. + } = *vertical; + + // Adjust the height of the vertical scrollbar if the horizontal scrollbar + // is present + let x_scrollbar_height = show_scrollbar_x + .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin); + + let total_scrollbar_width = + width.max(scroller_width) + 2.0 * margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width - total_scrollbar_width, + y: bounds.y, + width: total_scrollbar_width, + height: (bounds.height - x_scrollbar_height).max(0.0), + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - total_scrollbar_width / 2.0 + - width / 2.0, + y: bounds.y, + width, + height: (bounds.height - x_scrollbar_height).max(0.0), + }; + + let ratio = bounds.height / content_bounds.height; + // min height for easier grabbing with super tall content + let scroller_height = (bounds.height * ratio).max(2.0); + let scroller_offset = translation.y * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - total_scrollbar_width / 2.0 + - scroller_width / 2.0, + y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height) + .max(0.0), + width: scroller_width, + height: scroller_height, + }; + + Some(internals::Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: internals::Scroller { + bounds: scroller_bounds, + }, + alignment: vertical.alignment, + }) + } else { + None + }; + + let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { + let Properties { + width, + margin, + scroller_width, + .. + } = *horizontal; + + // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar + // is present + let scrollbar_y_width = show_scrollbar_y + .map_or(0.0, |v| v.width.max(v.scroller_width) + v.margin); + + let total_scrollbar_height = + width.max(scroller_width) + 2.0 * margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height - total_scrollbar_height, + width: (bounds.width - scrollbar_y_width).max(0.0), + height: total_scrollbar_height, + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height + - total_scrollbar_height / 2.0 + - width / 2.0, + width: (bounds.width - scrollbar_y_width).max(0.0), + height: width, + }; + + let ratio = bounds.width / content_bounds.width; + // min width for easier grabbing with extra wide content + let scroller_length = (bounds.width * ratio).max(2.0); + let scroller_offset = translation.x * ratio; + + let scroller_bounds = Rectangle { + x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) + .max(0.0), + y: bounds.y + bounds.height + - total_scrollbar_height / 2.0 + - scroller_width / 2.0, + width: scroller_length, + height: scroller_width, + }; + + Some(internals::Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: internals::Scroller { + bounds: scroller_bounds, + }, + alignment: horizontal.alignment, + }) + } else { + None + }; + + Self { + y: y_scrollbar, + x: x_scrollbar, + } + } + + fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) { + if let Some(cursor_position) = cursor.position() { + ( + self.y + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false), + self.x + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false), + ) + } else { + (false, false) + } + } + + fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> { + self.y.and_then(|scrollbar| { + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scrollbar.scroller.bounds.contains(cursor_position) { + (cursor_position.y - scrollbar.scroller.bounds.y) + / scrollbar.scroller.bounds.height + } else { + 0.5 + }) + } else { + None + } + }) + } + + fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> { + self.x.and_then(|scrollbar| { + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scrollbar.scroller.bounds.contains(cursor_position) { + (cursor_position.x - scrollbar.scroller.bounds.x) + / scrollbar.scroller.bounds.width + } else { + 0.5 + }) + } else { + None + } + }) + } + + fn active(&self) -> bool { + self.y.is_some() || self.x.is_some() + } +} + +pub(super) mod internals { + use crate::core::{Point, Rectangle}; + + use super::Alignment; + + #[derive(Debug, Copy, Clone)] + pub struct Scrollbar { + pub total_bounds: Rectangle, + pub bounds: Rectangle, + pub scroller: Scroller, + pub alignment: Alignment, + } + + impl Scrollbar { + /// Returns whether the mouse is over the scrollbar or not. + pub fn is_mouse_over(&self, cursor_position: Point) -> bool { + self.total_bounds.contains(cursor_position) + } + + /// Returns the y-axis scrolled percentage from the cursor position. + pub fn scroll_percentage_y( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + let percentage = (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height); + + match self.alignment { + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, + } + } + + /// Returns the x-axis scrolled percentage from the cursor position. + pub fn scroll_percentage_x( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + let percentage = (cursor_position.x + - self.bounds.x + - self.scroller.bounds.width * grabbed_at) + / (self.bounds.width - self.scroller.bounds.width); + + match self.alignment { + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, + } + } + } + + /// The handle of a [`Scrollbar`]. + #[derive(Debug, Clone, Copy)] + pub struct Scroller { + /// The bounds of the [`Scroller`]. + pub bounds: Rectangle, + } +} diff --git a/widget/src/slider.rs b/widget/src/slider.rs new file mode 100644 index 00000000..3ea4391b --- /dev/null +++ b/widget/src/slider.rs @@ -0,0 +1,470 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, + Size, Widget, +}; + +use std::ops::RangeInclusive; + +pub use iced_style::slider::{ + Appearance, Handle, HandleShape, Rail, StyleSheet, +}; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// A [`Slider`] will try to fill the horizontal space of its container. +/// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ```no_run +/// # type Slider<'a, T, Message> = +/// # iced_widget::Slider<'a, Message, T, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// #[derive(Clone)] +/// pub enum Message { +/// SliderChanged(f32), +/// } +/// +/// let value = 50.0; +/// +/// Slider::new(0.0..=100.0, value, Message::SliderChanged); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct Slider<'a, T, Message, Renderer = crate::Renderer> +where + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message + 'a>, + on_release: Option<Message>, + width: Length, + height: f32, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default height of a [`Slider`]. + pub const DEFAULT_HEIGHT: f32 = 22.0; + + /// Creates a new [`Slider`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`Slider`] + /// * a function that will be called when the [`Slider`] is dragged. + /// It receives the new value of the [`Slider`] and must produce a + /// `Message`. + pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + + Slider { + value, + range, + step: T::from(1), + on_change: Box::new(on_change), + on_release: None, + width: Length::Fill, + height: Self::DEFAULT_HEIGHT, + style: Default::default(), + } + } + + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. + /// + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`Slider`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Slider`]. + pub fn height(mut self, height: impl Into<Pixels>) -> Self { + self.height = height.into().0; + self + } + + /// Sets the style of the [`Slider`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, + Renderer: crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor, + shell, + tree.state.downcast_mut::<State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + &self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) + } +} + +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + crate::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + slider: Slider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} + +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update<Message, T>( + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive<T>, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option<Message>, +) -> event::Status +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, +{ + let is_dragging = state.is_dragging; + + let mut change = |cursor_position: Point| { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + *range.start() + } else if cursor_position.x >= bounds.x + bounds.width { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = cursor.position_over(layout.bounds()) + { + change(cursor_position); + state.is_dragging = true; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + let _ = cursor.position().map(change); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T, R>( + renderer: &mut R, + layout: Layout<'_>, + cursor: mouse::Cursor, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>, + style: &<R::Theme as StyleSheet>::Style, +) where + T: Into<f64> + Copy, + R: crate::core::Renderer, + R::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = if state.is_dragging { + style_sheet.dragging(style) + } else if is_mouse_over { + style_sheet.hovered(style) + } else { + style_sheet.active(style) + }; + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.height, border_radius), + }; + + let value = value.into() as f32; + let (range_start, range_end) = { + let (start, end) = range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + let rail_y = bounds.y + bounds.height / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset + handle_width / 2.0, + y: rail_y - style.rail.width / 2.0, + width: bounds.width - offset - handle_width / 2.0, + height: style.rail.width, + }, + border_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset, + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`Slider`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor: mouse::Cursor, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } +} + +/// The local state of a [`Slider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_dragging: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } +} diff --git a/widget/src/space.rs b/widget/src/space.rs new file mode 100644 index 00000000..9a5385e8 --- /dev/null +++ b/widget/src/space.rs @@ -0,0 +1,87 @@ +//! Distribute content vertically. +use crate::core; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{Element, Layout, Length, Rectangle, Size, Widget}; + +/// An amount of empty space. +/// +/// It can be useful if you want to fill some space with nothing. +#[derive(Debug)] +pub struct Space { + width: Length, + height: Length, +} + +impl Space { + /// Creates an amount of empty [`Space`] with the given width and height. + pub fn new(width: impl Into<Length>, height: impl Into<Length>) -> Self { + Space { + width: width.into(), + height: height.into(), + } + } + + /// Creates an amount of horizontal [`Space`]. + pub fn with_width(width: impl Into<Length>) -> Self { + Space { + width: width.into(), + height: Length::Shrink, + } + } + + /// Creates an amount of vertical [`Space`]. + pub fn with_height(height: impl Into<Length>) -> Self { + Space { + width: Length::Shrink, + height: height.into(), + } + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Space +where + Renderer: core::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + layout::Node::new(limits.resolve(Size::ZERO)) + } + + fn draw( + &self, + _state: &Tree, + _renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + } +} + +impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer> +where + Renderer: core::Renderer, + Message: 'a, +{ + fn from(space: Space) -> Element<'a, Message, Renderer> { + Element::new(space) + } +} diff --git a/widget/src/svg.rs b/widget/src/svg.rs new file mode 100644 index 00000000..1ccc5d62 --- /dev/null +++ b/widget/src/svg.rs @@ -0,0 +1,196 @@ +//! Display vector graphics in your application. +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::svg; +use crate::core::widget::Tree; +use crate::core::{ + ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, +}; + +use std::path::PathBuf; + +pub use crate::style::svg::{Appearance, StyleSheet}; +pub use svg::Handle; + +/// A vector graphics image. +/// +/// An [`Svg`] image resizes smoothly without losing any quality. +/// +/// [`Svg`] images can have a considerable rendering cost when resized, +/// specially when they are complex. +#[allow(missing_debug_implementations)] +pub struct Svg<Renderer = crate::Renderer> +where + Renderer: svg::Renderer, + Renderer::Theme: StyleSheet, +{ + handle: Handle, + width: Length, + height: Length, + content_fit: ContentFit, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<Renderer> Svg<Renderer> +where + Renderer: svg::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Svg`] from the given [`Handle`]. + pub fn new(handle: impl Into<Handle>) -> Self { + Svg { + handle: handle.into(), + width: Length::Fill, + height: Length::Shrink, + content_fit: ContentFit::Contain, + style: Default::default(), + } + } + + /// Creates a new [`Svg`] that will display the contents of the file at the + /// provided path. + #[must_use] + pub fn from_path(path: impl Into<PathBuf>) -> Self { + Self::new(Handle::from_path(path)) + } + + /// Sets the width of the [`Svg`]. + #[must_use] + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Svg`]. + #[must_use] + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`ContentFit`] of the [`Svg`]. + /// + /// Defaults to [`ContentFit::Contain`] + #[must_use] + pub fn content_fit(self, content_fit: ContentFit) -> Self { + Self { + content_fit, + ..self + } + } + + /// Sets the style variant of this [`Svg`]. + #[must_use] + pub fn style( + mut self, + style: <Renderer::Theme as StyleSheet>::Style, + ) -> Self { + self.style = style; + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Svg<Renderer> +where + Renderer: svg::Renderer, + Renderer::Theme: iced_style::svg::StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + // The raw w/h of the underlying image + let Size { width, height } = renderer.dimensions(&self.handle); + let image_size = Size::new(width as f32, height as f32); + + // The size to be available to the widget prior to `Shrink`ing + let raw_size = limits + .width(self.width) + .height(self.height) + .resolve(image_size); + + // The uncropped size of the image when fit to the bounds above + let full_size = self.content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let Size { width, height } = renderer.dimensions(&self.handle); + let image_size = Size::new(width as f32, height as f32); + + let bounds = layout.bounds(); + let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + + let render = |renderer: &mut Renderer| { + let offset = Vector::new( + (bounds.width - adjusted_fit.width).max(0.0) / 2.0, + (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + ); + + let drawing_bounds = Rectangle { + width: adjusted_fit.width, + height: adjusted_fit.height, + ..bounds + }; + + let appearance = theme.appearance(&self.style); + + renderer.draw( + self.handle.clone(), + appearance.color, + drawing_bounds + offset, + ); + }; + + if adjusted_fit.width > bounds.width + || adjusted_fit.height > bounds.height + { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } + } +} + +impl<'a, Message, Renderer> From<Svg<Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: svg::Renderer + 'a, + Renderer::Theme: iced_style::svg::StyleSheet, +{ + fn from(icon: Svg<Renderer>) -> Element<'a, Message, Renderer> { + Element::new(icon) + } +} diff --git a/widget/src/text.rs b/widget/src/text.rs new file mode 100644 index 00000000..ce4f44bd --- /dev/null +++ b/widget/src/text.rs @@ -0,0 +1,6 @@ +//! Draw and interact with text. +pub use crate::core::widget::text::*; + +/// A paragraph. +pub type Text<'a, Renderer = crate::Renderer> = + crate::core::widget::Text<'a, Renderer>; diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs new file mode 100644 index 00000000..03bcb86a --- /dev/null +++ b/widget/src/text_input.rs @@ -0,0 +1,1405 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +mod editor; +mod value; + +pub mod cursor; + +pub use cursor::Cursor; +pub use value::Value; + +use editor::Editor; + +use crate::core::alignment; +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout; +use crate::core::mouse::{self, click}; +use crate::core::renderer; +use crate::core::text::{self, Text}; +use crate::core::time::{Duration, Instant}; +use crate::core::touch; +use crate::core::widget; +use crate::core::widget::operation::{self, Operation}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; +use crate::core::{ + Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, Vector, Widget, +}; +use crate::runtime::Command; + +pub use iced_style::text_input::{Appearance, StyleSheet}; + +/// A field that can be filled with text. +/// +/// # Example +/// ```no_run +/// # pub type TextInput<'a, Message> = +/// # iced_widget::TextInput<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// #[derive(Debug, Clone)] +/// enum Message { +/// TextInputChanged(String), +/// } +/// +/// let value = "Some text"; +/// +/// let input = TextInput::new( +/// "This is the placeholder...", +/// value, +/// ) +/// .on_input(Message::TextInputChanged) +/// .padding(10); +/// ``` +///  +#[allow(missing_debug_implementations)] +pub struct TextInput<'a, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Option<Id>, + placeholder: String, + value: Value, + is_secure: bool, + font: Option<Renderer::Font>, + width: Length, + padding: Padding, + size: Option<f32>, + line_height: text::LineHeight, + on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, + on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, + on_submit: Option<Message>, + icon: Option<Icon<Renderer::Font>>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`TextInput`]. + /// + /// It expects: + /// - a placeholder, + /// - the current value + pub fn new(placeholder: &str, value: &str) -> Self { + TextInput { + id: None, + placeholder: String::from(placeholder), + value: Value::new(value), + is_secure: false, + font: None, + width: Length::Fill, + padding: Padding::new(5.0), + size: None, + line_height: text::LineHeight::default(), + on_input: None, + on_paste: None, + on_submit: None, + icon: None, + style: Default::default(), + } + } + + /// Sets the [`Id`] of the [`TextInput`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// Converts the [`TextInput`] into a secure password input. + pub fn password(mut self) -> Self { + self.is_secure = true; + self + } + + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`]. + /// + /// If this method is not called, the [`TextInput`] will be disabled. + pub fn on_input<F>(mut self, callback: F) -> Self + where + F: 'a + Fn(String) -> Message, + { + self.on_input = Some(Box::new(callback)); + self + } + + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed. + pub fn on_submit(mut self, message: Message) -> Self { + self.on_submit = Some(message); + self + } + + /// Sets the message that should be produced when some text is pasted into + /// the [`TextInput`]. + pub fn on_paste( + mut self, + on_paste: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_paste = Some(Box::new(on_paste)); + self + } + + /// Sets the [`Font`] of the [`TextInput`]. + /// + /// [`Font`]: text::Renderer::Font + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = Some(font); + self + } + + /// Sets the [`Icon`] of the [`TextInput`]. + pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self { + self.icon = Some(icon); + self + } + + /// Sets the width of the [`TextInput`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`TextInput`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into().0); + self + } + + /// Sets the [`LineHeight`] of the [`TextInput`]. + pub fn line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.line_height = line_height.into(); + self + } + + /// Sets the style of the [`TextInput`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + /// + /// [`Renderer`]: text::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor: mouse::Cursor, + value: Option<&Value>, + ) { + draw( + renderer, + theme, + layout, + cursor, + tree.state.downcast_ref::<State>(), + value.unwrap_or(&self.value), + &self.placeholder, + self.size, + self.line_height, + self.font, + self.on_input.is_none(), + self.is_secure, + self.icon.as_ref(), + &self.style, + ) + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn diff(&self, tree: &mut Tree) { + let state = tree.state.downcast_mut::<State>(); + + // Unfocus text input if it becomes disabled + if self.on_input.is_none() { + state.last_click = None; + state.is_focused = None; + state.is_pasting = None; + state.is_dragging = false; + } + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.size, + self.line_height, + self.icon.as_ref(), + ) + } + + fn operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); + operation.text_input(state, self.id.as_ref().map(|id| &id.0)); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + self.line_height, + self.font, + self.is_secure, + self.on_input.as_deref(), + self.on_paste.as_deref(), + &self.on_submit, + || tree.state.downcast_mut::<State>(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor, + tree.state.downcast_ref::<State>(), + &self.value, + &self.placeholder, + self.size, + self.line_height, + self.font, + self.on_input.is_none(), + self.is_secure, + self.icon.as_ref(), + &self.style, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor, self.on_input.is_none()) + } +} + +impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: TextInput<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The content of the [`Icon`]. +#[derive(Debug, Clone)] +pub struct Icon<Font> { + /// The font that will be used to display the `code_point`. + pub font: Font, + /// The unicode code point that will be used as the icon. + pub code_point: char, + /// The font size of the content. + pub size: Option<f32>, + /// The spacing between the [`Icon`] and the text in a [`TextInput`]. + pub spacing: f32, + /// The side of a [`TextInput`] where to display the [`Icon`]. + pub side: Side, +} + +/// The side of a [`TextInput`]. +#[derive(Debug, Clone)] +pub enum Side { + /// The left side of a [`TextInput`]. + Left, + /// The right side of a [`TextInput`]. + Right, +} + +/// The identifier of a [`TextInput`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +impl From<Id> for widget::Id { + fn from(id: Id) -> Self { + id.0 + } +} + +/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::focusable::focus(id.0)) +} + +/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// end. +pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::text_input::move_cursor_to_end(id.0)) +} + +/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// front. +pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::text_input::move_cursor_to_front(id.0)) +} + +/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// provided position. +pub fn move_cursor_to<Message: 'static>( + id: Id, + position: usize, +) -> Command<Message> { + Command::widget(operation::text_input::move_cursor_to(id.0, position)) +} + +/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. +pub fn select_all<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::text_input::select_all(id.0)) +} + +/// Computes the layout of a [`TextInput`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + size: Option<f32>, + line_height: text::LineHeight, + icon: Option<&Icon<Renderer::Font>>, +) -> layout::Node +where + Renderer: text::Renderer, +{ + let text_size = size.unwrap_or_else(|| renderer.default_size()); + let padding = padding.fit(Size::ZERO, limits.max()); + let limits = limits + .width(width) + .pad(padding) + .height(line_height.to_absolute(Pixels(text_size))); + + let text_bounds = limits.resolve(Size::ZERO); + + if let Some(icon) = icon { + let icon_width = renderer.measure_width( + &icon.code_point.to_string(), + icon.size.unwrap_or_else(|| renderer.default_size()), + icon.font, + text::Shaping::Advanced, + ); + + let mut text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ); + + let mut icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)); + + match icon.side { + Side::Left => { + text_node.move_to(Point::new( + padding.left + icon_width + icon.spacing, + padding.top, + )); + + icon_node.move_to(Point::new(padding.left, padding.top)); + } + Side::Right => { + text_node.move_to(Point::new(padding.left, padding.top)); + + icon_node.move_to(Point::new( + padding.left + text_bounds.width - icon_width, + padding.top, + )); + } + }; + + layout::Node::with_children( + text_bounds.pad(padding), + vec![text_node, icon_node], + ) + } else { + let mut text = layout::Node::new(text_bounds); + text.move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(text_bounds.pad(padding), vec![text]) + } +} + +/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] +/// accordingly. +pub fn update<'a, Message, Renderer>( + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + value: &mut Value, + size: Option<f32>, + line_height: text::LineHeight, + font: Option<Renderer::Font>, + is_secure: bool, + on_input: Option<&dyn Fn(String) -> Message>, + on_paste: Option<&dyn Fn(String) -> Message>, + on_submit: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status +where + Message: Clone, + Renderer: text::Renderer, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + + let click_position = if on_input.is_some() { + cursor.position_over(layout.bounds()) + } else { + None + }; + + state.is_focused = if click_position.is_some() { + state.is_focused.or_else(|| { + let now = Instant::now(); + + Some(Focus { + updated_at: now, + now, + }) + }) + } else { + None + }; + + if let Some(cursor_position) = click_position { + let text_layout = layout.children().next().unwrap(); + let target = cursor_position.x - text_layout.bounds().x; + + let click = + mouse::Click::new(cursor_position, state.last_click); + + match click.kind() { + click::Kind::Single => { + let position = if target > 0.0 { + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + line_height, + &value, + state, + target, + ) + } else { + None + } + .unwrap_or(0); + + if state.keyboard_modifiers.shift() { + state.cursor.select_range( + state.cursor.start(value), + position, + ); + } else { + state.cursor.move_to(position); + } + state.is_dragging = true; + } + click::Kind::Double => { + if is_secure { + state.cursor.select_all(value); + } else { + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + line_height, + value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); + } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(value); + state.is_dragging = false; + } + } + + state.last_click = Some(click); + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state().is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state(); + + if state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; + + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font, + size, + line_height, + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = on_input else { return event::Status::Ignored }; + + if state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() + { + let mut editor = Editor::new(value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + return event::Status::Captured; + } + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = on_input else { return event::Status::Ignored }; + + let modifiers = state.keyboard_modifiers; + focus.updated_at = Instant::now(); + + match key_code { + keyboard::KeyCode::Enter + | keyboard::KeyCode::NumpadEnter => { + if let Some(on_submit) = on_submit.clone() { + shell.publish(on_submit); + } + } + keyboard::KeyCode::Backspace => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(value).is_none() + { + if is_secure { + let cursor_pos = state.cursor.end(value); + state.cursor.select_range(0, cursor_pos); + } else { + state.cursor.select_left_by_words(value); + } + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.backspace(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + } + keyboard::KeyCode::Delete => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(value).is_none() + { + if is_secure { + let cursor_pos = state.cursor.end(value); + state + .cursor + .select_range(cursor_pos, value.len()); + } else { + state.cursor.select_right_by_words(value); + } + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + } + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) + && !is_secure + { + if modifiers.shift() { + state.cursor.select_left_by_words(value); + } else { + state.cursor.move_left_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_left(value) + } else { + state.cursor.move_left(value); + } + } + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) + && !is_secure + { + if modifiers.shift() { + state.cursor.select_right_by_words(value); + } else { + state.cursor.move_right_by_words(value); + } + } else if modifiers.shift() { + state.cursor.select_right(value) + } else { + state.cursor.move_right(value); + } + } + keyboard::KeyCode::Home => { + if modifiers.shift() { + state + .cursor + .select_range(state.cursor.start(value), 0); + } else { + state.cursor.move_to(0); + } + } + keyboard::KeyCode::End => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(value), + value.len(), + ); + } else { + state.cursor.move_to(value.len()); + } + } + keyboard::KeyCode::C + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(value) + { + clipboard + .write(value.select(start, end).to_string()); + } + } + keyboard::KeyCode::X + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(value) + { + clipboard + .write(value.select(start, end).to_string()); + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + } + keyboard::KeyCode::V => { + if state.keyboard_modifiers.command() { + let content = match state.is_pasting.take() { + Some(content) => content, + None => { + let content: String = clipboard + .read() + .unwrap_or_default() + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let mut editor = + Editor::new(value, &mut state.cursor); + + editor.paste(content.clone()); + + let message = if let Some(paste) = &on_paste { + (paste)(editor.contents()) + } else { + (on_input)(editor.contents()) + }; + shell.publish(message); + + state.is_pasting = Some(content); + } else { + state.is_pasting = None; + } + } + keyboard::KeyCode::A + if state.keyboard_modifiers.command() => + { + state.cursor.select_all(value); + } + keyboard::KeyCode::Escape => { + state.is_focused = None; + state.is_dragging = false; + state.is_pasting = None; + + state.keyboard_modifiers = + keyboard::Modifiers::default(); + } + keyboard::KeyCode::Tab + | keyboard::KeyCode::Up + | keyboard::KeyCode::Down => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + let state = state(); + + if state.is_focused.is_some() { + match key_code { + keyboard::KeyCode::V => { + state.is_pasting = None; + } + keyboard::KeyCode::Tab + | keyboard::KeyCode::Up + | keyboard::KeyCode::Down => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } else { + state.is_pasting = None; + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + } + Event::Window(_, window::Event::RedrawRequested(now)) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + focus.now = now; + + let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis(millis_until_redraw as u64), + )); + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +/// +/// [`Renderer`]: text::Renderer +pub fn draw<Renderer>( + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor: mouse::Cursor, + state: &State, + value: &Value, + placeholder: &str, + size: Option<f32>, + line_height: text::LineHeight, + font: Option<Renderer::Font>, + is_disabled: bool, + is_secure: bool, + icon: Option<&Icon<Renderer::Font>>, + style: &<Renderer::Theme as StyleSheet>::Style, +) where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + let secure_value = is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let bounds = layout.bounds(); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + let is_mouse_over = cursor.is_over(bounds); + + let appearance = if is_disabled { + theme.disabled(style) + } else if state.is_focused() { + theme.focused(style) + } else if is_mouse_over { + theme.hovered(style) + } else { + theme.active(style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + + if let Some(icon) = icon { + let icon_layout = children_layout.next().unwrap(); + + renderer.fill_text(Text { + content: &icon.code_point.to_string(), + size: icon.size.unwrap_or_else(|| renderer.default_size()), + line_height: text::LineHeight::default(), + font: icon.font, + color: appearance.icon_color, + bounds: Rectangle { + y: text_bounds.center_y(), + ..icon_layout.bounds() + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }); + } + + let text = value.to_string(); + let font = font.unwrap_or_else(|| renderer.default_font()); + let size = size.unwrap_or_else(|| renderer.default_size()); + + let (cursor, offset) = if let Some(focus) = &state.is_focused { + match state.cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + position, + font, + ); + + let is_cursor_visible = ((focus.now - focus.updated_at) + .as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; + + let cursor = if is_cursor_visible { + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.value_color(style), + )) + } else { + None + }; + + (cursor, offset) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + left, + font, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + right, + font, + ); + + let width = right_position - left_position; + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.selection_color(style), + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let text_width = renderer.measure_width( + if text.is_empty() { placeholder } else { &text }, + size, + font, + text::Shaping::Advanced, + ); + + let render = |renderer: &mut Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); + } + + renderer.fill_text(Text { + content: if text.is_empty() { placeholder } else { &text }, + color: if text.is_empty() { + theme.placeholder_color(style) + } else if is_disabled { + theme.disabled_color(style) + } else { + theme.value_color(style) + }, + font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size, + line_height, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }); + }; + + if text_width > text_bounds.width { + renderer.with_layer(text_bounds, |renderer| { + renderer.with_translation(Vector::new(-offset, 0.0), render) + }); + } else { + render(renderer); + } +} + +/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor: mouse::Cursor, + is_disabled: bool, +) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + if is_disabled { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } +} + +/// The state of a [`TextInput`]. +#[derive(Debug, Default, Clone)] +pub struct State { + is_focused: Option<Focus>, + is_dragging: bool, + is_pasting: Option<Value>, + last_click: Option<mouse::Click>, + cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, + // TODO: Add stateful horizontal scrolling offset +} + +#[derive(Debug, Clone, Copy)] +struct Focus { + updated_at: Instant, + now: Instant, +} + +impl State { + /// Creates a new [`State`], representing an unfocused [`TextInput`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new [`State`], representing a focused [`TextInput`]. + pub fn focused() -> Self { + Self { + is_focused: None, + is_dragging: false, + is_pasting: None, + last_click: None, + cursor: Cursor::default(), + keyboard_modifiers: keyboard::Modifiers::default(), + } + } + + /// Returns whether the [`TextInput`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused.is_some() + } + + /// Returns the [`Cursor`] of the [`TextInput`]. + pub fn cursor(&self) -> Cursor { + self.cursor + } + + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + let now = Instant::now(); + + self.is_focused = Some(Focus { + updated_at: now, + now, + }); + + self.move_cursor_to_end(); + } + + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = None; + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); + } + + /// Selects all the content of the [`TextInput`]. + pub fn select_all(&mut self) { + self.cursor.select_range(0, usize::MAX); + } +} + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} + +impl operation::TextInput for State { + fn move_cursor_to_front(&mut self) { + State::move_cursor_to_front(self) + } + + fn move_cursor_to_end(&mut self) { + State::move_cursor_to_end(self) + } + + fn move_cursor_to(&mut self, position: usize) { + State::move_cursor_to(self, position) + } + + fn select_all(&mut self) { + State::select_all(self) + } +} + +mod platform { + use crate::core::keyboard; + + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.alt() + } else { + modifiers.control() + } + } +} + +fn offset<Renderer>( + renderer: &Renderer, + text_bounds: Rectangle, + font: Renderer::Font, + size: f32, + value: &Value, + state: &State, +) -> f32 +where + Renderer: text::Renderer, +{ + if state.is_focused() { + let cursor = state.cursor(); + + let focus_position = match cursor.state(value) { + cursor::State::Index(i) => i, + cursor::State::Selection { end, .. } => end, + }; + + let (_, offset) = measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + focus_position, + font, + ); + + offset + } else { + 0.0 + } +} + +fn measure_cursor_and_scroll_offset<Renderer>( + renderer: &Renderer, + text_bounds: Rectangle, + value: &Value, + size: f32, + cursor_index: usize, + font: Renderer::Font, +) -> (f32, f32) +where + Renderer: text::Renderer, +{ + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = renderer.measure_width( + &text_before_cursor, + size, + font, + text::Shaping::Advanced, + ); + + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} + +/// Computes the position of the text cursor at the given X coordinate of +/// a [`TextInput`]. +fn find_cursor_position<Renderer>( + renderer: &Renderer, + text_bounds: Rectangle, + font: Option<Renderer::Font>, + size: Option<f32>, + line_height: text::LineHeight, + value: &Value, + state: &State, + x: f32, +) -> Option<usize> +where + Renderer: text::Renderer, +{ + let font = font.unwrap_or_else(|| renderer.default_font()); + let size = size.unwrap_or_else(|| renderer.default_size()); + + let offset = offset(renderer, text_bounds, font, size, value, state); + let value = value.to_string(); + + let char_offset = renderer + .hit_test( + &value, + size, + line_height, + font, + Size::INFINITY, + text::Shaping::Advanced, + Point::new(x + offset, text_bounds.height / 2.0), + true, + ) + .map(text::Hit::cursor)?; + + Some( + unicode_segmentation::UnicodeSegmentation::graphemes( + &value[..char_offset], + true, + ) + .count(), + ) +} + +const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; diff --git a/widget/src/text_input/cursor.rs b/widget/src/text_input/cursor.rs new file mode 100644 index 00000000..9680dfd7 --- /dev/null +++ b/widget/src/text_input/cursor.rs @@ -0,0 +1,189 @@ +//! Track the cursor of a text input. +use crate::text_input::Value; + +/// The cursor of a text input. +#[derive(Debug, Copy, Clone)] +pub struct Cursor { + state: State, +} + +/// The state of a [`Cursor`]. +#[derive(Debug, Copy, Clone)] +pub enum State { + /// Cursor without a selection + Index(usize), + + /// Cursor selecting a range of text + Selection { + /// The start of the selection + start: usize, + /// The end of the selection + end: usize, + }, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + state: State::Index(0), + } + } +} + +impl Cursor { + /// Returns the [`State`] of the [`Cursor`]. + pub fn state(&self, value: &Value) -> State { + match self.state { + State::Index(index) => State::Index(index.min(value.len())), + State::Selection { start, end } => { + let start = start.min(value.len()); + let end = end.min(value.len()); + + if start == end { + State::Index(start) + } else { + State::Selection { start, end } + } + } + } + } + + /// Returns the current selection of the [`Cursor`] for the given [`Value`]. + /// + /// `start` is guaranteed to be <= than `end`. + pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { + match self.state(value) { + State::Selection { start, end } => { + Some((start.min(end), start.max(end))) + } + _ => None, + } + } + + pub(crate) fn move_to(&mut self, position: usize) { + self.state = State::Index(position); + } + + pub(crate) fn move_right(&mut self, value: &Value) { + self.move_right_by_amount(value, 1) + } + + pub(crate) fn move_right_by_words(&mut self, value: &Value) { + self.move_to(value.next_end_of_word(self.right(value))) + } + + pub(crate) fn move_right_by_amount( + &mut self, + value: &Value, + amount: usize, + ) { + match self.state(value) { + State::Index(index) => { + self.move_to(index.saturating_add(amount).min(value.len())) + } + State::Selection { start, end } => self.move_to(end.max(start)), + } + } + + pub(crate) fn move_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => self.move_to(index - 1), + State::Selection { start, end } => self.move_to(start.min(end)), + _ => self.move_to(0), + } + } + + pub(crate) fn move_left_by_words(&mut self, value: &Value) { + self.move_to(value.previous_start_of_word(self.left(value))); + } + + pub(crate) fn select_range(&mut self, start: usize, end: usize) { + if start == end { + self.state = State::Index(start); + } else { + self.state = State::Selection { start, end }; + } + } + + pub(crate) fn select_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => { + self.select_range(index, index - 1) + } + State::Selection { start, end } if end > 0 => { + self.select_range(start, end - 1) + } + _ => {} + } + } + + pub(crate) fn select_right(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index < value.len() => { + self.select_range(index, index + 1) + } + State::Selection { start, end } if end < value.len() => { + self.select_range(start, end + 1) + } + _ => {} + } + } + + pub(crate) fn select_left_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => { + self.select_range(index, value.previous_start_of_word(index)) + } + State::Selection { start, end } => { + self.select_range(start, value.previous_start_of_word(end)) + } + } + } + + pub(crate) fn select_right_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => { + self.select_range(index, value.next_end_of_word(index)) + } + State::Selection { start, end } => { + self.select_range(start, value.next_end_of_word(end)) + } + } + } + + pub(crate) fn select_all(&mut self, value: &Value) { + self.select_range(0, value.len()); + } + + pub(crate) fn start(&self, value: &Value) -> usize { + let start = match self.state { + State::Index(index) => index, + State::Selection { start, .. } => start, + }; + + start.min(value.len()) + } + + pub(crate) fn end(&self, value: &Value) -> usize { + let end = match self.state { + State::Index(index) => index, + State::Selection { end, .. } => end, + }; + + end.min(value.len()) + } + + fn left(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.min(end), + } + } + + fn right(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.max(end), + } + } +} diff --git a/widget/src/text_input/editor.rs b/widget/src/text_input/editor.rs new file mode 100644 index 00000000..f1fd641f --- /dev/null +++ b/widget/src/text_input/editor.rs @@ -0,0 +1,70 @@ +use crate::text_input::{Cursor, Value}; + +pub struct Editor<'a> { + value: &'a mut Value, + cursor: &'a mut Cursor, +} + +impl<'a> Editor<'a> { + pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { + Editor { value, cursor } + } + + pub fn contents(&self) -> String { + self.value.to_string() + } + + pub fn insert(&mut self, character: char) { + if let Some((left, right)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + + self.value.insert(self.cursor.end(self.value), character); + self.cursor.move_right(self.value); + } + + pub fn paste(&mut self, content: Value) { + let length = content.len(); + if let Some((left, right)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + + self.value.insert_many(self.cursor.end(self.value), content); + + self.cursor.move_right_by_amount(self.value, length); + } + + pub fn backspace(&mut self) { + match self.cursor.selection(self.value) { + Some((start, end)) => { + self.cursor.move_left(self.value); + self.value.remove_many(start, end); + } + None => { + let start = self.cursor.start(self.value); + + if start > 0 { + self.cursor.move_left(self.value); + self.value.remove(start - 1); + } + } + } + } + + pub fn delete(&mut self) { + match self.cursor.selection(self.value) { + Some(_) => { + self.backspace(); + } + None => { + let end = self.cursor.end(self.value); + + if end < self.value.len() { + self.value.remove(end); + } + } + } + } +} diff --git a/widget/src/text_input/value.rs b/widget/src/text_input/value.rs new file mode 100644 index 00000000..cf4da562 --- /dev/null +++ b/widget/src/text_input/value.rs @@ -0,0 +1,133 @@ +use unicode_segmentation::UnicodeSegmentation; + +/// The value of a [`TextInput`]. +/// +/// [`TextInput`]: crate::widget::TextInput +// TODO: Reduce allocations, cache results (?) +#[derive(Debug, Clone)] +pub struct Value { + graphemes: Vec<String>, +} + +impl Value { + /// Creates a new [`Value`] from a string slice. + pub fn new(string: &str) -> Self { + let graphemes = UnicodeSegmentation::graphemes(string, true) + .map(String::from) + .collect(); + + Self { graphemes } + } + + /// Returns whether the [`Value`] is empty or not. + /// + /// A [`Value`] is empty when it contains no graphemes. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the total amount of graphemes in the [`Value`]. + pub fn len(&self) -> usize { + self.graphemes.len() + } + + /// Returns the position of the previous start of a word from the given + /// grapheme `index`. + pub fn previous_start_of_word(&self, index: usize) -> usize { + let previous_string = + &self.graphemes[..index.min(self.graphemes.len())].concat(); + + UnicodeSegmentation::split_word_bound_indices(previous_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next_back() + .map(|(i, previous_word)| { + index + - UnicodeSegmentation::graphemes(previous_word, true) + .count() + - UnicodeSegmentation::graphemes( + &previous_string[i + previous_word.len()..] as &str, + true, + ) + .count() + }) + .unwrap_or(0) + } + + /// Returns the position of the next end of a word from the given grapheme + /// `index`. + pub fn next_end_of_word(&self, index: usize) -> usize { + let next_string = &self.graphemes[index..].concat(); + + UnicodeSegmentation::split_word_bound_indices(next_string as &str) + .find(|(_, word)| !word.trim_start().is_empty()) + .map(|(i, next_word)| { + index + + UnicodeSegmentation::graphemes(next_word, true).count() + + UnicodeSegmentation::graphemes( + &next_string[..i] as &str, + true, + ) + .count() + }) + .unwrap_or(self.len()) + } + + /// Returns a new [`Value`] containing the graphemes from `start` until the + /// given `end`. + pub fn select(&self, start: usize, end: usize) -> Self { + let graphemes = + self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); + + Self { graphemes } + } + + /// Returns a new [`Value`] containing the graphemes until the given + /// `index`. + pub fn until(&self, index: usize) -> Self { + let graphemes = self.graphemes[..index.min(self.len())].to_vec(); + + Self { graphemes } + } + + /// Converts the [`Value`] into a `String`. + pub fn to_string(&self) -> String { + self.graphemes.concat() + } + + /// Inserts a new `char` at the given grapheme `index`. + pub fn insert(&mut self, index: usize, c: char) { + self.graphemes.insert(index, c.to_string()); + + self.graphemes = + UnicodeSegmentation::graphemes(&self.to_string() as &str, true) + .map(String::from) + .collect(); + } + + /// Inserts a bunch of graphemes at the given grapheme `index`. + pub fn insert_many(&mut self, index: usize, mut value: Value) { + let _ = self + .graphemes + .splice(index..index, value.graphemes.drain(..)); + } + + /// Removes the grapheme at the given `index`. + pub fn remove(&mut self, index: usize) { + let _ = self.graphemes.remove(index); + } + + /// Removes the graphemes from `start` to `end`. + pub fn remove_many(&mut self, start: usize, end: usize) { + let _ = self.graphemes.splice(start..end, std::iter::empty()); + } + + /// Returns a new [`Value`] with all its graphemes replaced with the + /// dot ('•') character. + pub fn secure(&self) -> Self { + Self { + graphemes: std::iter::repeat(String::from("•")) + .take(self.graphemes.len()) + .collect(), + } + } +} diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs new file mode 100644 index 00000000..1b31765f --- /dev/null +++ b/widget/src/toggler.rs @@ -0,0 +1,351 @@ +//! Show toggle controls using togglers. +use crate::core::alignment; +use crate::core::event; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text; +use crate::core::touch; +use crate::core::widget::Tree; +use crate::core::{ + Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, + Shell, Widget, +}; +use crate::{Row, Text}; + +pub use crate::style::toggler::{Appearance, StyleSheet}; + +/// A toggler widget. +/// +/// # Example +/// +/// ```no_run +/// # type Toggler<'a, Message> = +/// # iced_widget::Toggler<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_toggled = true; +/// +/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); +/// ``` +#[allow(missing_debug_implementations)] +pub struct Toggler<'a, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + is_toggled: bool, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, + label: Option<String>, + width: Length, + size: f32, + text_size: Option<f32>, + text_line_height: text::LineHeight, + text_alignment: alignment::Horizontal, + text_shaping: text::Shaping, + spacing: f32, + font: Option<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Toggler<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default size of a [`Toggler`]. + pub const DEFAULT_SIZE: f32 = 20.0; + + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + pub fn new<F>( + label: impl Into<Option<String>>, + is_toggled: bool, + f: F, + ) -> Self + where + F: 'a + Fn(bool) -> Message, + { + Toggler { + is_toggled, + on_toggle: Box::new(f), + label: label.into(), + width: Length::Fill, + size: Self::DEFAULT_SIZE, + text_size: None, + text_line_height: text::LineHeight::default(), + text_alignment: alignment::Horizontal::Left, + text_shaping: text::Shaping::Basic, + spacing: 0.0, + font: None, + style: Default::default(), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = size.into().0; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Toggler`]. + pub fn text_line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Toggler`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { + self.spacing = spacing.into().0; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + /// + /// [`Font`]: crate::text::Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the style of the [`Toggler`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Toggler<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let mut row = Row::<(), Renderer>::new() + .width(self.width) + .spacing(self.spacing) + .align_items(Alignment::Center); + + if let Some(label) = &self.label { + row = row.push( + Text::new(label) + .horizontal_alignment(self.text_alignment) + .font(self.font.unwrap_or_else(|| renderer.default_font())) + .width(self.width) + .size( + self.text_size + .unwrap_or_else(|| renderer.default_size()), + ) + .line_height(self.text_line_height) + .shaping(self.text_shaping), + ); + } + + row = row.push(Row::new().width(2.0 * self.size).height(self.size)); + + row.layout(renderer, limits) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let mouse_over = cursor.is_over(layout.bounds()); + + if mouse_over { + shell.publish((self.on_toggle)(!self.is_toggled)); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + /// Makes sure that the border radius of the toggler looks good at every size. + const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; + + /// The space ratio between the background Quad and the Toggler bounds, and + /// between the background Quad and foreground Quad. + const SPACE_RATIO: f32 = 0.05; + + let mut children = layout.children(); + + if let Some(label) = &self.label { + let label_layout = children.next().unwrap(); + + crate::text::draw( + renderer, + style, + label_layout, + label, + self.text_size, + self.text_line_height, + self.font, + Default::default(), + self.text_alignment, + alignment::Vertical::Center, + self.text_shaping, + ); + } + + let toggler_layout = children.next().unwrap(); + let bounds = toggler_layout.bounds(); + + let is_mouse_over = cursor.is_over(layout.bounds()); + + let style = if is_mouse_over { + theme.hovered(&self.style, self.is_toggled) + } else { + theme.active(&self.style, self.is_toggled) + }; + + let border_radius = bounds.height / BORDER_RADIUS_RATIO; + let space = SPACE_RATIO * bounds.height; + + let toggler_background_bounds = Rectangle { + x: bounds.x + space, + y: bounds.y + space, + width: bounds.width - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_background_bounds, + border_radius: border_radius.into(), + border_width: 1.0, + border_color: style + .background_border + .unwrap_or(style.background), + }, + style.background, + ); + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + if self.is_toggled { + bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + } else { + 2.0 * space + }, + y: bounds.y + (2.0 * space), + width: bounds.height - (4.0 * space), + height: bounds.height - (4.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_foreground_bounds, + border_radius: border_radius.into(), + border_width: 1.0, + border_color: style + .foreground_border + .unwrap_or(style.foreground), + }, + style.foreground, + ); + } +} + +impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet + crate::text::StyleSheet, +{ + fn from( + toggler: Toggler<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(toggler) + } +} diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs new file mode 100644 index 00000000..2dc3da01 --- /dev/null +++ b/widget/src/tooltip.rs @@ -0,0 +1,446 @@ +//! Display a widget over another. +use crate::container; +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::text; +use crate::core::widget::{self, Widget}; +use crate::core::{ + Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Vector, +}; +use crate::Text; + +use std::borrow::Cow; + +/// An element to display a widget over another. +#[allow(missing_debug_implementations)] +pub struct Tooltip<'a, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, +{ + content: Element<'a, Message, Renderer>, + tooltip: Text<'a, Renderer>, + position: Position, + gap: f32, + padding: f32, + snap_within_viewport: bool, + style: <Renderer::Theme as container::StyleSheet>::Style, +} + +impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, +{ + /// The default padding of a [`Tooltip`] drawn by this renderer. + const DEFAULT_PADDING: f32 = 5.0; + + /// Creates a new [`Tooltip`]. + /// + /// [`Tooltip`]: struct.Tooltip.html + pub fn new( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl Into<Cow<'a, str>>, + position: Position, + ) -> Self { + Tooltip { + content: content.into(), + tooltip: Text::new(tooltip), + position, + gap: 0.0, + padding: Self::DEFAULT_PADDING, + snap_within_viewport: true, + style: Default::default(), + } + } + + /// Sets the size of the text of the [`Tooltip`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.tooltip = self.tooltip.size(size); + self + } + + /// Sets the font of the [`Tooltip`]. + /// + /// [`Font`]: Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.tooltip = self.tooltip.font(font); + self + } + + /// Sets the gap between the content and its [`Tooltip`]. + pub fn gap(mut self, gap: impl Into<Pixels>) -> Self { + self.gap = gap.into().0; + self + } + + /// Sets the padding of the [`Tooltip`]. + pub fn padding(mut self, padding: impl Into<Pixels>) -> Self { + self.padding = padding.into().0; + self + } + + /// Sets whether the [`Tooltip`] is snapped within the viewport. + pub fn snap_within_viewport(mut self, snap: bool) -> Self { + self.snap_within_viewport = snap; + self + } + + /// Sets the style of the [`Tooltip`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Tooltip<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, +{ + fn children(&self) -> Vec<widget::Tree> { + vec![widget::Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut widget::Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State::default()) + } + + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::<State>() + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(renderer, limits) + } + + fn on_event( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::<State>(); + + *state = cursor + .position_over(layout.bounds()) + .map(|cursor_position| State::Hovered { cursor_position }) + .unwrap_or_default(); + + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + ) + } + + 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.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + inherited_style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_ref::<State>(); + + let content = self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + ); + + let tooltip = if let State::Hovered { cursor_position } = *state { + Some(overlay::Element::new( + layout.position(), + Box::new(Overlay { + tooltip: &self.tooltip, + cursor_position, + content_bounds: layout.bounds(), + snap_within_viewport: self.snap_within_viewport, + position: self.position, + gap: self.gap, + padding: self.padding, + style: &self.style, + }), + )) + } else { + None + }; + + if content.is_some() || tooltip.is_some() { + Some( + overlay::Group::with_children( + content.into_iter().chain(tooltip).collect(), + ) + .overlay(), + ) + } else { + None + } + } +} + +impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, +{ + fn from( + tooltip: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(tooltip) + } +} + +/// The position of the tooltip. Defaults to following the cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The tooltip will follow the cursor. + FollowCursor, + /// The tooltip will appear on the top of the widget. + Top, + /// The tooltip will appear on the bottom of the widget. + Bottom, + /// The tooltip will appear on the left of the widget. + Left, + /// The tooltip will appear on the right of the widget. + Right, +} + +#[derive(Debug, Clone, Copy, Default)] +enum State { + #[default] + Idle, + Hovered { + cursor_position: Point, + }, +} + +struct Overlay<'a, 'b, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + tooltip: &'b Text<'a, Renderer>, + cursor_position: Point, + content_bounds: Rectangle, + snap_within_viewport: bool, + position: Position, + gap: f32, + padding: f32, + style: &'b <Renderer::Theme as container::StyleSheet>::Style, +} + +impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer> + for Overlay<'a, 'b, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + _position: Point, + ) -> layout::Node { + let viewport = Rectangle::with_size(bounds); + + let text_layout = Widget::<(), Renderer>::layout( + self.tooltip, + renderer, + &layout::Limits::new( + Size::ZERO, + self.snap_within_viewport + .then(|| viewport.size()) + .unwrap_or(Size::INFINITY), + ) + .pad(Padding::new(self.padding)), + ); + + let text_bounds = text_layout.bounds(); + let x_center = self.content_bounds.x + + (self.content_bounds.width - text_bounds.width) / 2.0; + let y_center = self.content_bounds.y + + (self.content_bounds.height - text_bounds.height) / 2.0; + + let mut tooltip_bounds = { + let offset = match self.position { + Position::Top => Vector::new( + x_center, + self.content_bounds.y + - text_bounds.height + - self.gap + - self.padding, + ), + Position::Bottom => Vector::new( + x_center, + self.content_bounds.y + + self.content_bounds.height + + self.gap + + self.padding, + ), + Position::Left => Vector::new( + self.content_bounds.x + - text_bounds.width + - self.gap + - self.padding, + y_center, + ), + Position::Right => Vector::new( + self.content_bounds.x + + self.content_bounds.width + + self.gap + + self.padding, + y_center, + ), + Position::FollowCursor => Vector::new( + self.cursor_position.x, + self.cursor_position.y - text_bounds.height, + ), + }; + + Rectangle { + x: offset.x - self.padding, + y: offset.y - self.padding, + width: text_bounds.width + self.padding * 2.0, + height: text_bounds.height + self.padding * 2.0, + } + }; + + if self.snap_within_viewport { + if tooltip_bounds.x < viewport.x { + tooltip_bounds.x = viewport.x; + } else if viewport.x + viewport.width + < tooltip_bounds.x + tooltip_bounds.width + { + tooltip_bounds.x = + viewport.x + viewport.width - tooltip_bounds.width; + } + + if tooltip_bounds.y < viewport.y { + tooltip_bounds.y = viewport.y; + } else if viewport.y + viewport.height + < tooltip_bounds.y + tooltip_bounds.height + { + tooltip_bounds.y = + viewport.y + viewport.height - tooltip_bounds.height; + } + } + + layout::Node::with_children( + tooltip_bounds.size(), + vec![text_layout.translate(Vector::new(self.padding, self.padding))], + ) + .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y)) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + ) { + let style = <Renderer::Theme as container::StyleSheet>::appearance( + theme, self.style, + ); + + container::draw_background(renderer, &style, layout.bounds()); + + let defaults = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + Widget::<(), Renderer>::draw( + self.tooltip, + &widget::Tree::empty(), + renderer, + theme, + &defaults, + layout.children().next().unwrap(), + cursor_position, + &Rectangle::with_size(Size::INFINITY), + ); + } + + fn is_over( + &self, + _layout: Layout<'_>, + _renderer: &Renderer, + _cursor_position: Point, + ) -> bool { + false + } +} diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs new file mode 100644 index 00000000..91f2b466 --- /dev/null +++ b/widget/src/vertical_slider.rs @@ -0,0 +1,469 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`VerticalSlider`] has some local [`State`]. +use std::ops::RangeInclusive; + +pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet}; + +use crate::core; +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + Clipboard, Color, Element, Length, Pixels, Point, Rectangle, Shell, Size, + Widget, +}; + +/// An vertical bar and a handle that selects a single value from a range of +/// values. +/// +/// A [`VerticalSlider`] will try to fill the vertical space of its container. +/// +/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ```no_run +/// # type VerticalSlider<'a, T, Message> = +/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// #[derive(Clone)] +/// pub enum Message { +/// SliderChanged(f32), +/// } +/// +/// let value = 50.0; +/// +/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); +/// ``` +#[allow(missing_debug_implementations)] +pub struct VerticalSlider<'a, T, Message, Renderer = crate::Renderer> +where + Renderer: core::Renderer, + Renderer::Theme: StyleSheet, +{ + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message + 'a>, + on_release: Option<Message>, + width: f32, + height: Length, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default width of a [`VerticalSlider`]. + pub const DEFAULT_WIDTH: f32 = 22.0; + + /// Creates a new [`VerticalSlider`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`VerticalSlider`] + /// * a function that will be called when the [`VerticalSlider`] is dragged. + /// It receives the new value of the [`VerticalSlider`] and must produce a + /// `Message`. + pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + + VerticalSlider { + value, + range, + step: T::from(1), + on_change: Box::new(on_change), + on_release: None, + width: Self::DEFAULT_WIDTH, + height: Length::Fill, + style: Default::default(), + } + } + + /// Sets the release message of the [`VerticalSlider`]. + /// This is called when the mouse is released from the slider. + /// + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`VerticalSlider`]. + pub fn width(mut self, width: impl Into<Pixels>) -> Self { + self.width = width.into().0; + self + } + + /// Sets the height of the [`VerticalSlider`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the style of the [`VerticalSlider`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the step size of the [`VerticalSlider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, + Renderer: core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor, + shell, + tree.state.downcast_mut::<State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + &self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) + } +} + +impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + slider: VerticalSlider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} + +/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`] +/// accordingly. +pub fn update<Message, T>( + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive<T>, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option<Message>, +) -> event::Status +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, +{ + let is_dragging = state.is_dragging; + + let mut change = |cursor_position: Point| { + let bounds = layout.bounds(); + + let new_value = if cursor_position.y >= bounds.y + bounds.height { + *range.start() + } else if cursor_position.y <= bounds.y { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = cursor.position_over(layout.bounds()) + { + change(cursor_position); + state.is_dragging = true; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + let _ = cursor.position().map(change); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`VerticalSlider`]. +pub fn draw<T, R>( + renderer: &mut R, + layout: Layout<'_>, + cursor: mouse::Cursor, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>, + style: &<R::Theme as StyleSheet>::Style, +) where + T: Into<f64> + Copy, + R: core::Renderer, + R::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = if state.is_dragging { + style_sheet.dragging(style) + } else if is_mouse_over { + style_sheet.hovered(style) + } else { + style_sheet.active(style) + }; + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.width, border_radius), + }; + + let value = value.into() as f32; + let (range_start, range_end) = { + let (start, end) = range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.height - handle_width) * (value - range_end) + / (range_start - range_end) + }; + + let rail_x = bounds.x + bounds.width / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: offset + handle_width / 2.0, + }, + border_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y + offset + handle_width / 2.0, + width: style.rail.width, + height: bounds.height - offset - handle_width / 2.0, + }, + border_radius: style.rail.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - handle_height / 2.0, + y: bounds.y + offset, + width: handle_height, + height: handle_width, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor: mouse::Cursor, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } +} + +/// The local state of a [`VerticalSlider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_dragging: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } +} |