From 3a0d34c0240f4421737a6a08761f99d6f8140d02 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Sat, 4 Mar 2023 05:37:11 +0100 Subject: Create `iced_widget` subcrate and re-organize the whole codebase --- widget/src/button.rs | 455 +++++++++++ widget/src/canvas.rs | 238 ++++++ widget/src/canvas/cursor.rs | 64 ++ widget/src/canvas/event.rs | 21 + widget/src/canvas/program.rs | 109 +++ widget/src/checkbox.rs | 323 ++++++++ widget/src/column.rs | 264 +++++++ widget/src/container.rs | 368 +++++++++ widget/src/helpers.rs | 362 +++++++++ widget/src/image.rs | 205 +++++ widget/src/image/viewer.rs | 428 +++++++++++ widget/src/lazy.rs | 409 ++++++++++ widget/src/lazy/cache.rs | 13 + widget/src/lazy/component.rs | 575 ++++++++++++++ widget/src/lazy/helpers.rs | 39 + widget/src/lazy/responsive.rs | 427 +++++++++++ widget/src/lib.rs | 122 +++ widget/src/overlay.rs | 1 + widget/src/overlay/menu.rs | 519 +++++++++++++ widget/src/pane_grid.rs | 991 ++++++++++++++++++++++++ widget/src/pane_grid/axis.rs | 241 ++++++ widget/src/pane_grid/configuration.rs | 26 + widget/src/pane_grid/content.rs | 373 ++++++++++ widget/src/pane_grid/direction.rs | 12 + widget/src/pane_grid/draggable.rs | 12 + widget/src/pane_grid/node.rs | 250 +++++++ widget/src/pane_grid/pane.rs | 5 + widget/src/pane_grid/split.rs | 5 + widget/src/pane_grid/state.rs | 348 +++++++++ widget/src/pane_grid/title_bar.rs | 432 +++++++++++ widget/src/pick_list.rs | 658 ++++++++++++++++ widget/src/progress_bar.rs | 172 +++++ widget/src/qr_code.rs | 297 ++++++++ widget/src/radio.rs | 300 ++++++++ widget/src/row.rs | 253 +++++++ widget/src/rule.rs | 147 ++++ widget/src/scrollable.rs | 1325 +++++++++++++++++++++++++++++++++ widget/src/slider.rs | 471 ++++++++++++ widget/src/space.rs | 86 +++ widget/src/svg.rs | 195 +++++ widget/src/text.rs | 4 + widget/src/text_input.rs | 1221 ++++++++++++++++++++++++++++++ widget/src/text_input/cursor.rs | 189 +++++ widget/src/text_input/editor.rs | 70 ++ widget/src/text_input/value.rs | 133 ++++ widget/src/toggler.rs | 326 ++++++++ widget/src/tooltip.rs | 388 ++++++++++ widget/src/vertical_slider.rs | 471 ++++++++++++ 48 files changed, 14343 insertions(+) create mode 100644 widget/src/button.rs create mode 100644 widget/src/canvas.rs create mode 100644 widget/src/canvas/cursor.rs create mode 100644 widget/src/canvas/event.rs create mode 100644 widget/src/canvas/program.rs create mode 100644 widget/src/checkbox.rs create mode 100644 widget/src/column.rs create mode 100644 widget/src/container.rs create mode 100644 widget/src/helpers.rs create mode 100644 widget/src/image.rs create mode 100644 widget/src/image/viewer.rs create mode 100644 widget/src/lazy.rs create mode 100644 widget/src/lazy/cache.rs create mode 100644 widget/src/lazy/component.rs create mode 100644 widget/src/lazy/helpers.rs create mode 100644 widget/src/lazy/responsive.rs create mode 100644 widget/src/lib.rs create mode 100644 widget/src/overlay.rs create mode 100644 widget/src/overlay/menu.rs create mode 100644 widget/src/pane_grid.rs create mode 100644 widget/src/pane_grid/axis.rs create mode 100644 widget/src/pane_grid/configuration.rs create mode 100644 widget/src/pane_grid/content.rs create mode 100644 widget/src/pane_grid/direction.rs create mode 100644 widget/src/pane_grid/draggable.rs create mode 100644 widget/src/pane_grid/node.rs create mode 100644 widget/src/pane_grid/pane.rs create mode 100644 widget/src/pane_grid/split.rs create mode 100644 widget/src/pane_grid/state.rs create mode 100644 widget/src/pane_grid/title_bar.rs create mode 100644 widget/src/pick_list.rs create mode 100644 widget/src/progress_bar.rs create mode 100644 widget/src/qr_code.rs create mode 100644 widget/src/radio.rs create mode 100644 widget/src/row.rs create mode 100644 widget/src/rule.rs create mode 100644 widget/src/scrollable.rs create mode 100644 widget/src/slider.rs create mode 100644 widget/src/space.rs create mode 100644 widget/src/svg.rs create mode 100644 widget/src/text.rs create mode 100644 widget/src/text_input.rs create mode 100644 widget/src/text_input/cursor.rs create mode 100644 widget/src/text_input/editor.rs create mode 100644 widget/src/text_input/value.rs create mode 100644 widget/src/toggler.rs create mode 100644 widget/src/tooltip.rs create mode 100644 widget/src/vertical_slider.rs (limited to 'widget/src') diff --git a/widget/src/button.rs b/widget/src/button.rs new file mode 100644 index 00000000..d6fd3997 --- /dev/null +++ b/widget/src/button.rs @@ -0,0 +1,455 @@ +//! 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. +/// +/// ``` +/// # 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, msg: Message) -> Self { + self.on_press = Some(msg); + 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_position: Point, + 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_position, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + update( + event, + layout, + cursor_position, + 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_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let styling = draw( + renderer, + bounds, + cursor_position, + 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_position, + &bounds, + ); + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position, 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_position: Point, + 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 bounds.contains(cursor_position) { + 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 bounds.contains(cursor_position) { + 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_position: Point, + 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 = bounds.contains(cursor_position); + + 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.into(), + 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.into(), + 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_position: Point, + is_enabled: bool, +) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + + 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..171c4534 --- /dev/null +++ b/widget/src/canvas.rs @@ -0,0 +1,238 @@ +//! Draw 2D graphics for your users. +pub mod event; + +mod cursor; +mod program; + +pub use cursor::Cursor; +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, Point, 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, Cursor, Fill, Frame, Geometry, Path, Program}; +/// # use iced_widget::core::{Color, Rectangle}; +/// # 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: 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_position: Point, + _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, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + 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_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + 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_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let cursor = Cursor::from_window_position(cursor_position); + 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/cursor.rs b/widget/src/canvas/cursor.rs new file mode 100644 index 00000000..5a65e9a7 --- /dev/null +++ b/widget/src/canvas/cursor.rs @@ -0,0 +1,64 @@ +use crate::core::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + pub fn position(&self) -> Option<Point> { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + pub fn position_from(&self, origin: Point) -> Option<Point> { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} 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..efb33c56 --- /dev/null +++ b/widget/src/canvas/program.rs @@ -0,0 +1,109 @@ +use crate::canvas::event::{self, Event}; +use crate::canvas::mouse; +use crate::canvas::Cursor; +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: 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: 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: 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: 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: Cursor, + ) -> Vec<Renderer::Geometry> { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: 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..6a062f94 --- /dev/null +++ b/widget/src/checkbox.rs @@ -0,0 +1,323 @@ +//! 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, Point, Rectangle, + Shell, Widget, +}; +use crate::{Row, Text}; + +pub use iced_style::checkbox::{Appearance, StyleSheet}; + +/// 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>, +} + +/// A box that can be checked. +/// +/// # Example +/// +/// ``` +/// # 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>, + 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, + font: None, + icon: Icon { + font: Renderer::ICON_FONT, + code_point: Renderer::CHECKMARK_ICON, + size: None, + }, + 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 [`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()), + ), + ) + .layout(renderer, limits) + } + + fn on_event( + &mut self, + _tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _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 = layout.bounds().contains(cursor_position); + + 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_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + 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.into(), + border_width: custom_style.border_width, + border_color: custom_style.border_color, + }, + custom_style.background, + ); + + let Icon { + font, + code_point, + size, + } = &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, + 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, + }); + } + } + + { + let label_layout = children.next().unwrap(); + + crate::text::draw( + renderer, + style, + label_layout, + &self.label, + self.text_size, + self.font, + crate::text::Appearance { + color: custom_style.text_color, + }, + alignment::Horizontal::Left, + alignment::Vertical::Center, + ); + } + } +} + +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) + } +} diff --git a/widget/src/column.rs b/widget/src/column.rs new file mode 100644 index 00000000..8f363ec6 --- /dev/null +++ b/widget/src/column.rs @@ -0,0 +1,264 @@ +//! 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, Point, + 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_position: Point, + 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_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + 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..9d932772 --- /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_position: Point, + 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_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + 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.into(), + 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..1a73c16f --- /dev/null +++ b/widget/src/helpers.rs @@ -0,0 +1,362 @@ +//! 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::native::Command; +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::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, 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(value, label, 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, + on_change: impl Fn(String) -> Message + 'a, +) -> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: core::text::Renderer, + Renderer::Theme: text_input::StyleSheet, +{ + TextInput::new(placeholder, value, on_change) +} + +/// 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")] +#[cfg_attr(docsrs, doc(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")] +#[cfg_attr(docsrs, doc(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")] +#[cfg_attr(docsrs, doc(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()) +} diff --git a/widget/src/image.rs b/widget/src/image.rs new file mode 100644 index 00000000..22a3a1a1 --- /dev/null +++ b/widget/src/image.rs @@ -0,0 +1,205 @@ +//! Display images in your user interface. +pub mod viewer; +pub use viewer::Viewer; + +use crate::core::image; +use crate::core::layout; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{ + ContentFit, Element, Layout, Length, Point, 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 +/// +/// ``` +/// # 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_position: Point, + _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..0d60d818 --- /dev/null +++ b/widget/src/image/viewer.rs @@ -0,0 +1,428 @@ +//! 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_position: Point, + renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if is_mouse_over => + { + 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)) + if is_mouse_over => + { + 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_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + 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_position: Point, + _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..b08ed8cb --- /dev/null +++ b/widget/src/lazy.rs @@ -0,0 +1,409 @@ +#![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 ouroboros::self_referencing; +use std::cell::RefCell; +use std::hash::{Hash, Hasher as H}; +use std::rc::Rc; + +#[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>>, +{ + 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_position: Point, + 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_position, + renderer, + clipboard, + shell, + ) + }) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_element(|element| { + element.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.with_element(|element| { + element.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + 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) + }, + } + .build(), + )); + + let has_overlay = overlay + .with_overlay_maybe(|overlay| overlay::Element::position(overlay)); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) + } +} + +#[self_referencing] +struct Inner<'a, Message, Renderer> +where + 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)] + #[covariant] + overlay: Option<overlay::Element<'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(&overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.0 + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +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| { + let translation = position - overlay.position(); + + overlay.layout(renderer, bounds, translation) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .unwrap_or(event::Status::Ignored) + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, 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..0b8070af --- /dev/null +++ b/widget/src/lazy/component.rs @@ -0,0 +1,575 @@ +//! 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 ouroboros::self_referencing; +use std::cell::RefCell; +use std::marker::PhantomData; + +/// 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>, + ) { + } +} + +/// 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(), + )), + }) +} + +struct Instance<'a, Message, Renderer, Event, S> { + state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, +} + +#[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, +{ + fn rebuild_element(&self, state: &S) { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| Some(component.view(state)), + } + .build(), + ); + } + + fn rebuild_element_with_operation( + &self, + state: &mut S, + operation: &mut dyn widget::Operation<Message>, + ) { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + heads.component.operate(state, operation); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| Some(component.view(state)), + } + .build(), + ); + } + + 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.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 { + struct Tag<T>(T); + tree::Tag::of::<Tag<S>>() + } + + fn state(&self) -> tree::State { + tree::State::new(S::default()) + } + + fn children(&self) -> Vec<Tree> { + self.rebuild_element(&S::default()); + self.with_element(|element| vec![Tree::new(element)]) + } + + fn diff(&self, tree: &mut Tree) { + self.rebuild_element(tree.state.downcast_ref()); + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }) + } + + 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_position: Point, + 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_element_mut(|element| { + element.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + 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(tree.state.downcast_mut::<S>(), message) + }) { + shell.publish(message); + } + + self.state = RefCell::new(Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| { + Some(state.view(tree.state.downcast_ref::<S>())) + }, + } + .build(), + )); + + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }); + + 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( + tree.state.downcast_mut(), + 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); + } + } + + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)); + + element.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + &mut MapOperation { operation }, + ); + }); + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.with_element(|element| { + element.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + }); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_element(|element| { + element.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let overlay = OverlayBuilder { + 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, + ) + }, + ) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay.map(|position| { + overlay::Element::new( + position, + Box::new(OverlayInstance { + overlay: Some(overlay), + }), + ) + }) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer, Event, S> { + instance: &'a mut Instance<'b, Message, Renderer, Event, S>, + tree: &'a mut Tree, + types: PhantomData<(Message, Event, S)>, + + #[borrows(mut instance, mut tree)] + #[covariant] + overlay: Option<overlay::Element<'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(&overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_ref() + .unwrap() + .borrow_overlay() + .as_ref() + .map(f) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +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| { + let translation = position - overlay.position(); + + overlay.layout(renderer, bounds, translation) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: core::Event, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + renderer, + clipboard, + &mut local_shell, + ) + }) + .unwrap_or(event::Status::Ignored); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if !local_messages.is_empty() { + let overlay = self.overlay.take().unwrap().into_heads(); + let mut heads = overlay.instance.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(overlay.tree.state.downcast_mut::<S>(), message) + }) { + shell.publish(message); + } + + *overlay.instance.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| { + Some(state.view(overlay.tree.state.downcast_ref::<S>())) + }, + } + .build(), + ); + + overlay.instance.with_element(|element| { + overlay.tree.diff_children(std::slice::from_ref(&element)) + }); + + self.overlay = Some( + OverlayBuilder { + instance: overlay.instance, + tree: overlay.tree, + types: PhantomData, + overlay_builder: |_, _| None, + } + .build(), + ); + + shell.invalidate_layout(); + } + + event_status + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, 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..be60bb78 --- /dev/null +++ b/widget/src/lazy/helpers.rs @@ -0,0 +1,39 @@ +use crate::core::{self, Element, Size}; +use crate::lazy::component::{self, Component}; +use crate::lazy::{Lazy, Responsive}; + +use std::hash::Hash; + +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) +} + +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..7b2fc37c --- /dev/null +++ b/widget/src/lazy/responsive.rs @@ -0,0 +1,427 @@ +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 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: layout::Node::new(Size::ZERO), + element: Element::new(horizontal_space(0)), + }), + } + } +} + +struct Content<'a, Message, Renderer> { + size: Size, + layout: layout::Node, + element: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + fn update( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + 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; + + tree.diff(&self.element); + + self.layout = self + .element + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); + } + + 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, renderer.deref(), layout.bounds().size(), view); + + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + &self.layout, + ); + + 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_position: Point, + 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(); + + 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_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + 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, + renderer, + layout.bounds().size(), + &self.view, + ); + + let Content { + element, + layout: content_layout, + .. + } = content.deref_mut(); + + let content_layout = Layout::with_offset( + layout.bounds().position() - Point::ORIGIN, + content_layout, + ); + + element + .as_widget_mut() + .overlay(tree, content_layout, renderer) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::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)] + #[covariant] + overlay: Option<overlay::Element<'this, Message, Renderer>>, +} + +impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.borrow_overlay().as_ref().map(f) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +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| { + let translation = position - overlay.position(); + + overlay.layout(renderer, bounds, translation) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .unwrap_or(event::Status::Ignored) + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, cursor_position) + }) + .unwrap_or_default() + } +} diff --git a/widget/src/lib.rs b/widget/src/lib.rs new file mode 100644 index 00000000..4c1e7c1c --- /dev/null +++ b/widget/src/lib.rs @@ -0,0 +1,122 @@ +//! 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)] +pub use iced_native as native; +pub use iced_native::core; +pub use iced_renderer as renderer; +pub use iced_renderer::graphics; +pub use iced_style as style; + +mod column; +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 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/overlay.rs b/widget/src/overlay.rs new file mode 100644 index 00000000..b9a0e3e0 --- /dev/null +++ b/widget/src/overlay.rs @@ -0,0 +1 @@ +pub mod menu; diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs new file mode 100644 index 00000000..c322c8ba --- /dev/null +++ b/widget/src/overlay/menu.rs @@ -0,0 +1,519 @@ +//! 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, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + width: f32, + padding: Padding, + text_size: Option<f32>, + font: Option<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Renderer> Menu<'a, T, Renderer> +where + T: ToString + Clone, + 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>, + last_selection: &'a mut Option<T>, + ) -> Self { + Menu { + state, + options, + hovered_option, + last_selection, + width: 0.0, + padding: Padding::ZERO, + text_size: None, + 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 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<Message: 'a>( + 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, Renderer>, target_height: f32) -> Self + where + T: Clone + ToString, + { + let Menu { + state, + options, + hovered_option, + last_selection, + width, + padding, + font, + text_size, + style, + } = menu; + + let container = Container::new(Scrollable::new(List { + options, + hovered_option, + last_selection, + font, + text_size, + 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_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.container.on_event( + self.state, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.container.mouse_interaction( + self.state, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + 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.into(), + }, + appearance.background, + ); + + self.container.draw( + self.state, + renderer, + theme, + style, + layout, + cursor_position, + &bounds, + ); + } +} + +struct List<'a, T, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + padding: Padding, + text_size: Option<f32>, + font: Option<Renderer::Font>, + style: <Renderer::Theme as StyleSheet>::Style, +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for List<'a, T, 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 size = { + let intrinsic = Size::new( + 0.0, + (text_size * 1.2 + 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_position: Point, + renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + *self.last_selection = Some(option.clone()); + } + } + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let text_size = self + .text_size + .unwrap_or_else(|| renderer.default_size()); + + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / (text_size * 1.2 + self.padding.vertical())) + as usize, + ); + } + } + Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let text_size = self + .text_size + .unwrap_or_else(|| renderer.default_size()); + + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / (text_size * 1.2 + self.padding.vertical())) + as usize, + ); + + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + *self.last_selection = Some(option.clone()); + } + } + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + + 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_position: Point, + 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 = + (text_size * 1.2 + self.padding.vertical()) as usize; + + let offset = viewport.y - bounds.y; + let start = (offset / option_height as f32) as usize; + let end = + ((offset + viewport.height) / option_height as f32).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: text_size * 1.2 + self.padding.vertical(), + }; + + if is_selected { + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: Color::TRANSPARENT, + border_width: 0.0, + border_radius: appearance.border_radius.into(), + }, + 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, + 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, + }); + } + } +} + +impl<'a, T, Message, Renderer> From<List<'a, T, 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, 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..a97cef60 --- /dev/null +++ b/widget/src/pane_grid.rs @@ -0,0 +1,991 @@ +//! 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.8/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::{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 +/// +/// ``` +/// # 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_position: Point, + 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_position, + 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_position, + renderer, + clipboard, + shell, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref(), + self.contents.layout(), + layout, + cursor_position, + 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_position, + 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_position: Point, + viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref(), + self.contents.layout(), + layout, + cursor_position, + 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_position, + rectangle| { + content.draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + 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_position: Point, + 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 bounds.contains(cursor_position) { + 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 { + let mut dropped_region = contents + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != target => { + DragEvent::Dropped { pane, target } + } + _ => DragEvent::Canceled { pane }, + }; + + shell.publish(on_drag(event)); + } + + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } + } + 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) { + 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 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_position: Point, + 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 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_position: Point, + 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<'_>, + Point, + &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 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_position = if picked_pane.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let mut render_picked_pane = None; + + for ((id, pane), layout) in contents.zip(layout.children()) { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + render_picked_pane = Some((pane, origin, layout)); + } + _ => { + draw_pane( + pane, + renderer, + default_style, + layout, + pane_cursor_position, + viewport, + ); + } + } + } + + // Render picked pane last + if let Some((pane, origin, layout)) = render_picked_pane { + 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_position, + viewport, + ); + }); + }, + ); + }; + + 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, + ); + } + } +} + +/// 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 [`Pane`] where the picked one was dropped on. + target: Pane, + }, + + /// A [`Pane`] was picked and then dropped outside of other [`Pane`] + /// boundaries. + Canceled { + /// The picked [`Pane`]. + pane: Pane, + }, +} + +/// 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..035ef05b --- /dev/null +++ b/widget/src/pane_grid/content.rs @@ -0,0 +1,373 @@ +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_position: Point, + 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 = bounds.contains(cursor_position); + + self.body.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + body_layout, + cursor_position, + viewport, + ); + + title_bar.draw( + &tree.children[1], + renderer, + theme, + style, + title_bar_layout, + cursor_position, + viewport, + show_controls, + ); + } else { + self.body.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + 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_position: Point, + 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_position, + 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_position, + renderer, + clipboard, + shell, + ) + }; + + event_status.merge(body_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + 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 = title_bar + .is_over_pick_area(title_bar_layout, cursor_position); + + 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_position, + viewport, + renderer, + ); + + (children.next().unwrap(), mouse_interaction) + } else { + (layout, mouse::Interaction::default()) + }; + + self.body + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor_position, + 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..a9274dad --- /dev/null +++ b/widget/src/pane_grid/draggable.rs @@ -0,0 +1,12 @@ +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_position: Point, + ) -> bool; +} diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs new file mode 100644 index 00000000..3976acd8 --- /dev/null +++ b/widget/src/pane_grid/node.rs @@ -0,0 +1,250 @@ +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 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..a6e2ec7f --- /dev/null +++ b/widget/src/pane_grid/state.rs @@ -0,0 +1,348 @@ +//! The state of a [`PaneGrid`]. +//! +//! [`PaneGrid`]: crate::widget::PaneGrid +use crate::core::{Point, Size}; +use crate::pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}; + +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)> { + let node = self.internal.layout.find(pane)?; + + 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) + }; + + node.split(new_split, axis, new_pane); + + let _ = self.panes.insert(new_pane, state); + let _ = self.maximized.take(); + + Some((new_pane, new_split)) + } + + /// 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..2129937b --- /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_position: Point, + 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_position, + viewport, + ); + } + } + + if show_title { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &inherited_style, + title_layout, + cursor_position, + 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_position: Point, + 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_position, + 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_position, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; + + control_status.merge(title_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + 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_position, + 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..cd23cdd2 --- /dev/null +++ b/widget/src/pick_list.rs @@ -0,0 +1,658 @@ +//! 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, Point, 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>, + 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, + 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 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<T>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::<T>::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.font, + self.placeholder.as_deref(), + &self.options, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::<State<T>>(), + ) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let font = self.font.unwrap_or_else(|| renderer.default_font()); + draw( + renderer, + theme, + layout, + cursor_position, + self.padding, + self.text_size, + font, + self.placeholder.as_deref(), + self.selected.as_ref(), + &self.handle, + &self.style, + || tree.state.downcast_ref::<State<T>>(), + ) + } + + 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<T>>(); + + overlay( + layout, + state, + self.padding, + self.text_size, + self.font.unwrap_or_else(|| renderer.default_font()), + &self.options, + 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<T> { + menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, + is_open: bool, + hovered_option: Option<usize>, + last_selection: Option<T>, +} + +impl<T> State<T> { + /// 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(), + last_selection: Option::default(), + } + } +} + +impl<T> Default for State<T> { + 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>, +} + +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option<f32>, + 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( + label, + text_size, + font.unwrap_or_else(|| renderer.default_font()), + Size::new(f32::INFINITY, f32::INFINITY), + ); + + 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, text_size * 1.2); + + 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_position: Point, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State<T>, +) -> 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(); + + let event_status = 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 layout.bounds().contains(cursor_position) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = state.last_selection.take() { + shell.publish((on_selected)(last_selection)); + + state.is_open = false; + + event::Status::Captured + } else { + event_status + } + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() + && layout.bounds().contains(cursor_position) + && !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_position: Point, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + 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<T>, + padding: Padding, + text_size: Option<f32>, + font: Renderer::Font, + options: &'a [T], + 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, + &mut state.last_selection, + ) + .width(bounds.width) + .padding(padding) + .font(font) + .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_position: Point, + padding: Padding, + text_size: Option<f32>, + font: Renderer::Font, + placeholder: Option<&str>, + selected: Option<&T>, + handle: &Handle<Renderer::Font>, + style: &<Renderer::Theme as StyleSheet>::Style, + state: impl FnOnce() -> &'a State<T>, +) where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, + T: ToString + 'a, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + 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.into(), + }, + style.background, + ); + + let handle = match handle { + Handle::Arrow { size } => { + Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size)) + } + Handle::Static(Icon { + font, + code_point, + size, + }) => Some((*font, *code_point, *size)), + Handle::Dynamic { open, closed } => { + if state().is_open { + Some((open.font, open.code_point, open.size)) + } else { + Some((closed.font, closed.code_point, closed.size)) + } + } + Handle::None => None, + }; + + if let Some((font, code_point, size)) = handle { + let size = size.unwrap_or_else(|| renderer.default_size()); + + renderer.fill_text(Text { + content: &code_point.to_string(), + size, + font, + color: style.handle_color, + bounds: Rectangle { + x: bounds.x + bounds.width - padding.horizontal(), + y: bounds.center_y(), + height: size * 1.2, + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + }); + } + + 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, + 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: text_size * 1.2, + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + } +} diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs new file mode 100644 index 00000000..896d40fe --- /dev/null +++ b/widget/src/progress_bar.rs @@ -0,0 +1,172 @@ +//! Provide progress feedback to your users. +use crate::core::layout; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Widget, +}; + +use std::ops::RangeInclusive; + +pub use iced_style::progress_bar::{Appearance, StyleSheet}; + +/// A bar that displays progress. +/// +/// # Example +/// ``` +/// # 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_position: Point, + _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.into(), + 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.into(), + 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..7709125f --- /dev/null +++ b/widget/src/qr_code.rs @@ -0,0 +1,297 @@ +//! Encode and display information in a QR code. +use crate::canvas; +use crate::core::layout; +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::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_position: Point, + _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_primitive(geometry.0); + }); + } +} + +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..e5c67f3d --- /dev/null +++ b/widget/src/radio.rs @@ -0,0 +1,300 @@ +//! 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, Point, + Rectangle, Shell, Widget, +}; +use crate::{Row, Text}; + +pub use iced_style::radio::{Appearance, StyleSheet}; + +/// A circular button representing a choice. +/// +/// # Example +/// ``` +/// # type Radio<Message> = +/// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; +/// # +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// pub enum Choice { +/// A, +/// B, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// pub enum Message { +/// RadioSelected(Choice), +/// } +/// +/// let selected_choice = Some(Choice::A); +/// +/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected); +/// +/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected); +/// ``` +/// +///  +#[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>, + 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>( + value: V, + label: impl Into<String>, + 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, + 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 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()), + )) + .layout(renderer, limits) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _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 layout.bounds().contains(cursor_position) { + shell.publish(self.on_click.clone()); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + 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.font, + crate::text::Appearance { + color: custom_style.text_color, + }, + alignment::Horizontal::Left, + alignment::Vertical::Center, + ); + } + } +} + +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..3ce363f8 --- /dev/null +++ b/widget/src/row.rs @@ -0,0 +1,253 @@ +//! 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, Point, 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_position: Point, + 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_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + 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_position, + 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..3749d7ce --- /dev/null +++ b/widget/src/rule.rs @@ -0,0 +1,147 @@ +//! Display a horizontal or vertical rule for dividing content. +use crate::core::layout; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{ + Color, Element, Layout, Length, Pixels, Point, 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_position: Point, + _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.into(), + 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..49c780de --- /dev/null +++ b/widget/src/scrollable.rs @@ -0,0 +1,1325 @@ +//! 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::native::Command; + +pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; +pub use operation::scrollable::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>, + height: Length, + vertical: Properties, + horizontal: Option<Properties>, + content: Element<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(RelativeOffset) -> 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, + height: Length::Shrink, + vertical: Properties::default(), + horizontal: None, + 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 height of the [`Scrollable`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Configures the vertical scrollbar of the [`Scrollable`] . + pub fn vertical_scroll(mut self, properties: Properties) -> Self { + self.vertical = properties; + self + } + + /// Configures the horizontal scrollbar of the [`Scrollable`] . + pub fn horizontal_scroll(mut self, properties: Properties) -> Self { + self.horizontal = Some(properties); + self + } + + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the new relative x & y offset of the [`Scrollable`] + /// (e.g. `0` means beginning, while `1` means end). + pub fn on_scroll( + mut self, + f: impl Fn(RelativeOffset) -> 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 + } +} + +/// Properties of a scrollbar within a [`Scrollable`]. +#[derive(Debug)] +pub struct Properties { + width: f32, + margin: f32, + scroller_width: f32, +} + +impl Default for Properties { + fn default() -> Self { + Self { + width: 10.0, + margin: 0.0, + scroller_width: 10.0, + } + } +} + +impl Properties { + /// Creates new [`Properties`] for use in a [`Scrollable`]. + pub fn new() -> Self { + Self::default() + } + + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum width of 1. + pub fn width(mut self, width: impl Into<Pixels>) -> Self { + self.width = width.into().0.max(1.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`] . + /// Silently enforces a minimum width of 1. + pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { + self.scroller_width = scroller_width.into().0.max(1.0); + self + } +} + +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.content.as_widget().width() + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::<Message, Renderer>::width(self), + self.height, + self.horizontal.is_some(), + |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_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + tree.state.downcast_mut::<State>(), + event, + layout, + cursor_position, + clipboard, + shell, + &self.vertical, + self.horizontal.as_ref(), + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::<State>(), + renderer, + theme, + layout, + cursor_position, + &self.vertical, + self.horizontal.as_ref(), + &self.style, + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref::<State>(), + layout, + cursor_position, + &self.vertical, + self.horizontal.as_ref(), + |layout, cursor_position, viewport| { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + 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 offset = tree + .state + .downcast_ref::<State>() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(-offset.x, -offset.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)) +} + +/// Computes the layout of a [`Scrollable`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + horizontal_enabled: bool, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .max_height(f32::INFINITY) + .max_width(if horizontal_enabled { + f32::INFINITY + } else { + limits.max().width + }) + .width(width) + .height(height); + + let child_limits = layout::Limits::new( + Size::new(limits.min().width, 0.0), + Size::new( + if horizontal_enabled { + f32::INFINITY + } else { + limits.max().width + }, + f32::MAX, + ), + ); + + 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_position: Point, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + vertical: &Properties, + horizontal: Option<&Properties>, + on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>, + update_content: impl FnOnce( + Event, + Layout<'_>, + Point, + &mut dyn Clipboard, + &mut Shell<'_, Message>, + ) -> event::Status, +) -> event::Status { + let bounds = layout.bounds(); + let mouse_over_scrollable = bounds.contains(cursor_position); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); + + let event_status = { + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + cursor_position + state.offset(bounds, content_bounds) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(-1.0, -1.0) + }; + + update_content( + event.clone(), + content, + cursor_position, + 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; + } + + if mouse_over_scrollable { + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + 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, 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 { .. } => { + state.scroll_area_touched_at = Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); + + state.scroll(delta, 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 { + 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 { .. }) => { + 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 { .. }) => { + 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 { .. }) => { + 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_position: Point, + vertical: &Properties, + horizontal: Option<&Properties>, + content_interaction: impl FnOnce( + Layout<'_>, + Point, + &Rectangle, + ) -> mouse::Interaction, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let mouse_over_scrollable = bounds.contains(cursor_position); + + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); + + if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) + || state.scrollers_grabbed() + { + mouse::Interaction::Idle + } else { + let offset = state.offset(bounds, content_bounds); + + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + cursor_position + offset + } else { + Point::new(-1.0, -1.0) + }; + + content_interaction( + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset.y, + x: bounds.x + offset.x, + ..bounds + }, + ) + } +} + +/// Draws a [`Scrollable`]. +pub fn draw<Renderer>( + state: &State, + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor_position: Point, + vertical: &Properties, + horizontal: Option<&Properties>, + style: &<Renderer::Theme as StyleSheet>::Style, + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &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, vertical, horizontal, bounds, content_bounds); + + let mouse_over_scrollable = bounds.contains(cursor_position); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); + + let offset = state.offset(bounds, content_bounds); + + let cursor_position = if mouse_over_scrollable + && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) + { + cursor_position + offset + } else { + Point::new(-1.0, -1.0) + }; + + // Draw inner content + if scrollbars.active() { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(-offset.x, -offset.y), + |renderer| { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset.y, + x: bounds.x + offset.x, + ..bounds + }, + ); + }, + ); + }); + + let draw_scrollbar = + |renderer: &mut Renderer, + style: Scrollbar, + scrollbar: &internals::Scrollbar| { + //track + if 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.into(), + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + //thumb + if 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.into(), + 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 mouse_over_y_scrollbar { + theme.hovered(style) + } 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 mouse_over_x_scrollbar { + theme.hovered_horizontal(style) + } else { + theme.active_horizontal(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } + }, + ); + } else { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + x: bounds.x + offset.x, + y: bounds.y + offset.y, + ..bounds + }, + ); + } +} + +fn notify_on_scroll<Message>( + state: &State, + on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> 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 x = state.offset_x.absolute(bounds.width, content_bounds.width) + / (content_bounds.width - bounds.width); + + let y = state + .offset_y + .absolute(bounds.height, content_bounds.height) + / (content_bounds.height - bounds.height); + + shell.publish(on_scroll(RelativeOffset { x, y })) + } +} + +/// 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, +} + +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(), + } + } +} + +impl operation::Scrollable for State { + fn snap_to(&mut self, offset: RelativeOffset) { + State::snap_to(self, offset); + } +} + +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, window: f32, content: f32) -> f32 { + match self { + Offset::Absolute(absolute) => { + absolute.min((content - window).max(0.0)) + } + Offset::Relative(percentage) => { + ((content - window) * percentage).max(0.0) + } + } + } +} + +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>, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + 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)); + } + + /// 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 offset of the [`State`], given the bounds of the + /// [`Scrollable`] and its contents. + pub fn offset( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Vector { + Vector::new( + self.offset_x.absolute(bounds.width, content_bounds.width), + self.offset_y.absolute(bounds.height, content_bounds.height), + ) + } + + /// 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, + vertical: &Properties, + horizontal: Option<&Properties>, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Self { + let offset = state.offset(bounds, content_bounds); + + let show_scrollbar_x = horizontal.and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); + + let y_scrollbar = if content_bounds.height > bounds.height { + 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 = offset.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, + }, + }) + } 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 = y_scrollbar.map_or(0.0, |_| { + vertical.width.max(vertical.scroller_width) + vertical.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 = offset.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, + }, + }) + } else { + None + }; + + Self { + y: y_scrollbar, + x: x_scrollbar, + } + } + + fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { + ( + 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), + ) + } + + 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}; + + /// The scrollbar of a [`Scrollable`]. + #[derive(Debug, Copy, Clone)] + pub struct Scrollbar { + /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, + /// and the scrollbar margin. + pub total_bounds: Rectangle, + + /// The bounds of just the [`Scrollbar`]. + pub bounds: Rectangle, + + /// The state of this scrollbar's [`Scroller`]. + pub scroller: Scroller, + } + + 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 { + if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + // cursor position is unavailable! Set to either end or beginning of scrollbar depending + // on where the thumb currently is in the track + (self.scroller.bounds.y / self.total_bounds.height).round() + } else { + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) + } + } + + /// Returns the x-axis scrolled percentage from the cursor position. + pub fn scroll_percentage_x( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + (self.scroller.bounds.x / self.total_bounds.width).round() + } else { + (cursor_position.x + - self.bounds.x + - self.scroller.bounds.width * grabbed_at) + / (self.bounds.width - self.scroller.bounds.width) + } + } + } + + /// 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..70a84fc6 --- /dev/null +++ b/widget/src/slider.rs @@ -0,0 +1,471 @@ +//! 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::{ + Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Widget, +}; + +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Appearance, Handle, HandleShape, 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 +/// ``` +/// # 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_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + 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_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + &self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + layout, + cursor_position, + 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_position: Point, + 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 = || { + 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 layout.bounds().contains(cursor_position) { + change(); + 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 { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T, R>( + renderer: &mut R, + layout: Layout<'_>, + cursor_position: Point, + 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 = bounds.contains(cursor_position); + + 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 rail_y = bounds.y + (bounds.height / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - 1.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 1.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), + 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 handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius.into(), + 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_position: Point, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + 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..e1e09d5a --- /dev/null +++ b/widget/src/space.rs @@ -0,0 +1,86 @@ +//! Distribute content vertically. +use crate::core; +use crate::core::layout; +use crate::core::renderer; +use crate::core::widget::Tree; +use crate::core::{Element, Layout, Length, Point, 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_position: Point, + _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..89017fcf --- /dev/null +++ b/widget/src/svg.rs @@ -0,0 +1,195 @@ +//! Display vector graphics in your application. +use crate::core::layout; +use crate::core::renderer; +use crate::core::svg; +use crate::core::widget::Tree; +use crate::core::{ + ContentFit, Element, Layout, Length, Point, 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_position: Point, + _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..04c31edc --- /dev/null +++ b/widget/src/text.rs @@ -0,0 +1,4 @@ +pub use crate::core::widget::text::*; + +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..67d80e2b --- /dev/null +++ b/widget/src/text_input.rs @@ -0,0 +1,1221 @@ +//! 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::native::Command; + +pub use iced_style::text_input::{Appearance, StyleSheet}; + +/// A field that can be filled with text. +/// +/// # Example +/// ``` +/// # 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, +/// 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>, + on_change: Box<dyn Fn(String) -> Message + 'a>, + on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, + on_submit: Option<Message>, + 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, and + /// - a function that produces a message when the [`TextInput`] changes. + pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self + where + F: 'a + Fn(String) -> Message, + { + 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, + on_change: Box::new(on_change), + on_paste: None, + on_submit: 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 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 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 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 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_position: Point, + value: Option<&Value>, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + value.unwrap_or(&self.value), + &self.placeholder, + self.size, + self.font, + self.is_secure, + &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 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) + } + + 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_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + self.font, + self.is_secure, + self.on_change.as_ref(), + 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_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + &self.value, + &self.placeholder, + self.size, + self.font, + self.is_secure, + &self.style, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } +} + +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 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>, +) -> 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(text_size * 1.2); + + let mut text = layout::Node::new(limits.resolve(Size::ZERO)); + text.move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(text.size().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_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + value: &mut Value, + size: Option<f32>, + font: Option<Renderer::Font>, + is_secure: bool, + on_change: &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 is_clicked = layout.bounds().contains(cursor_position); + + state.is_focused = if is_clicked { + state.is_focused.or_else(|| { + let now = Instant::now(); + + Some(Focus { + updated_at: now, + now, + }) + }) + } else { + None + }; + + if is_clicked { + 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, + &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, + 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, + &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 { + 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_change)(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 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_change)(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_change)(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_change)(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_change)(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_position: Point, + state: &State, + value: &Value, + placeholder: &str, + size: Option<f32>, + font: Option<Renderer::Font>, + is_secure: bool, + 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 text_bounds = layout.children().next().unwrap().bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + let appearance = 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.into(), + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + + 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, + ); + + let render = |renderer: &mut Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); + } + + renderer.fill_text(Text { + content: if text.is_empty() { placeholder } else { &text }, + color: if text.is_empty() { + theme.placeholder_color(style) + } else { + theme.value_color(style) + }, + font, + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + }; + + 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_position: Point, +) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + 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); + + 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>, + 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, + font, + Size::INFINITY, + 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..f23f9b27 --- /dev/null +++ b/widget/src/toggler.rs @@ -0,0 +1,326 @@ +//! 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::widget::Tree; +use crate::core::{ + Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, + Rectangle, Shell, Widget, +}; +use crate::{Row, Text}; + +pub use crate::style::toggler::{Appearance, StyleSheet}; + +/// A toggler widget. +/// +/// # Example +/// +/// ``` +/// # 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_alignment: alignment::Horizontal, + 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_alignment: alignment::Horizontal::Left, + 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 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 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()), + ), + ); + } + + 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_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = layout.bounds().contains(cursor_position); + + 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_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _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.font, + Default::default(), + self.text_alignment, + alignment::Vertical::Center, + ); + } + + let toggler_layout = children.next().unwrap(); + let bounds = toggler_layout.bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + 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..084650d1 --- /dev/null +++ b/widget/src/tooltip.rs @@ -0,0 +1,388 @@ +//! Display a widget over another. +use crate::container; +use crate::core; +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::Tree; +use crate::core::{ + Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Vector, Widget, +}; +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<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 on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + 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, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + ); + + let tooltip = &self.tooltip; + + draw( + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + self.position, + self.gap, + self.padding, + self.snap_within_viewport, + &self.style, + |renderer, limits| { + Widget::<(), Renderer>::layout(tooltip, renderer, limits) + }, + |renderer, defaults, layout, cursor_position, viewport| { + Widget::<(), Renderer>::draw( + tooltip, + &Tree::empty(), + renderer, + theme, + defaults, + layout, + cursor_position, + 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<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, +} + +/// Draws a [`Tooltip`]. +pub fn draw<Renderer>( + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + position: Position, + gap: f32, + padding: f32, + snap_within_viewport: bool, + style: &<Renderer::Theme as container::StyleSheet>::Style, + layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, + draw_text: impl FnOnce( + &mut Renderer, + &renderer::Style, + Layout<'_>, + Point, + &Rectangle, + ), +) where + Renderer: core::Renderer, + Renderer::Theme: container::StyleSheet, +{ + use container::StyleSheet; + + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let style = theme.appearance(style); + + let defaults = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + let text_layout = layout_text( + renderer, + &layout::Limits::new( + Size::ZERO, + snap_within_viewport + .then(|| viewport.size()) + .unwrap_or(Size::INFINITY), + ) + .pad(Padding::new(padding)), + ); + + let text_bounds = text_layout.bounds(); + let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0; + let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0; + + let mut tooltip_bounds = { + let offset = match position { + Position::Top => Vector::new( + x_center, + bounds.y - text_bounds.height - gap - padding, + ), + Position::Bottom => Vector::new( + x_center, + bounds.y + bounds.height + gap + padding, + ), + Position::Left => Vector::new( + bounds.x - text_bounds.width - gap - padding, + y_center, + ), + Position::Right => Vector::new( + bounds.x + bounds.width + gap + padding, + y_center, + ), + Position::FollowCursor => Vector::new( + cursor_position.x, + cursor_position.y - text_bounds.height, + ), + }; + + Rectangle { + x: offset.x - padding, + y: offset.y - padding, + width: text_bounds.width + padding * 2.0, + height: text_bounds.height + padding * 2.0, + } + }; + + if 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; + } + } + + renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| { + container::draw_background(renderer, &style, tooltip_bounds); + + draw_text( + renderer, + &defaults, + Layout::with_offset( + Vector::new( + tooltip_bounds.x + padding, + tooltip_bounds.y + padding, + ), + &text_layout, + ), + cursor_position, + viewport, + ) + }); + } +} diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs new file mode 100644 index 00000000..64a9583c --- /dev/null +++ b/widget/src/vertical_slider.rs @@ -0,0 +1,471 @@ +//! 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::{ + Background, 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 +/// ``` +/// # 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_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + 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_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + &self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + layout, + cursor_position, + 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_position: Point, + 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 = || { + 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 layout.bounds().contains(cursor_position) { + change(); + 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 { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`VerticalSlider`]. +pub fn draw<T, R>( + renderer: &mut R, + layout: Layout<'_>, + cursor_position: Point, + 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 = bounds.contains(cursor_position); + + 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 rail_x = bounds.x + (bounds.width / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - 1.0, + y: bounds.y, + width: 2.0, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x + 1.0, + y: bounds.y, + width: 2.0, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), + 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 handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.height - handle_width) * (value - range_end) + / (range_start - range_end) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - (handle_height / 2.0), + y: bounds.y + handle_offset.round(), + width: handle_height, + height: handle_width, + }, + border_radius: handle_border_radius.into(), + 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_position: Point, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + 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() + } +} -- cgit From cfb8abb6f5806e08ccc3a80233e1fb1768adeaf7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Sun, 5 Mar 2023 04:19:31 +0100 Subject: Use `no_run` for widget doc-tests --- widget/src/button.rs | 2 +- widget/src/checkbox.rs | 2 +- widget/src/image.rs | 2 +- widget/src/pane_grid.rs | 2 +- widget/src/progress_bar.rs | 2 +- widget/src/radio.rs | 2 +- widget/src/slider.rs | 2 +- widget/src/text_input.rs | 2 +- widget/src/toggler.rs | 2 +- widget/src/vertical_slider.rs | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) (limited to 'widget/src') diff --git a/widget/src/button.rs b/widget/src/button.rs index d6fd3997..7eee69cb 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -18,7 +18,7 @@ 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>>; /// # diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 6a062f94..d1f886c6 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -30,7 +30,7 @@ pub struct Icon<Font> { /// /// # Example /// -/// ``` +/// ```no_run /// # type Checkbox<'a, Message> = /// # iced_widget::Checkbox<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; /// # diff --git a/widget/src/image.rs b/widget/src/image.rs index 22a3a1a1..abcb6ef2 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -23,7 +23,7 @@ pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { /// /// # Example /// -/// ``` +/// ```no_run /// # use iced_widget::image::{self, Image}; /// # /// let image = Image::<image::Handle>::new("resources/ferris.png"); diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index a97cef60..257c0144 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -67,7 +67,7 @@ use crate::core::{ /// /// ## Example /// -/// ``` +/// ```no_run /// # use iced_widget::{pane_grid, text}; /// # /// # type PaneGrid<'a, Message> = diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 896d40fe..ef0d87d5 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -13,7 +13,7 @@ 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>>; /// # diff --git a/widget/src/radio.rs b/widget/src/radio.rs index e5c67f3d..c2b6b017 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -18,7 +18,7 @@ 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>>; /// # diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 70a84fc6..e1153d2d 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -25,7 +25,7 @@ pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; /// 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>>; /// # diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 67d80e2b..d1c48fbd 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -35,7 +35,7 @@ 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>>; /// # diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index f23f9b27..713a9c30 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -18,7 +18,7 @@ pub use crate::style::toggler::{Appearance, StyleSheet}; /// /// # Example /// -/// ``` +/// ```no_run /// # type Toggler<'a, Message> = /// # iced_widget::Toggler<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; /// # diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 64a9583c..62dc997f 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -26,7 +26,7 @@ use crate::core::{ /// 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>>; /// # -- cgit From 99e0a71504456976ba88040f5d1d3bbc347694ea Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Sun, 5 Mar 2023 06:35:20 +0100 Subject: Rename `iced_native` to `iced_runtime` --- widget/src/helpers.rs | 2 +- widget/src/lib.rs | 4 ++-- widget/src/scrollable.rs | 2 +- widget/src/text_input.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) (limited to 'widget/src') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1a73c16f..a43e7248 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -5,12 +5,12 @@ use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; -use crate::native::Command; 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}; diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 4c1e7c1c..a3e7c8bc 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -14,10 +14,10 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] -pub use iced_native as native; -pub use iced_native::core; 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; diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 49c780de..5a7481f7 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -13,7 +13,7 @@ use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; -use crate::native::Command; +use crate::runtime::Command; pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; pub use operation::scrollable::RelativeOffset; diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index d1c48fbd..d066109a 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -28,7 +28,7 @@ use crate::core::{ Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; -use crate::native::Command; +use crate::runtime::Command; pub use iced_style::text_input::{Appearance, StyleSheet}; -- cgit From 0f7abffc0e94b4bb9f8117db633bfd07d900eb93 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 22 Mar 2023 00:36:57 +0100 Subject: Draft (very) basic incremental rendering for `iced_tiny_skia` --- widget/src/text_input.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'widget/src') diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index d066109a..4f018284 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -953,6 +953,8 @@ pub fn draw<Renderer>( 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 { -- cgit From 33b5a900197e2798a393d6d9a0834039666eddbb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 19 Apr 2023 01:19:56 +0200 Subject: Make basic text shaping the default shaping strategy --- widget/src/checkbox.rs | 2 ++ widget/src/overlay/menu.rs | 1 + widget/src/pick_list.rs | 3 +++ widget/src/radio.rs | 1 + widget/src/text_input.rs | 7 ++++++- widget/src/toggler.rs | 1 + 6 files changed, 14 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 6505cfdd..e28f76af 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -273,6 +273,7 @@ where color: custom_style.icon_color, horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, + advanced_shape: true, }); } } @@ -292,6 +293,7 @@ where }, alignment::Horizontal::Left, alignment::Vertical::Center, + false, ); } } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index c322c8ba..c904730d 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -500,6 +500,7 @@ where }, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, + advanced_shape: false, }); } } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index cd23cdd2..d44f4cae 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -366,6 +366,7 @@ where text_size, font.unwrap_or_else(|| renderer.default_font()), Size::new(f32::INFINITY, f32::INFINITY), + false, ); width.round() @@ -628,6 +629,7 @@ pub fn draw<'a, T, Renderer>( }, horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, + advanced_shape: false, }); } @@ -653,6 +655,7 @@ pub fn draw<'a, T, Renderer>( }, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, + advanced_shape: false, }); } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index c3229aed..b685c1a1 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -309,6 +309,7 @@ where }, alignment::Horizontal::Left, alignment::Vertical::Center, + false, ); } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 9db382f7..abf858ca 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -463,6 +463,7 @@ where &icon.code_point.to_string(), icon.size.unwrap_or_else(|| renderer.default_size()), icon.font, + true, ); let mut text_node = layout::Node::new( @@ -975,6 +976,7 @@ pub fn draw<Renderer>( bounds: icon_layout.bounds(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, + advanced_shape: true, }); } @@ -1079,6 +1081,7 @@ pub fn draw<Renderer>( if text.is_empty() { placeholder } else { &text }, size, font, + true, ); let render = |renderer: &mut Renderer| { @@ -1106,6 +1109,7 @@ pub fn draw<Renderer>( size, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, + advanced_shape: true, }); }; @@ -1311,7 +1315,7 @@ where let text_before_cursor = value.until(cursor_index).to_string(); let text_value_width = - renderer.measure_width(&text_before_cursor, size, font); + renderer.measure_width(&text_before_cursor, size, font, true); let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); @@ -1346,6 +1350,7 @@ where Size::INFINITY, Point::new(x + offset, text_bounds.height / 2.0), true, + true, ) .map(text::Hit::cursor)?; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 713a9c30..d3033ddb 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -249,6 +249,7 @@ where Default::default(), self.text_alignment, alignment::Vertical::Center, + false, ); } -- cgit From 4bd290afe7d81d9aaf7467b3ce91491f6600261a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 19 Apr 2023 02:00:45 +0200 Subject: Introduce `text::Shaping` enum and replace magic boolean --- widget/src/checkbox.rs | 19 ++++++++++++++++--- widget/src/overlay/menu.rs | 13 ++++++++++++- widget/src/pick_list.rs | 46 ++++++++++++++++++++++++++++++++++++---------- widget/src/radio.rs | 22 ++++++++++++++++++---- widget/src/text.rs | 1 + widget/src/text_input.rs | 18 +++++++++++------- widget/src/toggler.rs | 13 +++++++++++-- 7 files changed, 105 insertions(+), 27 deletions(-) (limited to 'widget/src') diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index e28f76af..2a09b8fd 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -46,6 +46,7 @@ where size: f32, spacing: f32, text_size: Option<f32>, + text_shaping: text::Shaping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, style: <Renderer::Theme as StyleSheet>::Style, @@ -82,11 +83,13 @@ where size: Self::DEFAULT_SIZE, spacing: Self::DEFAULT_SPACING, text_size: None, + text_shaping: text::Shaping::Basic, font: None, icon: Icon { font: Renderer::ICON_FONT, code_point: Renderer::CHECKMARK_ICON, size: None, + shaping: text::Shaping::Basic, }, style: Default::default(), } @@ -116,6 +119,12 @@ where 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 @@ -171,7 +180,8 @@ where .size( self.text_size .unwrap_or_else(|| renderer.default_size()), - ), + ) + .shaping(self.text_shaping), ) .layout(renderer, limits) } @@ -257,6 +267,7 @@ where font, code_point, size, + shaping, } = &self.icon; let size = size.unwrap_or(bounds.height * 0.7); @@ -273,7 +284,7 @@ where color: custom_style.icon_color, horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, - advanced_shape: true, + shaping: *shaping, }); } } @@ -293,7 +304,7 @@ where }, alignment::Horizontal::Left, alignment::Vertical::Center, - false, + self.text_shaping, ); } } @@ -322,4 +333,6 @@ pub struct Icon<Font> { pub code_point: char, /// Font size of the content. pub size: Option<f32>, + /// The shaping strategy of the icon. + pub shaping: text::Shaping, } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index c904730d..7de3cbae 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -31,6 +31,7 @@ where width: f32, padding: Padding, text_size: Option<f32>, + text_shaping: text::Shaping, font: Option<Renderer::Font>, style: <Renderer::Theme as StyleSheet>::Style, } @@ -58,6 +59,7 @@ where width: 0.0, padding: Padding::ZERO, text_size: None, + text_shaping: text::Shaping::Basic, font: None, style: Default::default(), } @@ -81,6 +83,12 @@ where 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()); @@ -168,6 +176,7 @@ where padding, font, text_size, + text_shaping, style, } = menu; @@ -177,6 +186,7 @@ where last_selection, font, text_size, + text_shaping, padding, style: style.clone(), })); @@ -311,6 +321,7 @@ where last_selection: &'a mut Option<T>, padding: Padding, text_size: Option<f32>, + text_shaping: text::Shaping, font: Option<Renderer::Font>, style: <Renderer::Theme as StyleSheet>::Style, } @@ -500,7 +511,7 @@ where }, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, - advanced_shape: false, + shaping: self.text_shaping, }); } } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index d44f4cae..c0cb2946 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -36,6 +36,7 @@ where width: Length, padding: Padding, text_size: Option<f32>, + text_shaping: text::Shaping, font: Option<Renderer::Font>, handle: Handle<Renderer::Font>, style: <Renderer::Theme as StyleSheet>::Style, @@ -71,6 +72,7 @@ where width: Length::Shrink, padding: Self::DEFAULT_PADDING, text_size: None, + text_shaping: text::Shaping::Basic, font: None, handle: Default::default(), style: Default::default(), @@ -101,6 +103,12 @@ where 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()); @@ -164,6 +172,7 @@ where self.width, self.padding, self.text_size, + self.text_shaping, self.font, self.placeholder.as_deref(), &self.options, @@ -221,6 +230,7 @@ where cursor_position, self.padding, self.text_size, + self.text_shaping, font, self.placeholder.as_deref(), self.selected.as_ref(), @@ -243,6 +253,7 @@ where state, self.padding, self.text_size, + self.text_shaping, self.font.unwrap_or_else(|| renderer.default_font()), &self.options, self.style.clone(), @@ -336,6 +347,8 @@ pub struct Icon<Font> { pub code_point: char, /// Font size of the content. pub size: Option<f32>, + /// The shaping strategy of the icon. + pub shaping: text::Shaping, } /// Computes the layout of a [`PickList`]. @@ -345,6 +358,7 @@ pub fn layout<Renderer, T>( width: Length, padding: Padding, text_size: Option<f32>, + text_shaping: text::Shaping, font: Option<Renderer::Font>, placeholder: Option<&str>, options: &[T], @@ -366,7 +380,7 @@ where text_size, font.unwrap_or_else(|| renderer.default_font()), Size::new(f32::INFINITY, f32::INFINITY), - false, + text_shaping, ); width.round() @@ -516,6 +530,7 @@ pub fn overlay<'a, T, Message, Renderer>( state: &'a mut State<T>, padding: Padding, text_size: Option<f32>, + text_shaping: text::Shaping, font: Renderer::Font, options: &'a [T], style: <Renderer::Theme as StyleSheet>::Style, @@ -543,6 +558,7 @@ where .width(bounds.width) .padding(padding) .font(font) + .text_shaping(text_shaping) .style(style); if let Some(text_size) = text_size { @@ -563,6 +579,7 @@ pub fn draw<'a, T, Renderer>( cursor_position: Point, padding: Padding, text_size: Option<f32>, + text_shaping: text::Shaping, font: Renderer::Font, placeholder: Option<&str>, selected: Option<&T>, @@ -595,25 +612,34 @@ pub fn draw<'a, T, Renderer>( ); let handle = match handle { - Handle::Arrow { size } => { - Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size)) - } + Handle::Arrow { size } => Some(( + Renderer::ICON_FONT, + Renderer::ARROW_DOWN_ICON, + *size, + text::Shaping::Basic, + )), Handle::Static(Icon { font, code_point, size, - }) => Some((*font, *code_point, *size)), + shaping, + }) => Some((*font, *code_point, *size, *shaping)), Handle::Dynamic { open, closed } => { if state().is_open { - Some((open.font, open.code_point, open.size)) + Some((open.font, open.code_point, open.size, open.shaping)) } else { - Some((closed.font, closed.code_point, closed.size)) + Some(( + closed.font, + closed.code_point, + closed.size, + closed.shaping, + )) } } Handle::None => None, }; - if let Some((font, code_point, size)) = handle { + if let Some((font, code_point, size, shaping)) = handle { let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_text(Text { @@ -629,7 +655,7 @@ pub fn draw<'a, T, Renderer>( }, horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, - advanced_shape: false, + shaping, }); } @@ -655,7 +681,7 @@ pub fn draw<'a, T, Renderer>( }, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, - advanced_shape: false, + shaping: text_shaping, }); } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index b685c1a1..f62f4703 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -81,6 +81,7 @@ where size: f32, spacing: f32, text_size: Option<f32>, + text_shaping: text::Shaping, font: Option<Renderer::Font>, style: <Renderer::Theme as StyleSheet>::Style, } @@ -123,6 +124,7 @@ where size: Self::DEFAULT_SIZE, spacing: Self::DEFAULT_SPACING, //15 text_size: None, + text_shaping: text::Shaping::Basic, font: None, style: Default::default(), } @@ -152,6 +154,12 @@ where 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()); @@ -192,9 +200,15 @@ where .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()), - )) + .push( + Text::new(&self.label) + .width(self.width) + .size( + self.text_size + .unwrap_or_else(|| renderer.default_size()), + ) + .shaping(self.text_shaping), + ) .layout(renderer, limits) } @@ -309,7 +323,7 @@ where }, alignment::Horizontal::Left, alignment::Vertical::Center, - false, + self.text_shaping, ); } } diff --git a/widget/src/text.rs b/widget/src/text.rs index 04c31edc..50aa1370 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,3 +1,4 @@ +pub use crate::core::text::Shaping; pub use crate::core::widget::text::*; pub type Text<'a, Renderer = crate::Renderer> = diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index abf858ca..32d0b1f8 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -463,7 +463,7 @@ where &icon.code_point.to_string(), icon.size.unwrap_or_else(|| renderer.default_size()), icon.font, - true, + text::Shaping::Advanced, ); let mut text_node = layout::Node::new( @@ -976,7 +976,7 @@ pub fn draw<Renderer>( bounds: icon_layout.bounds(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - advanced_shape: true, + shaping: text::Shaping::Advanced, }); } @@ -1081,7 +1081,7 @@ pub fn draw<Renderer>( if text.is_empty() { placeholder } else { &text }, size, font, - true, + text::Shaping::Advanced, ); let render = |renderer: &mut Renderer| { @@ -1109,7 +1109,7 @@ pub fn draw<Renderer>( size, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, - advanced_shape: true, + shaping: text::Shaping::Advanced, }); }; @@ -1314,8 +1314,12 @@ where { let text_before_cursor = value.until(cursor_index).to_string(); - let text_value_width = - renderer.measure_width(&text_before_cursor, size, font, true); + 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); @@ -1348,9 +1352,9 @@ where size, font, Size::INFINITY, + text::Shaping::Advanced, Point::new(x + offset, text_bounds.height / 2.0), true, - true, ) .map(text::Hit::cursor)?; diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index d3033ddb..639bbb3b 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -43,6 +43,7 @@ where size: f32, text_size: Option<f32>, text_alignment: alignment::Horizontal, + text_shaping: text::Shaping, spacing: f32, font: Option<Renderer::Font>, style: <Renderer::Theme as StyleSheet>::Style, @@ -80,6 +81,7 @@ where size: Self::DEFAULT_SIZE, text_size: None, text_alignment: alignment::Horizontal::Left, + text_shaping: text::Shaping::Basic, spacing: 0.0, font: None, style: Default::default(), @@ -110,6 +112,12 @@ where 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; @@ -167,7 +175,8 @@ where .size( self.text_size .unwrap_or_else(|| renderer.default_size()), - ), + ) + .shaping(self.text_shaping), ); } @@ -249,7 +258,7 @@ where Default::default(), self.text_alignment, alignment::Vertical::Center, - false, + self.text_shaping, ); } -- cgit From 9499a8f9e6f9971dedfae563cb133232aa3cebc2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 4 May 2023 13:00:16 +0200 Subject: Support configurable `LineHeight` in text widgets --- widget/src/checkbox.rs | 14 ++++++++++++++ widget/src/overlay/menu.rs | 46 +++++++++++++++++++++++++++++++++++----------- widget/src/pick_list.rs | 26 ++++++++++++++++++++++---- widget/src/radio.rs | 13 +++++++++++++ widget/src/text_input.rs | 30 +++++++++++++++++++++++++++++- widget/src/toggler.rs | 13 +++++++++++++ 6 files changed, 126 insertions(+), 16 deletions(-) (limited to 'widget/src') diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 2a09b8fd..c34fd0bb 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -46,6 +46,7 @@ where 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>, @@ -83,6 +84,7 @@ where 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 { @@ -119,6 +121,15 @@ where 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; @@ -181,6 +192,7 @@ where self.text_size .unwrap_or_else(|| renderer.default_size()), ) + .line_height(self.text_line_height) .shaping(self.text_shaping), ) .layout(renderer, limits) @@ -276,6 +288,7 @@ where content: &code_point.to_string(), font: *font, size, + line_height: text::LineHeight::default(), bounds: Rectangle { x: bounds.center_x(), y: bounds.center_y(), @@ -298,6 +311,7 @@ where label_layout, &self.label, self.text_size, + self.text_line_height, self.font, crate::text::Appearance { color: custom_style.text_color, diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 7de3cbae..dfb6a22a 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -31,6 +31,7 @@ where 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, @@ -59,6 +60,7 @@ where 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(), @@ -83,6 +85,15 @@ where 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; @@ -176,6 +187,7 @@ where padding, font, text_size, + text_line_height, text_shaping, style, } = menu; @@ -186,6 +198,7 @@ where last_selection, font, text_size, + text_line_height, text_shaping, padding, style: style.clone(), @@ -321,6 +334,7 @@ where last_selection: &'a mut Option<T>, 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, @@ -352,10 +366,13 @@ where 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, - (text_size * 1.2 + self.padding.vertical()) + (f32::from(text_line_height) + self.padding.vertical()) * self.options.len() as f32, ); @@ -395,9 +412,12 @@ where .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 - bounds.y) - / (text_size * 1.2 + self.padding.vertical())) + ((cursor_position.y - bounds.y) / option_height) as usize, ); } @@ -410,9 +430,12 @@ where .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 - bounds.y) - / (text_size * 1.2 + self.padding.vertical())) + ((cursor_position.y - bounds.y) / option_height) as usize, ); @@ -462,12 +485,12 @@ where let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let option_height = - (text_size * 1.2 + self.padding.vertical()) as usize; + 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 f32) as usize; - let end = - ((offset + viewport.height) / option_height as f32).ceil() as usize; + 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())]; @@ -477,9 +500,9 @@ where let bounds = Rectangle { x: bounds.x, - y: bounds.y + (option_height * i) as f32, + y: bounds.y + (option_height * i as f32), width: bounds.width, - height: text_size * 1.2 + self.padding.vertical(), + height: option_height, }; if is_selected { @@ -503,6 +526,7 @@ where ..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 diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index c0cb2946..bc2c9066 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -36,6 +36,7 @@ where 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>, @@ -72,6 +73,7 @@ where 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(), @@ -103,6 +105,15 @@ where 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; @@ -172,6 +183,7 @@ where self.width, self.padding, self.text_size, + self.text_line_height, self.text_shaping, self.font, self.placeholder.as_deref(), @@ -230,6 +242,7 @@ where cursor_position, self.padding, self.text_size, + self.text_line_height, self.text_shaping, font, self.placeholder.as_deref(), @@ -358,6 +371,7 @@ pub fn layout<Renderer, T>( width: Length, padding: Padding, text_size: Option<f32>, + text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, placeholder: Option<&str>, @@ -375,11 +389,10 @@ where let max_width = match width { Length::Shrink => { let measure = |label: &str| -> f32 { - let (width, _) = renderer.measure( + let width = renderer.measure_width( label, text_size, font.unwrap_or_else(|| renderer.default_font()), - Size::new(f32::INFINITY, f32::INFINITY), text_shaping, ); @@ -400,8 +413,10 @@ where }; let size = { - let intrinsic = - Size::new(max_width + text_size + padding.left, text_size * 1.2); + 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) }; @@ -579,6 +594,7 @@ pub fn draw<'a, T, Renderer>( cursor_position: Point, padding: Padding, text_size: Option<f32>, + text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Renderer::Font, placeholder: Option<&str>, @@ -645,6 +661,7 @@ pub fn draw<'a, T, Renderer>( renderer.fill_text(Text { content: &code_point.to_string(), size, + line_height: text::LineHeight::default(), font, color: style.handle_color, bounds: Rectangle { @@ -667,6 +684,7 @@ pub fn draw<'a, T, Renderer>( renderer.fill_text(Text { content: label, size: text_size, + line_height: text_line_height, font, color: if is_selected { style.text_color diff --git a/widget/src/radio.rs b/widget/src/radio.rs index f62f4703..9dad1e22 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -81,6 +81,7 @@ where 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, @@ -124,6 +125,7 @@ where 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(), @@ -154,6 +156,15 @@ where 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; @@ -207,6 +218,7 @@ where self.text_size .unwrap_or_else(|| renderer.default_size()), ) + .line_height(self.text_line_height) .shaping(self.text_shaping), ) .layout(renderer, limits) @@ -317,6 +329,7 @@ where label_layout, &self.label, self.text_size, + self.text_line_height, self.font, crate::text::Appearance { color: custom_style.text_color, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 364ec3cd..bbc07dac 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -68,6 +68,7 @@ where 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>, @@ -96,6 +97,7 @@ where width: Length::Fill, padding: Padding::new(5.0), size: None, + line_height: text::LineHeight::default(), on_input: None, on_paste: None, on_submit: None, @@ -177,6 +179,15 @@ where 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, @@ -208,6 +219,7 @@ where value.unwrap_or(&self.value), &self.placeholder, self.size, + self.line_height, self.font, self.on_input.is_none(), self.is_secure, @@ -263,6 +275,7 @@ where self.width, self.padding, self.size, + self.line_height, self.icon.as_ref(), ) } @@ -299,6 +312,7 @@ where shell, &mut self.value, self.size, + self.line_height, self.font, self.is_secure, self.on_input.as_deref(), @@ -327,6 +341,7 @@ where &self.value, &self.placeholder, self.size, + self.line_height, self.font, self.on_input.is_none(), self.is_secure, @@ -447,6 +462,7 @@ pub fn layout<Renderer>( width: Length, padding: Padding, size: Option<f32>, + line_height: text::LineHeight, icon: Option<&Icon<Renderer::Font>>, ) -> layout::Node where @@ -454,7 +470,10 @@ where { 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(text_size * 1.2); + let limits = limits + .width(width) + .pad(padding) + .height(line_height.to_absolute(Pixels(text_size))); let text_bounds = limits.resolve(Size::ZERO); @@ -515,6 +534,7 @@ pub fn update<'a, Message, Renderer>( 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>, @@ -567,6 +587,7 @@ where text_layout.bounds(), font, size, + line_height, &value, state, target, @@ -595,6 +616,7 @@ where text_layout.bounds(), font, size, + line_height, value, state, target, @@ -644,6 +666,7 @@ where text_layout.bounds(), font, size, + line_height, &value, state, target, @@ -926,6 +949,7 @@ pub fn draw<Renderer>( value: &Value, placeholder: &str, size: Option<f32>, + line_height: text::LineHeight, font: Option<Renderer::Font>, is_disabled: bool, is_secure: bool, @@ -971,6 +995,7 @@ pub fn draw<Renderer>( 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 { @@ -1110,6 +1135,7 @@ pub fn draw<Renderer>( ..text_bounds }, size, + line_height, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, @@ -1336,6 +1362,7 @@ fn find_cursor_position<Renderer>( text_bounds: Rectangle, font: Option<Renderer::Font>, size: Option<f32>, + line_height: text::LineHeight, value: &Value, state: &State, x: f32, @@ -1353,6 +1380,7 @@ where .hit_test( &value, size, + line_height, font, Size::INFINITY, text::Shaping::Advanced, diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 639bbb3b..b1ba65c9 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -42,6 +42,7 @@ where width: Length, size: f32, text_size: Option<f32>, + text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, spacing: f32, @@ -80,6 +81,7 @@ where 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, @@ -106,6 +108,15 @@ where 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; @@ -176,6 +187,7 @@ where self.text_size .unwrap_or_else(|| renderer.default_size()), ) + .line_height(self.text_line_height) .shaping(self.text_shaping), ); } @@ -254,6 +266,7 @@ where label_layout, label, self.text_size, + self.text_line_height, self.font, Default::default(), self.text_alignment, -- cgit From 16bf8fc7622fbe67a7d81cbe6daad329385cc44c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Mon, 8 May 2023 15:37:29 +0200 Subject: Export `Shaping` and `LineHeight` in `widget::text` --- widget/src/text.rs | 1 - 1 file changed, 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/text.rs b/widget/src/text.rs index 50aa1370..04c31edc 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,4 +1,3 @@ -pub use crate::core::text::Shaping; pub use crate::core::widget::text::*; pub type Text<'a, Renderer = crate::Renderer> = -- cgit From 180cb073bdb1429e566839b09d6fe86114c83673 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Mon, 8 May 2023 16:19:28 +0200 Subject: Add `line_height` to `checkbox::Icon` --- widget/src/checkbox.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index c34fd0bb..7d43bb4a 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -91,6 +91,7 @@ where font: Renderer::ICON_FONT, code_point: Renderer::CHECKMARK_ICON, size: None, + line_height: text::LineHeight::default(), shaping: text::Shaping::Basic, }, style: Default::default(), @@ -279,6 +280,7 @@ where font, code_point, size, + line_height, shaping, } = &self.icon; let size = size.unwrap_or(bounds.height * 0.7); @@ -288,7 +290,7 @@ where content: &code_point.to_string(), font: *font, size, - line_height: text::LineHeight::default(), + line_height: *line_height, bounds: Rectangle { x: bounds.center_x(), y: bounds.center_y(), @@ -347,6 +349,8 @@ pub struct Icon<Font> { 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, } -- cgit From f0c87375d53d620b5939d87e1bc54288f9c184ac Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 10 May 2023 00:02:34 +0200 Subject: Add `line_height` to `pick_list::Icon` --- widget/src/pick_list.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index bc2c9066..50b47417 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -360,6 +360,8 @@ pub struct Icon<Font> { 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, } @@ -632,22 +634,31 @@ pub fn draw<'a, T, Renderer>( 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, *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.shaping)) + 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, )) } @@ -655,19 +666,19 @@ pub fn draw<'a, T, Renderer>( Handle::None => None, }; - if let Some((font, code_point, size, shaping)) = handle { + 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: text::LineHeight::default(), + line_height, font, color: style.handle_color, bounds: Rectangle { x: bounds.x + bounds.width - padding.horizontal(), y: bounds.center_y(), - height: size * 1.2, + height: f32::from(line_height.to_absolute(Pixels(size))), ..bounds }, horizontal_alignment: alignment::Horizontal::Right, -- cgit From 1400b5187d6fa30f5fa4f83684939f6dc87cb55f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 10 May 2023 00:37:08 +0200 Subject: Fix bounds of `PickList` text label --- widget/src/pick_list.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 50b47417..8c445dda 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -706,7 +706,9 @@ pub fn draw<'a, T, Renderer>( x: bounds.x + padding.left, y: bounds.center_y(), width: bounds.width - padding.horizontal(), - height: text_size * 1.2, + height: f32::from( + text_line_height.to_absolute(Pixels(text_size)), + ), }, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, -- cgit From 8622e998f2701e7f4ca8d2f71c85150f436a9945 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 11 May 2023 15:25:58 +0200 Subject: Write missing documentation in `iced_graphics` --- widget/src/canvas/program.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'widget/src') diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index efb33c56..ad0fbb83 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -2,7 +2,7 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; use crate::canvas::Cursor; use crate::core::Rectangle; -use crate::graphics::geometry; +use crate::graphics::geometry::{self, Geometry}; /// The state and logic of a [`Canvas`]. /// @@ -52,7 +52,7 @@ where theme: &Renderer::Theme, bounds: Rectangle, cursor: Cursor, - ) -> Vec<Renderer::Geometry>; + ) -> Vec<Geometry>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -94,7 +94,7 @@ where theme: &Renderer::Theme, bounds: Rectangle, cursor: Cursor, - ) -> Vec<Renderer::Geometry> { + ) -> Vec<Geometry> { T::draw(self, state, renderer, theme, bounds, cursor) } -- cgit From b60194844a0f81a864f00e0637b6a4d131194fc5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 11 May 2023 15:40:57 +0200 Subject: Write missing documentation in `iced_widget` --- widget/src/lazy.rs | 3 +++ widget/src/lazy/helpers.rs | 8 ++++++++ widget/src/lib.rs | 2 +- widget/src/overlay.rs | 1 + widget/src/text.rs | 2 ++ 5 files changed, 15 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index b08ed8cb..0ad46865 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -26,6 +26,7 @@ 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, @@ -41,6 +42,8 @@ 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, diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index be60bb78..8ca9cb86 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -4,6 +4,8 @@ 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, @@ -29,6 +31,12 @@ where 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> diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 904f62ad..ab1ab95b 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,7 +4,7 @@ )] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, diff --git a/widget/src/overlay.rs b/widget/src/overlay.rs index b9a0e3e0..bc0ed744 100644 --- a/widget/src/overlay.rs +++ b/widget/src/overlay.rs @@ -1 +1,2 @@ +//! Display interactive elements on top of other widgets. pub mod menu; diff --git a/widget/src/text.rs b/widget/src/text.rs index 04c31edc..ce4f44bd 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,4 +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>; -- cgit From cf434236e7e15e0fa05e5915b8d4d78dcaf1b7e8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 11 May 2023 17:28:51 +0200 Subject: Enable `doc_auto_cfg` when generating documentation --- widget/src/helpers.rs | 3 --- widget/src/lib.rs | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'widget/src') diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 336ac4ee..3f5136f8 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -310,7 +310,6 @@ where /// /// [`Image`]: widget::Image #[cfg(feature = "image")] -#[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { crate::Image::new(handle.into()) } @@ -320,7 +319,6 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { /// [`Svg`]: widget::Svg /// [`Handle`]: widget::svg::Handle #[cfg(feature = "svg")] -#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub fn svg<Renderer>( handle: impl Into<core::svg::Handle>, ) -> crate::Svg<Renderer> @@ -333,7 +331,6 @@ where /// Creates a new [`Canvas`]. #[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub fn canvas<P, Message, Renderer>( program: P, ) -> crate::Canvas<P, Message, Renderer> diff --git a/widget/src/lib.rs b/widget/src/lib.rs index ab1ab95b..9da13f9b 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -14,6 +14,7 @@ )] #![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; -- cgit From 99aa54cd88d7eb99149699d539ee4d59e08047b1 Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Tue, 16 May 2023 16:12:29 +0100 Subject: Add pane_grid functionality to split a pane with another pane --- widget/src/pane_grid.rs | 110 ++++++++++++++++++++++++++++++++++++++++-- widget/src/pane_grid/state.rs | 51 +++++++++++++++++++- 2 files changed, 156 insertions(+), 5 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 67145e8e..f7fdc072 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -594,13 +594,18 @@ pub fn update<'a, Message, T: Draggable>( if let Some(on_drag) = on_drag { let mut dropped_region = contents .zip(layout.children()) - .filter(|(_, layout)| { - layout.bounds().contains(cursor_position) + .filter_map(|(target, layout)| { + layout_region(layout, cursor_position) + .map(|region| (target, region)) }); let event = match dropped_region.next() { - Some(((target, _), _)) if pane != target => { - DragEvent::Dropped { pane, target } + Some(((target, _), region)) if pane != target => { + DragEvent::Dropped { + pane, + target, + region, + } } _ => DragEvent::Canceled { pane }, }; @@ -657,6 +662,28 @@ pub fn update<'a, Message, T: Draggable>( 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::Left + } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) { + Region::Right + } else if cursor_position.y < (bounds.y + bounds.height / 3.0) { + Region::Top + } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) { + Region::Bottom + } else { + Region::Center + }; + + Some(region) +} + fn click_pane<'a, Message, T>( action: &mut state::Action, layout: Layout<'_>, @@ -810,6 +837,36 @@ pub fn draw<Renderer, T>( Some((dragging, origin)) if id == dragging => { render_picked_pane = Some((pane, origin, layout)); } + Some((dragging, _)) if id != dragging => { + draw_pane( + pane, + renderer, + default_style, + layout, + pane_cursor_position, + viewport, + ); + + if picked_pane.is_some() { + if let Some(region) = layout_region(layout, cursor_position) + { + let bounds = layout_region_bounds(layout, region); + let hovered_region_style = theme.hovered_region(style); + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: hovered_region_style + .border_radius + .into(), + border_width: hovered_region_style.border_width, + border_color: hovered_region_style.border_color, + }, + theme.hovered_region(style).background, + ); + } + } + } _ => { draw_pane( pane, @@ -884,6 +941,32 @@ pub fn draw<Renderer, T>( } } +fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle { + let bounds = layout.bounds(); + + match region { + Region::Center => bounds, + Region::Top => Rectangle { + height: bounds.height / 2.0, + ..bounds + }, + Region::Left => Rectangle { + width: bounds.width / 2.0, + ..bounds + }, + Region::Right => Rectangle { + x: bounds.x + bounds.width / 2.0, + width: bounds.width / 2.0, + ..bounds + }, + Region::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 { @@ -900,6 +983,9 @@ pub enum DragEvent { /// The [`Pane`] where the picked one was dropped on. target: Pane, + + /// The [`Region`] of the target [`Pane`] where the picked one was dropped on. + region: Region, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] @@ -910,6 +996,22 @@ pub enum DragEvent { }, } +/// The region of a [`Pane`]. +#[derive(Debug, Clone, Copy, Default)] +pub enum Region { + /// Center region. + #[default] + Center, + /// Top region. + Top, + /// Left region. + Left, + /// Right region. + Right, + /// Bottom region. + Bottom, +} + /// An event produced during a resize interaction of a [`PaneGrid`]. #[derive(Debug, Clone, Copy)] pub struct ResizeEvent { diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index a6e2ec7f..b57cdd92 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -2,7 +2,9 @@ //! //! [`PaneGrid`]: crate::widget::PaneGrid use crate::core::{Point, Size}; -use crate::pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}; +use crate::pane_grid::{ + Axis, Configuration, Direction, Node, Pane, Region, Split, +}; use std::collections::HashMap; @@ -165,6 +167,53 @@ impl<T> State<T> { Some((new_pane, new_split)) } + /// 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: (T, &Pane), + region: Region, + ) { + match region { + Region::Center => { + let (_, pane) = pane; + self.swap(pane, target); + } + Region::Top => { + self.split_and_swap(Axis::Horizontal, target, pane, true) + } + Region::Bottom => { + self.split_and_swap(Axis::Horizontal, target, pane, false) + } + Region::Left => { + self.split_and_swap(Axis::Vertical, target, pane, true) + } + Region::Right => { + self.split_and_swap(Axis::Vertical, target, pane, false) + } + } + } + + fn split_and_swap( + &mut self, + axis: Axis, + target: &Pane, + pane: (T, &Pane), + invert: bool, + ) { + let (state, pane) = pane; + + if let Some((new_pane, _)) = self.split(axis, target, state) { + if invert { + self.swap(target, &new_pane); + } + + let _ = self.close(pane); + } + } + /// Swaps the position of the provided panes in the [`State`]. /// /// If you want to swap panes on drag and drop in your [`PaneGrid`], you -- cgit From 0cb84c1c4cc128f8260054192b4f7798de52a82a Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Wed, 17 May 2023 15:30:35 +0100 Subject: Change name of hovered region style struct, reorder lines & export Appearance --- widget/src/pane_grid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index f7fdc072..e6ffb1d6 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -30,7 +30,7 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; -pub use crate::style::pane_grid::{Line, StyleSheet}; +pub use crate::style::pane_grid::{Appearance, Line, StyleSheet}; use crate::container; use crate::core::event::{self, Event}; -- cgit From bc590e2d6f3a39b09b11928c829f05d7bc9f9211 Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Fri, 19 May 2023 12:12:08 +0100 Subject: Take pane state internally --- widget/src/pane_grid/state.rs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index b57cdd92..6ab34884 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -170,17 +170,9 @@ impl<T> State<T> { /// 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: (T, &Pane), - region: Region, - ) { + pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) { match region { - Region::Center => { - let (_, pane) = pane; - self.swap(pane, target); - } + Region::Center => self.swap(pane, target), Region::Top => { self.split_and_swap(Axis::Horizontal, target, pane, true) } @@ -200,17 +192,15 @@ impl<T> State<T> { &mut self, axis: Axis, target: &Pane, - pane: (T, &Pane), + pane: &Pane, invert: bool, ) { - let (state, pane) = pane; - - if let Some((new_pane, _)) = self.split(axis, target, state) { - if invert { - self.swap(target, &new_pane); + if let Some((state, _)) = self.close(pane) { + if let Some((new_pane, _)) = self.split(axis, target, state) { + if invert { + self.swap(target, &new_pane); + } } - - let _ = self.close(pane); } } -- cgit From 9b5f32ee403c8b0730e3bac2b48aab6b87d7b653 Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Fri, 19 May 2023 12:15:44 +0100 Subject: Rename invert -> swap --- widget/src/pane_grid/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 6ab34884..1f034ca3 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -193,11 +193,11 @@ impl<T> State<T> { axis: Axis, target: &Pane, pane: &Pane, - invert: bool, + swap: bool, ) { if let Some((state, _)) = self.close(pane) { if let Some((new_pane, _)) = self.split(axis, target, state) { - if invert { + if swap { self.swap(target, &new_pane); } } -- cgit From 12a57fae5c4a8cf24976c551a64753b5da32ee30 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Wed, 10 May 2023 17:09:29 -0700 Subject: Remove min width 1 from scrollbar & scroller --- widget/src/scrollable.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index fd51a6a8..c3478493 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -128,9 +128,8 @@ impl Properties { } /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. pub fn width(mut self, width: impl Into<Pixels>) -> Self { - self.width = width.into().0.max(1.0); + self.width = width.into().0.max(0.0); self } @@ -141,9 +140,8 @@ impl Properties { } /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { - self.scroller_width = scroller_width.into().0.max(1.0); + self.scroller_width = scroller_width.into().0.max(0.0); self } } -- cgit From 29326952b420c22c34a5a1315e8f8a1ce77311ef Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Tue, 23 May 2023 04:39:41 +0200 Subject: Avoid drawing empty quads in `widget::scrollable` --- widget/src/scrollable.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c3478493..af5424c0 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -809,9 +809,11 @@ pub fn draw<Renderer>( style: Scrollbar, scrollbar: &internals::Scrollbar| { //track - if style.background.is_some() - || (style.border_color != Color::TRANSPARENT - && style.border_width > 0.0) + 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 { @@ -827,9 +829,11 @@ pub fn draw<Renderer>( } //thumb - if style.scroller.color != Color::TRANSPARENT - || (style.scroller.border_color != Color::TRANSPARENT - && style.scroller.border_width > 0.0) + 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 { -- cgit From 1c86defab5f5491b5f6b6e45faabf1b91ed195a3 Mon Sep 17 00:00:00 2001 From: Casper Storm <casper.storm@lich.io> Date: Tue, 23 May 2023 12:26:16 +0200 Subject: Extend border radius on relevant widgets --- widget/src/slider.rs | 20 ++++++++++---------- widget/src/vertical_slider.rs | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) (limited to 'widget/src') diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 18a49665..d3c4a936 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -368,16 +368,16 @@ pub fn draw<T, R>( 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), - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.height, border_radius), - }; + 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) = { diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 2635611d..3b2430c4 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -366,16 +366,16 @@ pub fn draw<T, R>( 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), - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.width, border_radius), - }; + 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) = { @@ -431,7 +431,7 @@ pub fn draw<T, R>( width: handle_height, height: handle_width, }, - border_radius: handle_border_radius.into(), + border_radius: handle_border_radius, border_width: style.handle.border_width, border_color: style.handle.border_color, }, -- cgit From 1234d528121265698f9f426ca89fc687dc95dc01 Mon Sep 17 00:00:00 2001 From: Casper Storm <casper.storm@lich.io> Date: Tue, 23 May 2023 15:28:45 +0200 Subject: clippy --- widget/src/button.rs | 4 ++-- widget/src/checkbox.rs | 2 +- widget/src/container.rs | 2 +- widget/src/overlay/menu.rs | 4 ++-- widget/src/pane_grid.rs | 3 +-- widget/src/pick_list.rs | 2 +- widget/src/progress_bar.rs | 4 ++-- widget/src/rule.rs | 2 +- widget/src/scrollable.rs | 4 ++-- widget/src/slider.rs | 2 +- widget/src/text_input.rs | 2 +- 11 files changed, 15 insertions(+), 16 deletions(-) (limited to 'widget/src') diff --git a/widget/src/button.rs b/widget/src/button.rs index 7eee69cb..70fed1d5 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -395,7 +395,7 @@ where y: bounds.y + styling.shadow_offset.y, ..bounds }, - border_radius: styling.border_radius.into(), + border_radius: styling.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -406,7 +406,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: styling.border_radius.into(), + border_radius: styling.border_radius, border_width: styling.border_width, border_color: styling.border_color, }, diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 7d43bb4a..4c8a989b 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -269,7 +269,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: custom_style.border_radius.into(), + border_radius: custom_style.border_radius, border_width: custom_style.border_width, border_color: custom_style.border_color, }, diff --git a/widget/src/container.rs b/widget/src/container.rs index 9d932772..13e76551 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -332,7 +332,7 @@ pub fn draw_background<Renderer>( renderer.fill_quad( renderer::Quad { bounds, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, border_width: appearance.border_width, border_color: appearance.border_color, }, diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 0acc6f79..84cc800c 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -307,7 +307,7 @@ where bounds, border_color: appearance.border_color, border_width: appearance.border_width, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, }, appearance.background, ); @@ -515,7 +515,7 @@ where }, border_color: Color::TRANSPARENT, border_width: 0.0, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, }, appearance.selected_background, ); diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index e6ffb1d6..7bddc4a6 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -857,8 +857,7 @@ pub fn draw<Renderer, T>( renderer::Quad { bounds, border_radius: hovered_region_style - .border_radius - .into(), + .border_radius, border_width: hovered_region_style.border_width, border_color: hovered_region_style.border_color, }, diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 8c445dda..dcd0629b 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -624,7 +624,7 @@ pub fn draw<'a, T, Renderer>( bounds, border_color: style.border_color, border_width: style.border_width, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, }, style.background, ); diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index ef0d87d5..9e1e9131 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -133,7 +133,7 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -147,7 +147,7 @@ where width: active_progress_width, ..bounds }, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 3749d7ce..272bd2b3 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -125,7 +125,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: style.radius.into(), + border_radius: style.radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index af5424c0..12e544c5 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -818,7 +818,7 @@ pub fn draw<Renderer>( renderer.fill_quad( renderer::Quad { bounds: scrollbar.bounds, - border_radius: style.border_radius.into(), + border_radius: style.border_radius, border_width: style.border_width, border_color: style.border_color, }, @@ -838,7 +838,7 @@ pub fn draw<Renderer>( renderer.fill_quad( renderer::Quad { bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius.into(), + border_radius: style.scroller.border_radius, border_width: style.scroller.border_width, border_color: style.scroller.border_color, }, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index d3c4a936..2851d8c3 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -433,7 +433,7 @@ pub fn draw<T, R>( width: handle_width, height: handle_height, }, - border_radius: handle_border_radius.into(), + border_radius: handle_border_radius, border_width: style.handle.border_width, border_color: style.handle.border_color, }, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index bbc07dac..8f243c1a 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -982,7 +982,7 @@ pub fn draw<Renderer>( renderer.fill_quad( renderer::Quad { bounds, - border_radius: appearance.border_radius.into(), + border_radius: appearance.border_radius, border_width: appearance.border_width, border_color: appearance.border_color, }, -- cgit From 0b504bec8ab17b1c98f3bb957f2ec5ba80b99ec7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 1 Jun 2023 04:59:49 +0200 Subject: Invalidate `Responsive` layout when size changes without a `view` call --- widget/src/lazy/responsive.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'widget/src') diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index b41d978b..9b4fd9dd 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -81,6 +81,7 @@ where self.element = view(new_size); self.size = new_size; + self.layout = None; tree.diff(&self.element); } -- cgit From 9902e778167af8d4b108102619ee1ed8d0de0be5 Mon Sep 17 00:00:00 2001 From: Casper Storm <casper.storm@lich.io> Date: Thu, 1 Jun 2023 10:46:33 +0200 Subject: Add border_radius to slider rail --- widget/src/slider.rs | 4 ++-- widget/src/vertical_slider.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'widget/src') diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 2851d8c3..c2498b87 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -403,7 +403,7 @@ pub fn draw<T, R>( width: offset + handle_width / 2.0, height: style.rail.width, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -418,7 +418,7 @@ pub fn draw<T, R>( width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 3b2430c4..b14e5401 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -401,7 +401,7 @@ pub fn draw<T, R>( width: style.rail.width, height: offset + handle_width / 2.0, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -416,7 +416,7 @@ pub fn draw<T, R>( width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border_radius: Default::default(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, -- cgit From 1e2965d5423fc35943388aa32a6cae2c6b0f2336 Mon Sep 17 00:00:00 2001 From: Casper Storm <casper.storm@lich.io> Date: Thu, 1 Jun 2023 13:30:48 +0200 Subject: only add border radius to the visible part --- widget/src/slider.rs | 7 +++++-- widget/src/vertical_slider.rs | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'widget/src') diff --git a/widget/src/slider.rs b/widget/src/slider.rs index c2498b87..af6a824f 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -367,6 +367,7 @@ pub fn draw<T, R>( } else { style_sheet.active(style) }; + let border_radius: [f32; 4] = style.rail.border_radius.into(); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { @@ -403,7 +404,8 @@ pub fn draw<T, R>( width: offset + handle_width / 2.0, height: style.rail.width, }, - border_radius: style.rail.border_radius, + border_radius: [border_radius[0], 0.0, 0.0, border_radius[3]] + .into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -418,7 +420,8 @@ pub fn draw<T, R>( width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border_radius: style.rail.border_radius, + border_radius: [0.0, border_radius[1], border_radius[2], 0.0] + .into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index b14e5401..c6569c13 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -365,6 +365,7 @@ pub fn draw<T, R>( } else { style_sheet.active(style) }; + let border_radius: [f32; 4] = style.rail.border_radius.into(); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { @@ -401,7 +402,8 @@ pub fn draw<T, R>( width: style.rail.width, height: offset + handle_width / 2.0, }, - border_radius: style.rail.border_radius, + border_radius: [border_radius[0], border_radius[1], 0.0, 0.0] + .into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -416,7 +418,8 @@ pub fn draw<T, R>( width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border_radius: style.rail.border_radius, + border_radius: [0.0, 0.0, border_radius[2], border_radius[3]] + .into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, -- cgit From 66d4decc0c5c6ea787ec482ff6841829dfc7ffba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 1 Jun 2023 17:27:52 +0200 Subject: Fix `quad` glitch when rounding borders of a `Slider` rail --- widget/src/slider.rs | 7 ++----- widget/src/vertical_slider.rs | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) (limited to 'widget/src') diff --git a/widget/src/slider.rs b/widget/src/slider.rs index af6a824f..c2498b87 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -367,7 +367,6 @@ pub fn draw<T, R>( } else { style_sheet.active(style) }; - let border_radius: [f32; 4] = style.rail.border_radius.into(); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { @@ -404,8 +403,7 @@ pub fn draw<T, R>( width: offset + handle_width / 2.0, height: style.rail.width, }, - border_radius: [border_radius[0], 0.0, 0.0, border_radius[3]] - .into(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -420,8 +418,7 @@ pub fn draw<T, R>( width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border_radius: [0.0, border_radius[1], border_radius[2], 0.0] - .into(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index c6569c13..b14e5401 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -365,7 +365,6 @@ pub fn draw<T, R>( } else { style_sheet.active(style) }; - let border_radius: [f32; 4] = style.rail.border_radius.into(); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { @@ -402,8 +401,7 @@ pub fn draw<T, R>( width: style.rail.width, height: offset + handle_width / 2.0, }, - border_radius: [border_radius[0], border_radius[1], 0.0, 0.0] - .into(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -418,8 +416,7 @@ pub fn draw<T, R>( width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border_radius: [0.0, 0.0, border_radius[2], border_radius[3]] - .into(), + border_radius: style.rail.border_radius, border_width: 0.0, border_color: Color::TRANSPARENT, }, -- cgit From 5b5000e3ae9789cf1d83cecd08f4d0cedc16d788 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Tue, 6 Jun 2023 16:18:20 +0200 Subject: Introduce `on_press_maybe` helper for `Button` --- widget/src/button.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'widget/src') diff --git a/widget/src/button.rs b/widget/src/button.rs index 70fed1d5..c656142c 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -102,8 +102,17 @@ where /// 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, msg: Message) -> Self { - self.on_press = Some(msg); + 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 } -- cgit From 34451bff185d8875f55747ee97ed746828e30f40 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 8 Jun 2023 20:11:59 +0200 Subject: Implement basic cursor availability --- widget/src/button.rs | 39 +++--- widget/src/canvas.rs | 19 ++- widget/src/canvas/cursor.rs | 64 ---------- widget/src/canvas/program.rs | 13 +- widget/src/checkbox.rs | 17 ++- widget/src/column.rs | 30 ++--- widget/src/container.rs | 12 +- widget/src/image.rs | 5 +- widget/src/image/viewer.rs | 25 ++-- widget/src/lazy.rs | 36 ++---- widget/src/lazy/component.rs | 29 ++--- widget/src/lazy/responsive.rs | 50 +++----- widget/src/mouse_area.rs | 20 +-- widget/src/overlay/menu.rs | 68 ++++------ widget/src/pane_grid.rs | 160 ++++++++++++------------ widget/src/pane_grid/content.rs | 72 ++++++----- widget/src/pane_grid/draggable.rs | 6 +- widget/src/pane_grid/title_bar.rs | 18 +-- widget/src/pick_list.rs | 30 ++--- widget/src/progress_bar.rs | 7 +- widget/src/qr_code.rs | 3 +- widget/src/radio.rs | 17 ++- widget/src/row.rs | 30 ++--- widget/src/rule.rs | 5 +- widget/src/scrollable.rs | 256 +++++++++++++++++++------------------- widget/src/slider.rs | 36 +++--- widget/src/space.rs | 5 +- widget/src/svg.rs | 5 +- widget/src/text_input.rs | 38 +++--- widget/src/toggler.rs | 16 +-- widget/src/tooltip.rs | 33 ++--- widget/src/vertical_slider.rs | 37 +++--- 32 files changed, 523 insertions(+), 678 deletions(-) delete mode 100644 widget/src/canvas/cursor.rs (limited to 'widget/src') diff --git a/widget/src/button.rs b/widget/src/button.rs index 70fed1d5..89af341c 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -187,7 +187,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -196,7 +196,7 @@ where &mut tree.children[0], event.clone(), layout.children().next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -204,14 +204,9 @@ where return event::Status::Captured; } - update( - event, - layout, - cursor_position, - shell, - &self.on_press, - || tree.state.downcast_mut::<State>(), - ) + update(event, layout, cursor, shell, &self.on_press, || { + tree.state.downcast_mut::<State>() + }) } fn draw( @@ -221,7 +216,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -230,7 +225,7 @@ where let styling = draw( renderer, bounds, - cursor_position, + cursor, self.on_press.is_some(), theme, &self.style, @@ -245,7 +240,7 @@ where text_color: styling.text_color, }, content_layout, - cursor_position, + cursor, &bounds, ); } @@ -254,11 +249,11 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_press.is_some()) + mouse_interaction(layout, cursor, self.on_press.is_some()) } fn overlay<'b>( @@ -305,7 +300,7 @@ impl State { pub fn update<'a, Message: Clone>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_press: &Option<Message>, state: impl FnOnce() -> &'a mut State, @@ -316,7 +311,7 @@ pub fn update<'a, Message: Clone>( if on_press.is_some() { let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if cursor.is_over(&bounds) { let state = state(); state.is_pressed = true; @@ -335,7 +330,7 @@ pub fn update<'a, Message: Clone>( let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if cursor.is_over(&bounds) { shell.publish(on_press); } @@ -358,7 +353,7 @@ pub fn update<'a, Message: Clone>( pub fn draw<'a, Renderer: crate::core::Renderer>( renderer: &mut Renderer, bounds: Rectangle, - cursor_position: Point, + cursor: mouse::Cursor, is_enabled: bool, style_sheet: &dyn StyleSheet< Style = <Renderer::Theme as StyleSheet>::Style, @@ -369,7 +364,7 @@ pub fn draw<'a, Renderer: crate::core::Renderer>( where Renderer::Theme: StyleSheet, { - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); let styling = if !is_enabled { style_sheet.disabled(style) @@ -442,10 +437,10 @@ pub fn layout<Renderer>( /// Returns the [`mouse::Interaction`] of a [`Button`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, is_enabled: bool, ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); + let is_mouse_over = cursor.is_over(&layout.bounds()); if is_mouse_over && is_enabled { mouse::Interaction::Pointer diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 171c4534..96062038 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -1,10 +1,8 @@ //! Draw 2D graphics for your users. pub mod event; -mod cursor; mod program; -pub use cursor::Cursor; pub use event::Event; pub use program::Program; @@ -17,7 +15,7 @@ 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, Point, Rectangle, Size, Vector}; +use crate::core::{Length, Rectangle, Size, Vector}; use crate::graphics::geometry; use std::marker::PhantomData; @@ -28,8 +26,9 @@ use std::marker::PhantomData; /// 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, Cursor, Fill, Frame, Geometry, Path, Program}; +/// # 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>; @@ -43,7 +42,7 @@ use std::marker::PhantomData; /// impl Program<()> for Circle { /// type State = (); /// -/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// 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()); /// @@ -144,7 +143,7 @@ where tree: &mut Tree, event: core::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -160,8 +159,6 @@ where _ => None, }; - let cursor = Cursor::from_window_position(cursor_position); - if let Some(canvas_event) = canvas_event { let state = tree.state.downcast_mut::<P::State>(); @@ -182,12 +179,11 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let bounds = layout.bounds(); - let cursor = Cursor::from_window_position(cursor_position); let state = tree.state.downcast_ref::<P::State>(); self.program.mouse_interaction(state, bounds, cursor) @@ -200,7 +196,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -209,7 +205,6 @@ where return; } - let cursor = Cursor::from_window_position(cursor_position); let state = tree.state.downcast_ref::<P::State>(); renderer.with_translation( diff --git a/widget/src/canvas/cursor.rs b/widget/src/canvas/cursor.rs deleted file mode 100644 index 5a65e9a7..00000000 --- a/widget/src/canvas/cursor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::core::{Point, Rectangle}; - -/// The mouse cursor state. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Cursor { - /// The cursor has a defined position. - Available(Point), - - /// The cursor is currently unavailable (i.e. out of bounds or busy). - Unavailable, -} - -impl Cursor { - // TODO: Remove this once this type is used in `iced_native` to encode - // proper cursor availability - pub(crate) fn from_window_position(position: Point) -> Self { - if position.x < 0.0 || position.y < 0.0 { - Cursor::Unavailable - } else { - Cursor::Available(position) - } - } - - /// Returns the absolute position of the [`Cursor`], if available. - pub fn position(&self) -> Option<Point> { - match self { - Cursor::Available(position) => Some(*position), - Cursor::Unavailable => None, - } - } - - /// Returns the relative position of the [`Cursor`] inside the given bounds, - /// if available. - /// - /// If the [`Cursor`] is not over the provided bounds, this method will - /// return `None`. - pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> { - if self.is_over(bounds) { - self.position_from(bounds.position()) - } else { - None - } - } - - /// Returns the relative position of the [`Cursor`] from the given origin, - /// if available. - pub fn position_from(&self, origin: Point) -> Option<Point> { - match self { - Cursor::Available(position) => { - Some(Point::new(position.x - origin.x, position.y - origin.y)) - } - Cursor::Unavailable => None, - } - } - - /// Returns whether the [`Cursor`] is currently over the provided bounds - /// or not. - pub fn is_over(&self, bounds: &Rectangle) -> bool { - match self { - Cursor::Available(position) => bounds.contains(*position), - Cursor::Unavailable => false, - } - } -} diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index ad0fbb83..929ee285 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,6 +1,5 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; -use crate::canvas::Cursor; use crate::core::Rectangle; use crate::graphics::geometry::{self, Geometry}; @@ -33,7 +32,7 @@ where _state: &mut Self::State, _event: Event, _bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { (event::Status::Ignored, None) } @@ -51,7 +50,7 @@ where renderer: &Renderer, theme: &Renderer::Theme, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> Vec<Geometry>; /// Returns the current mouse interaction of the [`Program`]. @@ -64,7 +63,7 @@ where &self, _state: &Self::State, _bounds: Rectangle, - _cursor: Cursor, + _cursor: mouse::Cursor, ) -> mouse::Interaction { mouse::Interaction::default() } @@ -82,7 +81,7 @@ where state: &mut Self::State, event: Event, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> (event::Status, Option<Message>) { T::update(self, state, event, bounds, cursor) } @@ -93,7 +92,7 @@ where renderer: &Renderer, theme: &Renderer::Theme, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> Vec<Geometry> { T::draw(self, state, renderer, theme, bounds, cursor) } @@ -102,7 +101,7 @@ where &self, state: &Self::State, bounds: Rectangle, - cursor: Cursor, + cursor: mouse::Cursor, ) -> mouse::Interaction { T::mouse_interaction(self, state, bounds, cursor) } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 4c8a989b..86f573dd 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -8,8 +8,8 @@ use crate::core::text; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, + Widget, }; use crate::{Row, Text}; @@ -204,7 +204,7 @@ where _tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -212,7 +212,7 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - let mouse_over = layout.bounds().contains(cursor_position); + let mouse_over = cursor.is_over(&layout.bounds()); if mouse_over { shell.publish((self.on_toggle)(!self.is_checked)); @@ -230,11 +230,11 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(&layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -248,11 +248,10 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&layout.bounds()); let mut children = layout.children(); diff --git a/widget/src/column.rs b/widget/src/column.rs index 8f363ec6..d92d794b 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -6,8 +6,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Widget, + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Widget, }; /// A container that distributes its contents vertically. @@ -166,7 +166,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -180,7 +180,7 @@ where state, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -193,7 +193,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -203,11 +203,7 @@ where .zip(layout.children()) .map(|((child, state), layout)| { child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, + state, layout, cursor, viewport, renderer, ) }) .max() @@ -221,7 +217,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { for ((child, state), layout) in self @@ -230,15 +226,9 @@ where .zip(&tree.children) .zip(layout.children()) { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); } } diff --git a/widget/src/container.rs b/widget/src/container.rs index 13e76551..da9a31d6 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -196,7 +196,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -205,7 +205,7 @@ where &mut tree.children[0], event, layout.children().next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -216,14 +216,14 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout.children().next().unwrap(), - cursor_position, + cursor, viewport, renderer, ) @@ -236,7 +236,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let style = theme.appearance(&self.style); @@ -253,7 +253,7 @@ where .unwrap_or(renderer_style.text_color), }, layout.children().next().unwrap(), - cursor_position, + cursor, viewport, ); } diff --git a/widget/src/image.rs b/widget/src/image.rs index abcb6ef2..66bf2156 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -4,10 +4,11 @@ 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, Point, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; use std::hash::Hash; @@ -186,7 +187,7 @@ where _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw(renderer, layout, &self.handle, self.content_fit) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 0d60d818..a2ee1e5c 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -144,18 +144,19 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, _shell: &mut Shell<'_, Message>, ) -> event::Status { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) - if is_mouse_over => - { + 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, .. } => { @@ -205,9 +206,11 @@ where event::Status::Captured } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - if is_mouse_over => - { + 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); @@ -277,13 +280,13 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); if state.is_cursor_grabbed() { mouse::Interaction::Grabbing @@ -301,7 +304,7 @@ where _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State>(); diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 0ad46865..92a611c3 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -181,7 +181,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -191,7 +191,7 @@ where &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -203,7 +203,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -211,7 +211,7 @@ where element.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -225,7 +225,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.with_element(|element| { @@ -235,7 +235,7 @@ where theme, style, layout, - cursor_position, + cursor, viewport, ) }) @@ -342,27 +342,22 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); + overlay.draw(renderer, theme, style, layout, cursor); }); } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + overlay.mouse_interaction(layout, cursor, viewport, renderer) }) .unwrap_or_default() } @@ -371,20 +366,13 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + 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_position, - renderer, - clipboard, - shell, - ) + overlay.on_event(event, layout, cursor, renderer, clipboard, shell) }) .unwrap_or(event::Status::Ignored) } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 49ae68af..f462c8cf 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -265,7 +265,7 @@ where tree: &mut Tree, event: core::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -279,7 +279,7 @@ where &mut t.borrow_mut().as_mut().unwrap().children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -397,7 +397,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); @@ -408,7 +408,7 @@ where theme, style, layout, - cursor_position, + cursor, viewport, ); }); @@ -418,7 +418,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -427,7 +427,7 @@ where element.as_widget().mouse_interaction( &tree.borrow().as_ref().unwrap().children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -569,27 +569,22 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); + overlay.draw(renderer, theme, style, layout, cursor); }); } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + overlay.mouse_interaction(layout, cursor, viewport, renderer) }) .unwrap_or_default() } @@ -598,7 +593,7 @@ where &mut self, event: core::Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -611,7 +606,7 @@ where overlay.on_event( event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 9b4fd9dd..bd6385bc 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -177,7 +177,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -198,7 +198,7 @@ where tree, event, layout, - cursor_position, + cursor, renderer, clipboard, &mut local_shell, @@ -222,7 +222,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State>(); @@ -235,13 +235,7 @@ where &self.view, |tree, renderer, layout, element| { element.as_widget().draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, + tree, renderer, theme, style, layout, cursor, viewport, ) }, ) @@ -251,7 +245,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -264,13 +258,9 @@ where layout, &self.view, |tree, renderer, layout, element| { - element.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) + element + .as_widget() + .mouse_interaction(tree, layout, cursor, viewport, renderer) }, ) } @@ -384,27 +374,22 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); + overlay.draw(renderer, theme, style, layout, cursor); }); } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) + overlay.mouse_interaction(layout, cursor, viewport, renderer) }) .unwrap_or_default() } @@ -413,20 +398,13 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + 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_position, - renderer, - clipboard, - shell, - ) + overlay.on_event(event, layout, cursor, renderer, clipboard, shell) }) .unwrap_or(event::Status::Ignored) } diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 0232c494..423f070c 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -8,7 +8,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget, + Clipboard, Element, Layout, Length, Rectangle, Shell, Widget, }; /// Emit messages on mouse events. @@ -146,7 +146,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -155,7 +155,7 @@ where &mut tree.children[0], event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -163,21 +163,21 @@ where return event::Status::Captured; } - update(self, &event, layout, cursor_position, shell) + update(self, &event, layout, cursor, shell) } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -190,7 +190,7 @@ where theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( @@ -199,7 +199,7 @@ where theme, renderer_style, layout, - cursor_position, + cursor, viewport, ); } @@ -237,10 +237,10 @@ fn update<Message: Clone, Renderer>( widget: &mut MouseArea<'_, Message, Renderer>, event: &Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, ) -> event::Status { - if !layout.bounds().contains(cursor_position) { + if !cursor.is_over(&layout.bounds()) { return event::Status::Ignored; } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 84cc800c..fe1175ac 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -259,36 +259,25 @@ where &mut self, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { self.container.on_event( - self.state, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, + self.state, event, layout, cursor, renderer, clipboard, shell, ) } fn mouse_interaction( &self, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.container.mouse_interaction( - self.state, - layout, - cursor_position, - viewport, - renderer, - ) + self.container + .mouse_interaction(self.state, layout, cursor, viewport, renderer) } fn draw( @@ -297,7 +286,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) { let appearance = theme.appearance(&self.style); let bounds = layout.bounds(); @@ -312,15 +301,8 @@ where appearance.background, ); - self.container.draw( - self.state, - renderer, - theme, - style, - layout, - cursor_position, - &bounds, - ); + self.container + .draw(self.state, renderer, theme, style, layout, cursor, &bounds); } } @@ -387,7 +369,7 @@ where _state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, _shell: &mut Shell<'_, Message>, @@ -396,7 +378,7 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if cursor.is_over(&bounds) { if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { *self.last_selection = Some(option.clone()); @@ -405,9 +387,9 @@ where } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { + if let Some(cursor_position) = + cursor.position_in(&layout.bounds()) + { let text_size = self .text_size .unwrap_or_else(|| renderer.default_size()); @@ -416,16 +398,14 @@ where self.text_line_height.to_absolute(Pixels(text_size)), ) + self.padding.vertical(); - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) / option_height) - as usize, - ); + *self.hovered_option = + Some((cursor_position.y / option_height) as usize); } } Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { + if let Some(cursor_position) = + cursor.position_in(&layout.bounds()) + { let text_size = self .text_size .unwrap_or_else(|| renderer.default_size()); @@ -434,10 +414,8 @@ where self.text_line_height.to_absolute(Pixels(text_size)), ) + self.padding.vertical(); - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) / option_height) - as usize, - ); + *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) { @@ -456,11 +434,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); + let is_mouse_over = cursor.is_over(&layout.bounds()); if is_mouse_over { mouse::Interaction::Pointer @@ -476,7 +454,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let appearance = theme.appearance(&self.style); diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 7bddc4a6..7e0198fe 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -313,7 +313,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -331,7 +331,7 @@ where self.contents.layout(), &event, layout, - cursor_position, + cursor, shell, self.spacing, self.contents.iter(), @@ -353,7 +353,7 @@ where tree, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -367,7 +367,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -375,7 +375,7 @@ where tree.state.downcast_ref(), self.contents.layout(), layout, - cursor_position, + cursor, self.spacing, self.on_resize.as_ref().map(|(leeway, _)| *leeway), ) @@ -388,7 +388,7 @@ where content.mouse_interaction( tree, layout, - cursor_position, + cursor, viewport, renderer, self.drag_enabled(), @@ -406,14 +406,14 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { draw( tree.state.downcast_ref(), self.contents.layout(), layout, - cursor_position, + cursor, renderer, theme, style, @@ -425,20 +425,9 @@ where .iter() .zip(&tree.children) .map(|((pane, content), tree)| (pane, (content, tree))), - |(content, tree), - renderer, - style, - layout, - cursor_position, - rectangle| { + |(content, tree), renderer, style, layout, cursor, rectangle| { content.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - rectangle, + tree, renderer, theme, style, layout, cursor, rectangle, ); }, ) @@ -520,7 +509,7 @@ pub fn update<'a, Message, T: Draggable>( node: &Node, event: &Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, spacing: f32, contents: impl Iterator<Item = (Pane, T)>, @@ -535,7 +524,7 @@ pub fn update<'a, Message, T: Draggable>( | Event::Touch(touch::Event::FingerPressed { .. }) => { let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if let Some(cursor_position) = cursor.position_over(&bounds) { event_status = event::Status::Captured; match on_resize { @@ -592,14 +581,18 @@ pub fn update<'a, Message, T: Draggable>( | Event::Touch(touch::Event::FingerLost { .. }) => { if let Some((pane, _)) = action.picked_pane() { if let Some(on_drag) = on_drag { - let mut dropped_region = contents - .zip(layout.children()) - .filter_map(|(target, layout)| { - layout_region(layout, cursor_position) - .map(|region| (target, region)) + let dropped_region = + cursor.position().and_then(|cursor_position| { + contents + .zip(layout.children()) + .filter_map(|(target, layout)| { + layout_region(layout, cursor_position) + .map(|region| (target, region)) + }) + .next() }); - let event = match dropped_region.next() { + let event = match dropped_region { Some(((target, _), region)) if pane != target => { DragEvent::Dropped { pane, @@ -634,24 +627,32 @@ pub fn update<'a, Message, T: Draggable>( ); if let Some((axis, rectangle, _)) = splits.get(&split) { - 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; + 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; + } } } } @@ -724,7 +725,7 @@ pub fn mouse_interaction( action: &state::Action, node: &Node, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, spacing: f32, resize_leeway: Option<f32>, ) -> Option<mouse::Interaction> { @@ -735,6 +736,7 @@ pub fn mouse_interaction( 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()); @@ -764,7 +766,7 @@ pub fn draw<Renderer, T>( action: &state::Action, node: &Node, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &mut Renderer, theme: &Renderer::Theme, default_style: &renderer::Style, @@ -778,7 +780,7 @@ pub fn draw<Renderer, T>( &mut Renderer, &renderer::Style, Layout<'_>, - Point, + mouse::Cursor, &Rectangle, ), ) where @@ -802,6 +804,7 @@ pub fn draw<Renderer, T>( }) .or_else(|| match resize_leeway { Some(leeway) => { + let cursor_position = cursor.position()?; let bounds = layout.bounds(); let relative_cursor = Point::new( @@ -822,12 +825,10 @@ pub fn draw<Renderer, T>( None => None, }); - let pane_cursor_position = if picked_pane.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) + let pane_cursor = if picked_pane.is_some() { + mouse::Cursor::Unavailable } else { - cursor_position + cursor }; let mut render_picked_pane = None; @@ -843,12 +844,15 @@ pub fn draw<Renderer, T>( renderer, default_style, layout, - pane_cursor_position, + pane_cursor, viewport, ); if picked_pane.is_some() { - if let Some(region) = layout_region(layout, cursor_position) + if let Some(region) = + cursor.position().and_then(|cursor_position| { + layout_region(layout, cursor_position) + }) { let bounds = layout_region_bounds(layout, region); let hovered_region_style = theme.hovered_region(style); @@ -872,7 +876,7 @@ pub fn draw<Renderer, T>( renderer, default_style, layout, - pane_cursor_position, + pane_cursor, viewport, ); } @@ -881,25 +885,27 @@ pub fn draw<Renderer, T>( // Render picked pane last if let Some((pane, origin, layout)) = render_picked_pane { - 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_position, - viewport, - ); - }); - }, - ); - }; + 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 let Some((axis, split_region, is_picked)) = picked_split { let highlight = if is_picked { diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 035ef05b..fe5c6af0 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -95,7 +95,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { use container::StyleSheet; @@ -113,7 +113,7 @@ where let title_bar_layout = children.next().unwrap(); let body_layout = children.next().unwrap(); - let show_controls = bounds.contains(cursor_position); + let show_controls = cursor.is_over(&bounds); self.body.as_widget().draw( &tree.children[0], @@ -121,7 +121,7 @@ where theme, style, body_layout, - cursor_position, + cursor, viewport, ); @@ -131,7 +131,7 @@ where theme, style, title_bar_layout, - cursor_position, + cursor, viewport, show_controls, ); @@ -142,7 +142,7 @@ where theme, style, layout, - cursor_position, + cursor, viewport, ); } @@ -218,7 +218,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -233,7 +233,7 @@ where &mut tree.children[1], event.clone(), children.next().unwrap(), - cursor_position, + cursor, renderer, clipboard, shell, @@ -251,7 +251,7 @@ where &mut tree.children[0], event, body_layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -265,42 +265,48 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + 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 = title_bar - .is_over_pick_area(title_bar_layout, cursor_position); - - 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_position, - viewport, - renderer, - ); + 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; + } - (children.next().unwrap(), mouse_interaction) - } else { - (layout, mouse::Interaction::default()) - }; + 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_position, + cursor, viewport, renderer, ) diff --git a/widget/src/pane_grid/draggable.rs b/widget/src/pane_grid/draggable.rs index a9274dad..9d31feb5 100644 --- a/widget/src/pane_grid/draggable.rs +++ b/widget/src/pane_grid/draggable.rs @@ -4,9 +4,5 @@ use crate::core::{Layout, Point}; 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_position: Point, - ) -> bool; + fn can_be_dragged_at(&self, layout: Layout<'_>, cursor: Point) -> bool; } diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 2129937b..2fe79f80 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -122,7 +122,7 @@ where theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, show_controls: bool, ) { @@ -158,7 +158,7 @@ where theme, &inherited_style, controls_layout, - cursor_position, + cursor, viewport, ); } @@ -171,7 +171,7 @@ where theme, &inherited_style, title_layout, - cursor_position, + cursor, viewport, ); } @@ -300,7 +300,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -324,7 +324,7 @@ where &mut tree.children[1], event.clone(), controls_layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -338,7 +338,7 @@ where &mut tree.children[0], event, title_layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -354,7 +354,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -367,7 +367,7 @@ where let title_interaction = self.content.as_widget().mouse_interaction( &tree.children[0], title_layout, - cursor_position, + cursor, viewport, renderer, ); @@ -377,7 +377,7 @@ where let controls_interaction = controls.as_widget().mouse_interaction( &tree.children[1], controls_layout, - cursor_position, + cursor, viewport, renderer, ); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index dcd0629b..d1c03bcd 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -11,8 +11,8 @@ 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, Point, Rectangle, - Shell, Size, Widget, + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, + Size, Widget, }; use crate::overlay::menu::{self, Menu}; use crate::scrollable; @@ -196,7 +196,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -204,7 +204,7 @@ where update( event, layout, - cursor_position, + cursor, shell, self.on_selected.as_ref(), self.selected.as_ref(), @@ -217,11 +217,11 @@ where &self, _tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) + mouse_interaction(layout, cursor) } fn draw( @@ -231,7 +231,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -239,7 +239,7 @@ where renderer, theme, layout, - cursor_position, + cursor, self.padding, self.text_size, self.text_line_height, @@ -431,7 +431,7 @@ where pub fn update<'a, T, Message>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_selected: &dyn Fn(T) -> Message, selected: Option<&T>, @@ -452,7 +452,7 @@ where state.is_open = false; event::Status::Captured - } else if layout.bounds().contains(cursor_position) { + } else if cursor.is_over(&layout.bounds()) { state.is_open = true; state.hovered_option = options.iter().position(|option| Some(option) == selected); @@ -478,7 +478,7 @@ where let state = state(); if state.keyboard_modifiers.command() - && layout.bounds().contains(cursor_position) + && cursor.is_over(&layout.bounds()) && !state.is_open { fn find_next<'a, T: PartialEq>( @@ -529,10 +529,10 @@ where /// Returns the current [`mouse::Interaction`] of a [`PickList`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); if is_mouse_over { mouse::Interaction::Pointer @@ -593,7 +593,7 @@ pub fn draw<'a, T, Renderer>( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, padding: Padding, text_size: Option<f32>, text_line_height: text::LineHeight, @@ -610,7 +610,7 @@ pub fn draw<'a, T, Renderer>( T: ToString + 'a, { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); let is_selected = selected.is_some(); let style = if is_mouse_over { diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 9e1e9131..37c6bc72 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -1,10 +1,9 @@ //! 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, Point, Rectangle, Size, Widget, -}; +use crate::core::{Color, Element, Layout, Length, Rectangle, Size, Widget}; use std::ops::RangeInclusive; @@ -115,7 +114,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 7709125f..06be93c0 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -1,6 +1,7 @@ //! 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::{ @@ -74,7 +75,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { _theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 9dad1e22..9c954275 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -8,8 +8,8 @@ use crate::core::text; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Widget, + Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, + Shell, Widget, }; use crate::{Row, Text}; @@ -229,7 +229,7 @@ where _state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -237,7 +237,7 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(&layout.bounds()) { shell.publish(self.on_click.clone()); return event::Status::Captured; @@ -253,11 +253,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(&layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -271,11 +271,10 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&layout.bounds()); let mut children = layout.children(); diff --git a/widget/src/row.rs b/widget/src/row.rs index 3ce363f8..1db22416 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -6,8 +6,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, + Widget, }; /// A container that distributes its contents horizontally. @@ -155,7 +155,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -169,7 +169,7 @@ where state, event.clone(), layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -182,7 +182,7 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { @@ -192,11 +192,7 @@ where .zip(layout.children()) .map(|((child, state), layout)| { child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, + state, layout, cursor, viewport, renderer, ) }) .max() @@ -210,7 +206,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { for ((child, state), layout) in self @@ -219,15 +215,9 @@ where .zip(&tree.children) .zip(layout.children()) { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); } } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 272bd2b3..d703e6ae 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,9 +1,10 @@ //! 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, Point, Rectangle, Size, Widget, + Color, Element, Layout, Length, Pixels, Rectangle, Size, Widget, }; pub use crate::style::rule::{Appearance, FillMode, StyleSheet}; @@ -86,7 +87,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 12e544c5..e5cda4df 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -222,7 +222,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -231,18 +231,18 @@ where tree.state.downcast_mut::<State>(), event, layout, - cursor_position, + cursor, clipboard, shell, &self.vertical, self.horizontal.as_ref(), &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { + |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -258,7 +258,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( @@ -266,18 +266,18 @@ where renderer, theme, layout, - cursor_position, + cursor, &self.vertical, self.horizontal.as_ref(), &self.style, - |renderer, layout, cursor_position, viewport| { + |renderer, layout, cursor, viewport| { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, - cursor_position, + cursor, viewport, ) }, @@ -288,21 +288,21 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { mouse_interaction( tree.state.downcast_ref::<State>(), layout, - cursor_position, + cursor, &self.vertical, self.horizontal.as_ref(), - |layout, cursor_position, viewport| { + |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -428,7 +428,7 @@ pub fn update<Message>( state: &mut State, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, vertical: &Properties, @@ -437,13 +437,13 @@ pub fn update<Message>( update_content: impl FnOnce( Event, Layout<'_>, - Point, + mouse::Cursor, &mut dyn Clipboard, &mut Shell<'_, Message>, ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(&bounds); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); @@ -452,28 +452,21 @@ pub fn update<Message>( Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); let event_status = { - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + state.offset(bounds, content_bounds) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + state.offset(bounds, content_bounds), + ) + } + _ => mouse::Cursor::Unavailable, }; - update_content( - event.clone(), - content, - cursor_position, - clipboard, - shell, - ) + update_content(event.clone(), content, cursor, clipboard, shell) }; if let event::Status::Captured = event_status { @@ -487,76 +480,71 @@ pub fn update<Message>( return event::Status::Ignored; } - if mouse_over_scrollable { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - 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), - }; + let Some(cursor_position) = cursor_over_scrollable else { + return event::Status::Ignored + }; + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + 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) + }; - state.scroll(delta, bounds, content_bounds); + movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); + state.scroll(delta, 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 { .. } => { + state.scroll_area_touched_at = Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); + + state.scroll(delta, bounds, content_bounds); - 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 { .. } => { state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, - ); - - state.scroll(delta, 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; + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } } - - return event::Status::Captured; + 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 { @@ -700,17 +688,17 @@ pub fn update<Message>( pub fn mouse_interaction( state: &State, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, vertical: &Properties, horizontal: Option<&Properties>, content_interaction: impl FnOnce( Layout<'_>, - Point, + mouse::Cursor, &Rectangle, ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(&bounds); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); @@ -719,7 +707,7 @@ pub fn mouse_interaction( Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() @@ -728,17 +716,18 @@ pub fn mouse_interaction( } else { let offset = state.offset(bounds, content_bounds); - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + offset) + } + _ => mouse::Cursor::Unavailable, }; content_interaction( content_layout, - cursor_position, + cursor, &Rectangle { y: bounds.y + offset.y, x: bounds.x + offset.x, @@ -754,11 +743,11 @@ pub fn draw<Renderer>( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, vertical: &Properties, horizontal: Option<&Properties>, style: &<Renderer::Theme as StyleSheet>::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, @@ -770,18 +759,19 @@ pub fn draw<Renderer>( let scrollbars = Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let mouse_over_scrollable = bounds.contains(cursor_position); + let cursor_over_scrollable = cursor.position_over(&bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); + scrollbars.is_mouse_over(cursor); let offset = state.offset(bounds, content_bounds); - let cursor_position = if mouse_over_scrollable - && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + offset) + } + _ => mouse::Cursor::Unavailable, }; // Draw inner content @@ -793,7 +783,7 @@ pub fn draw<Renderer>( draw_content( renderer, content_layout, - cursor_position, + cursor, &Rectangle { y: bounds.y + offset.y, x: bounds.x + offset.x, @@ -858,7 +848,7 @@ pub fn draw<Renderer>( if let Some(scrollbar) = scrollbars.y { let style = if state.y_scroller_grabbed_at.is_some() { theme.dragging(style) - } else if mouse_over_scrollable { + } else if cursor_over_scrollable.is_some() { theme.hovered(style, mouse_over_y_scrollbar) } else { theme.active(style) @@ -871,7 +861,7 @@ pub fn draw<Renderer>( if let Some(scrollbar) = scrollbars.x { let style = if state.x_scroller_grabbed_at.is_some() { theme.dragging_horizontal(style) - } else if mouse_over_scrollable { + } else if cursor_over_scrollable.is_some() { theme.hovered_horizontal(style, mouse_over_x_scrollbar) } else { theme.active_horizontal(style) @@ -885,7 +875,7 @@ pub fn draw<Renderer>( draw_content( renderer, content_layout, - cursor_position, + cursor, &Rectangle { x: bounds.x + offset.x, y: bounds.y + offset.y, @@ -1283,17 +1273,21 @@ impl Scrollbars { } } - fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { - ( - 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), - ) + 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> { diff --git a/widget/src/slider.rs b/widget/src/slider.rs index c2498b87..4f35805c 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -183,7 +183,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -191,7 +191,7 @@ where update( event, layout, - cursor_position, + cursor, shell, tree.state.downcast_mut::<State>(), &mut self.value, @@ -209,13 +209,13 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( renderer, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), self.value, &self.range, @@ -228,15 +228,11 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - layout, - cursor_position, - tree.state.downcast_ref::<State>(), - ) + mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) } } @@ -260,7 +256,7 @@ where pub fn update<Message, T>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, state: &mut State, value: &mut T, @@ -275,7 +271,7 @@ where { let is_dragging = state.is_dragging; - let mut change = || { + let mut change = |cursor_position: Point| { let bounds = layout.bounds(); let new_value = if cursor_position.x <= bounds.x { *range.start() @@ -309,8 +305,10 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); + if let Some(cursor_position) = + cursor.position_over(&layout.bounds()) + { + change(cursor_position); state.is_dragging = true; return event::Status::Captured; @@ -331,7 +329,7 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { - change(); + let _ = cursor.position().map(change); return event::Status::Captured; } @@ -346,7 +344,7 @@ where pub fn draw<T, R>( renderer: &mut R, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, value: T, range: &RangeInclusive<T>, @@ -358,7 +356,7 @@ pub fn draw<T, R>( R::Theme: StyleSheet, { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); let style = if state.is_dragging { style_sheet.dragging(style) @@ -444,11 +442,11 @@ pub fn draw<T, R>( /// Computes the current [`mouse::Interaction`] of a [`Slider`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); if state.is_dragging { mouse::Interaction::Grabbing diff --git a/widget/src/space.rs b/widget/src/space.rs index e1e09d5a..9a5385e8 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -1,9 +1,10 @@ //! 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, Point, Rectangle, Size, Widget}; +use crate::core::{Element, Layout, Length, Rectangle, Size, Widget}; /// An amount of empty space. /// @@ -69,7 +70,7 @@ where _theme: &Renderer::Theme, _style: &renderer::Style, _layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { } diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 89017fcf..1ccc5d62 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -1,10 +1,11 @@ //! 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, Point, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; use std::path::PathBuf; @@ -143,7 +144,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let Size { width, height } = renderer.dimensions(&self.handle); diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 8f243c1a..5b3705d4 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -207,14 +207,14 @@ where renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, value: Option<&Value>, ) { draw( renderer, theme, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), value.unwrap_or(&self.value), &self.placeholder, @@ -298,7 +298,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -306,7 +306,7 @@ where update( event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -329,14 +329,14 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( renderer, theme, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), &self.value, &self.placeholder, @@ -354,11 +354,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_input.is_none()) + mouse_interaction(layout, cursor, self.on_input.is_none()) } } @@ -528,7 +528,7 @@ where pub fn update<'a, Message, Renderer>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -550,10 +550,14 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - let is_clicked = - layout.bounds().contains(cursor_position) && on_input.is_some(); - state.is_focused = if is_clicked { + 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(); @@ -566,7 +570,7 @@ where None }; - if is_clicked { + if let Some(cursor_position) = click_position { let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; @@ -944,7 +948,7 @@ pub fn draw<Renderer>( renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, value: &Value, placeholder: &str, @@ -967,7 +971,7 @@ pub fn draw<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&bounds); let appearance = if is_disabled { theme.disabled(style) @@ -1154,10 +1158,10 @@ pub fn draw<Renderer>( /// Computes the current [`mouse::Interaction`] of the [`TextInput`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, is_disabled: bool, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(&layout.bounds()) { if is_disabled { mouse::Interaction::NotAllowed } else { diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index b1ba65c9..28715c5a 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -7,8 +7,8 @@ use crate::core::renderer; use crate::core::text; use crate::core::widget::Tree; use crate::core::{ - Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, - Rectangle, Shell, Widget, + Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, + Shell, Widget, }; use crate::{Row, Text}; @@ -202,14 +202,14 @@ where _state: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + 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)) => { - let mouse_over = layout.bounds().contains(cursor_position); + let mouse_over = cursor.is_over(&layout.bounds()); if mouse_over { shell.publish((self.on_toggle)(!self.is_toggled)); @@ -227,11 +227,11 @@ where &self, _state: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { + if cursor.is_over(&layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -245,7 +245,7 @@ where theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { /// Makes sure that the border radius of the toggler looks good at every size. @@ -278,7 +278,7 @@ where let toggler_layout = children.next().unwrap(); let bounds = toggler_layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.is_over(&layout.bounds()); let style = if is_mouse_over { theme.hovered(&self.style, self.is_toggled) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 084650d1..024cfa9c 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -9,7 +9,7 @@ use crate::core::renderer; use crate::core::text; use crate::core::widget::Tree; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; use crate::Text; @@ -136,7 +136,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -145,7 +145,7 @@ where &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -156,14 +156,14 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, - cursor_position, + cursor, viewport, renderer, ) @@ -176,7 +176,7 @@ where theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( @@ -185,7 +185,7 @@ where theme, inherited_style, layout, - cursor_position, + cursor, viewport, ); @@ -196,7 +196,7 @@ where theme, inherited_style, layout, - cursor_position, + cursor, viewport, self.position, self.gap, @@ -206,7 +206,7 @@ where |renderer, limits| { Widget::<(), Renderer>::layout(tooltip, renderer, limits) }, - |renderer, defaults, layout, cursor_position, viewport| { + |renderer, defaults, layout, viewport| { Widget::<(), Renderer>::draw( tooltip, &Tree::empty(), @@ -214,7 +214,7 @@ where theme, defaults, layout, - cursor_position, + cursor, viewport, ); }, @@ -270,7 +270,7 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, viewport: &Rectangle, position: Position, gap: f32, @@ -278,13 +278,7 @@ pub fn draw<Renderer>( snap_within_viewport: bool, style: &<Renderer::Theme as container::StyleSheet>::Style, layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, - draw_text: impl FnOnce( - &mut Renderer, - &renderer::Style, - Layout<'_>, - Point, - &Rectangle, - ), + draw_text: impl FnOnce(&mut Renderer, &renderer::Style, Layout<'_>, &Rectangle), ) where Renderer: core::Renderer, Renderer::Theme: container::StyleSheet, @@ -293,7 +287,7 @@ pub fn draw<Renderer>( let bounds = layout.bounds(); - if bounds.contains(cursor_position) { + if let Some(cursor_position) = cursor.position_over(&bounds) { let style = theme.appearance(style); let defaults = renderer::Style { @@ -380,7 +374,6 @@ pub fn draw<Renderer>( ), &text_layout, ), - cursor_position, viewport, ) }); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index b14e5401..47b14d57 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -180,7 +180,7 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -188,7 +188,7 @@ where update( event, layout, - cursor_position, + cursor, shell, tree.state.downcast_mut::<State>(), &mut self.value, @@ -206,13 +206,13 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { draw( renderer, layout, - cursor_position, + cursor, tree.state.downcast_ref::<State>(), self.value, &self.range, @@ -225,15 +225,11 @@ where &self, tree: &Tree, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - layout, - cursor_position, - tree.state.downcast_ref::<State>(), - ) + mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) } } @@ -257,7 +253,7 @@ where pub fn update<Message, T>( event: Event, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, state: &mut State, value: &mut T, @@ -272,8 +268,9 @@ where { let is_dragging = state.is_dragging; - let mut change = || { + 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 { @@ -307,8 +304,10 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); + if let Some(cursor_position) = + cursor.position_over(&layout.bounds()) + { + change(cursor_position); state.is_dragging = true; return event::Status::Captured; @@ -329,7 +328,7 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { if is_dragging { - change(); + let _ = cursor.position().map(change); return event::Status::Captured; } @@ -344,7 +343,7 @@ where pub fn draw<T, R>( renderer: &mut R, layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, value: T, range: &RangeInclusive<T>, @@ -356,7 +355,7 @@ pub fn draw<T, R>( R::Theme: StyleSheet, { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.position_over(&bounds).is_some(); let style = if state.is_dragging { style_sheet.dragging(style) @@ -442,11 +441,11 @@ pub fn draw<T, R>( /// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`]. pub fn mouse_interaction( layout: Layout<'_>, - cursor_position: Point, + cursor: mouse::Cursor, state: &State, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over = cursor.position_over(&bounds).is_some(); if state.is_dragging { mouse::Interaction::Grabbing -- cgit From 5c8cfb411ed0a9a6e55bd1193cd7e97252e63d28 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 8 Jun 2023 20:16:46 +0200 Subject: Take `Rectangle` by value in `Cursor` API --- widget/src/button.rs | 8 ++++---- widget/src/checkbox.rs | 6 +++--- widget/src/image/viewer.rs | 2 +- widget/src/mouse_area.rs | 2 +- widget/src/overlay/menu.rs | 10 ++++------ widget/src/pane_grid.rs | 2 +- widget/src/pane_grid/content.rs | 2 +- widget/src/pick_list.rs | 8 ++++---- widget/src/radio.rs | 6 +++--- widget/src/scrollable.rs | 6 +++--- widget/src/slider.rs | 7 +++---- widget/src/text_input.rs | 6 +++--- widget/src/toggler.rs | 6 +++--- widget/src/tooltip.rs | 2 +- widget/src/vertical_slider.rs | 7 +++---- 15 files changed, 38 insertions(+), 42 deletions(-) (limited to 'widget/src') diff --git a/widget/src/button.rs b/widget/src/button.rs index 89af341c..0f3cbde2 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -311,7 +311,7 @@ pub fn update<'a, Message: Clone>( if on_press.is_some() { let bounds = layout.bounds(); - if cursor.is_over(&bounds) { + if cursor.is_over(bounds) { let state = state(); state.is_pressed = true; @@ -330,7 +330,7 @@ pub fn update<'a, Message: Clone>( let bounds = layout.bounds(); - if cursor.is_over(&bounds) { + if cursor.is_over(bounds) { shell.publish(on_press); } @@ -364,7 +364,7 @@ pub fn draw<'a, Renderer: crate::core::Renderer>( where Renderer::Theme: StyleSheet, { - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); let styling = if !is_enabled { style_sheet.disabled(style) @@ -440,7 +440,7 @@ pub fn mouse_interaction( cursor: mouse::Cursor, is_enabled: bool, ) -> mouse::Interaction { - let is_mouse_over = cursor.is_over(&layout.bounds()); + let is_mouse_over = cursor.is_over(layout.bounds()); if is_mouse_over && is_enabled { mouse::Interaction::Pointer diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 86f573dd..aa0bff42 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -212,7 +212,7 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - let mouse_over = cursor.is_over(&layout.bounds()); + let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { shell.publish((self.on_toggle)(!self.is_checked)); @@ -234,7 +234,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if cursor.is_over(&layout.bounds()) { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -251,7 +251,7 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let is_mouse_over = cursor.is_over(&layout.bounds()); + let is_mouse_over = cursor.is_over(layout.bounds()); let mut children = layout.children(); diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index a2ee1e5c..8040d6bd 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -286,7 +286,7 @@ where ) -> mouse::Interaction { let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); if state.is_cursor_grabbed() { mouse::Interaction::Grabbing diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 423f070c..da7dc88f 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -240,7 +240,7 @@ fn update<Message: Clone, Renderer>( cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, ) -> event::Status { - if !cursor.is_over(&layout.bounds()) { + if !cursor.is_over(layout.bounds()) { return event::Status::Ignored; } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index fe1175ac..b699def0 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -376,9 +376,7 @@ where ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let bounds = layout.bounds(); - - if cursor.is_over(&bounds) { + if cursor.is_over(layout.bounds()) { if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { *self.last_selection = Some(option.clone()); @@ -388,7 +386,7 @@ where } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = - cursor.position_in(&layout.bounds()) + cursor.position_in(layout.bounds()) { let text_size = self .text_size @@ -404,7 +402,7 @@ where } Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = - cursor.position_in(&layout.bounds()) + cursor.position_in(layout.bounds()) { let text_size = self .text_size @@ -438,7 +436,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let is_mouse_over = cursor.is_over(&layout.bounds()); + let is_mouse_over = cursor.is_over(layout.bounds()); if is_mouse_over { mouse::Interaction::Pointer diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 7e0198fe..040d6bb3 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -524,7 +524,7 @@ pub fn update<'a, Message, T: Draggable>( | Event::Touch(touch::Event::FingerPressed { .. }) => { let bounds = layout.bounds(); - if let Some(cursor_position) = cursor.position_over(&bounds) { + if let Some(cursor_position) = cursor.position_over(bounds) { event_status = event::Status::Captured; match on_resize { diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index fe5c6af0..c28ae6e3 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -113,7 +113,7 @@ where let title_bar_layout = children.next().unwrap(); let body_layout = children.next().unwrap(); - let show_controls = cursor.is_over(&bounds); + let show_controls = cursor.is_over(bounds); self.body.as_widget().draw( &tree.children[0], diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index d1c03bcd..dc53a9c8 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -452,7 +452,7 @@ where state.is_open = false; event::Status::Captured - } else if cursor.is_over(&layout.bounds()) { + } else if cursor.is_over(layout.bounds()) { state.is_open = true; state.hovered_option = options.iter().position(|option| Some(option) == selected); @@ -478,7 +478,7 @@ where let state = state(); if state.keyboard_modifiers.command() - && cursor.is_over(&layout.bounds()) + && cursor.is_over(layout.bounds()) && !state.is_open { fn find_next<'a, T: PartialEq>( @@ -532,7 +532,7 @@ pub fn mouse_interaction( cursor: mouse::Cursor, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); if is_mouse_over { mouse::Interaction::Pointer @@ -610,7 +610,7 @@ pub fn draw<'a, T, Renderer>( T: ToString + 'a, { let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); let is_selected = selected.is_some(); let style = if is_mouse_over { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 9c954275..5b883147 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -237,7 +237,7 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if cursor.is_over(&layout.bounds()) { + if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); return event::Status::Captured; @@ -257,7 +257,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if cursor.is_over(&layout.bounds()) { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -274,7 +274,7 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let is_mouse_over = cursor.is_over(&layout.bounds()); + let is_mouse_over = cursor.is_over(layout.bounds()); let mut children = layout.children(); diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e5cda4df..d90aca2b 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -443,7 +443,7 @@ pub fn update<Message>( ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(&bounds); + let cursor_over_scrollable = cursor.position_over(bounds); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); @@ -698,7 +698,7 @@ pub fn mouse_interaction( ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(&bounds); + let cursor_over_scrollable = cursor.position_over(bounds); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); @@ -759,7 +759,7 @@ pub fn draw<Renderer>( let scrollbars = Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let cursor_over_scrollable = cursor.position_over(&bounds); + let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 4f35805c..3ea4391b 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -305,8 +305,7 @@ where 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()) + if let Some(cursor_position) = cursor.position_over(layout.bounds()) { change(cursor_position); state.is_dragging = true; @@ -356,7 +355,7 @@ pub fn draw<T, R>( R::Theme: StyleSheet, { let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); let style = if state.is_dragging { style_sheet.dragging(style) @@ -446,7 +445,7 @@ pub fn mouse_interaction( state: &State, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); if state.is_dragging { mouse::Interaction::Grabbing diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 5b3705d4..272263f9 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -552,7 +552,7 @@ where let state = state(); let click_position = if on_input.is_some() { - cursor.position_over(&layout.bounds()) + cursor.position_over(layout.bounds()) } else { None }; @@ -971,7 +971,7 @@ pub fn draw<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - let is_mouse_over = cursor.is_over(&bounds); + let is_mouse_over = cursor.is_over(bounds); let appearance = if is_disabled { theme.disabled(style) @@ -1161,7 +1161,7 @@ pub fn mouse_interaction( cursor: mouse::Cursor, is_disabled: bool, ) -> mouse::Interaction { - if cursor.is_over(&layout.bounds()) { + if cursor.is_over(layout.bounds()) { if is_disabled { mouse::Interaction::NotAllowed } else { diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 28715c5a..8b51f2d0 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -209,7 +209,7 @@ where ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let mouse_over = cursor.is_over(&layout.bounds()); + let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { shell.publish((self.on_toggle)(!self.is_toggled)); @@ -231,7 +231,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if cursor.is_over(&layout.bounds()) { + if cursor.is_over(layout.bounds()) { mouse::Interaction::Pointer } else { mouse::Interaction::default() @@ -278,7 +278,7 @@ where let toggler_layout = children.next().unwrap(); let bounds = toggler_layout.bounds(); - let is_mouse_over = cursor.is_over(&layout.bounds()); + let is_mouse_over = cursor.is_over(layout.bounds()); let style = if is_mouse_over { theme.hovered(&self.style, self.is_toggled) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 024cfa9c..d425de01 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -287,7 +287,7 @@ pub fn draw<Renderer>( let bounds = layout.bounds(); - if let Some(cursor_position) = cursor.position_over(&bounds) { + if let Some(cursor_position) = cursor.position_over(bounds) { let style = theme.appearance(style); let defaults = renderer::Style { diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 47b14d57..91f2b466 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -304,8 +304,7 @@ where 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()) + if let Some(cursor_position) = cursor.position_over(layout.bounds()) { change(cursor_position); state.is_dragging = true; @@ -355,7 +354,7 @@ pub fn draw<T, R>( R::Theme: StyleSheet, { let bounds = layout.bounds(); - let is_mouse_over = cursor.position_over(&bounds).is_some(); + let is_mouse_over = cursor.is_over(bounds); let style = if state.is_dragging { style_sheet.dragging(style) @@ -445,7 +444,7 @@ pub fn mouse_interaction( state: &State, ) -> mouse::Interaction { let bounds = layout.bounds(); - let is_mouse_over = cursor.position_over(&bounds).is_some(); + let is_mouse_over = cursor.is_over(bounds); if state.is_dragging { mouse::Interaction::Grabbing -- cgit From 27639c4ce6161fa07986c2f1d472a8a259ae2129 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 8 Jun 2023 21:03:24 +0200 Subject: Fix mouse interactions in `Scrollable` --- widget/src/scrollable.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index d90aca2b..5c00ee20 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -480,10 +480,6 @@ pub fn update<Message>( return event::Status::Ignored; } - let Some(cursor_position) = cursor_over_scrollable else { - return event::Status::Ignored - }; - match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { let delta = match delta { @@ -512,12 +508,20 @@ pub fn update<Message>( { 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, @@ -559,6 +563,10 @@ pub fn update<Message>( 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, @@ -585,6 +593,10 @@ pub fn update<Message>( 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) { @@ -625,6 +637,10 @@ pub fn update<Message>( } 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( @@ -652,6 +668,10 @@ pub fn update<Message>( 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) { -- cgit From 38da9535831d4371cac0bf21282d4c1a6a82c209 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Mon, 12 Jun 2023 13:15:11 -0700 Subject: Only scroll w/ wheel if over scrollable --- widget/src/scrollable.rs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5c00ee20..010befac 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -482,6 +482,10 @@ pub fn update<Message>( 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 (?) -- cgit From 55dc3b5619392f4a20389255708c61082b3d4c1a Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Sat, 18 Feb 2023 14:31:38 -0800 Subject: Introduce internal `overlay::Nested` for `UserInterface` --- widget/src/lazy.rs | 9 +++++++-- widget/src/lazy/component.rs | 9 +++++++-- widget/src/lazy/responsive.rs | 9 +++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) (limited to 'widget/src') diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 92a611c3..89376136 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -377,9 +377,14 @@ where .unwrap_or(event::Status::Ignored) } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) + overlay.is_over(layout, renderer, cursor_position) }) .unwrap_or_default() } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index f462c8cf..edd0c2a2 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -655,9 +655,14 @@ where event_status } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) + overlay.is_over(layout, renderer, cursor_position) }) .unwrap_or_default() } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index bd6385bc..c00b8618 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -409,9 +409,14 @@ where .unwrap_or(event::Status::Ignored) } - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) + overlay.is_over(layout, renderer, cursor_position) }) .unwrap_or_default() } -- cgit From b0205e03d8e4794850e55e8c4bf83a40dd41aa9d Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Sun, 19 Feb 2023 17:43:13 -0800 Subject: Use nested for lazy widgets --- widget/src/lazy.rs | 33 ++++++++++++++++++--------------- widget/src/lazy/component.rs | 42 +++++++++++++++++++++++++----------------- widget/src/lazy/responsive.rs | 27 +++++++++++++++------------ 3 files changed, 58 insertions(+), 44 deletions(-) (limited to 'widget/src') diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 89376136..4903d0ed 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -20,6 +20,7 @@ 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; @@ -260,14 +261,17 @@ where .unwrap(), tree: &mut tree.children[0], overlay_builder: |element, tree| { - element.as_widget_mut().overlay(tree, layout, renderer) + 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::Element::position(overlay)); + let has_overlay = + overlay.with_overlay_maybe(|overlay| overlay.position()); has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) @@ -285,8 +289,8 @@ where tree: &'a mut Tree, #[borrows(mut element, mut tree)] - #[covariant] - overlay: Option<overlay::Element<'this, Message, Renderer>>, + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Message, Renderer>>>, } struct Overlay<'a, Message, Renderer>(Option<Inner<'a, Message, Renderer>>); @@ -301,19 +305,20 @@ impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> { impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> { fn with_overlay_maybe<T>( &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f) + 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 overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.0 - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + self.0.as_mut().unwrap().with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) } } @@ -329,9 +334,7 @@ where position: Point, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) + overlay.layout(renderer, bounds, position) }) .unwrap_or_default() } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index edd0c2a2..f955d9dd 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -9,6 +9,7 @@ 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; @@ -455,11 +456,18 @@ where 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, - ) + element + .as_mut() + .unwrap() + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout, + renderer, + ) + .map(|overlay| { + RefCell::new(Nested::new(overlay)) + }) }, ) }, @@ -468,7 +476,7 @@ where )); let has_overlay = overlay.0.as_ref().unwrap().with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) + overlay.as_ref().map(|nested| nested.borrow().position()) }); has_overlay.map(|position| { @@ -503,8 +511,8 @@ struct Inner<'a, 'b, Message, Renderer, Event, S> { types: PhantomData<(Message, Event, S)>, #[borrows(mut instance, mut tree)] - #[covariant] - overlay: Option<overlay::Element<'this, Event, Renderer>>, + #[not_covariant] + overlay: Option<RefCell<Nested<'this, Event, Renderer>>>, } struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { @@ -516,7 +524,7 @@ impl<'a, 'b, Message, Renderer, Event, S> { fn with_overlay_maybe<T>( &self, - f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T, ) -> Option<T> { self.overlay .as_ref() @@ -524,14 +532,14 @@ impl<'a, 'b, Message, Renderer, Event, S> .0 .as_ref() .unwrap() - .borrow_overlay() - .as_ref() - .map(f) + .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 overlay::Element<'_, Event, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T, ) -> Option<T> { self.overlay .as_mut() @@ -539,7 +547,9 @@ impl<'a, 'b, Message, Renderer, Event, S> .0 .as_mut() .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + .with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) } } @@ -556,9 +566,7 @@ where position: Point, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) + overlay.layout(renderer, bounds, position) }) .unwrap_or_default() } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index c00b8618..07300857 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -9,6 +9,7 @@ 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}; @@ -298,13 +299,13 @@ where element .as_widget_mut() .overlay(tree, content_layout, renderer) + .map(|overlay| RefCell::new(Nested::new(overlay))) }, } .build(); - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); + let has_overlay = + overlay.with_overlay_maybe(|overlay| overlay.position()); has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) @@ -329,23 +330,27 @@ struct Overlay<'a, 'b, Message, Renderer> { types: PhantomData<Message>, #[borrows(mut content, mut tree)] - #[covariant] - overlay: Option<overlay::Element<'this, Message, Renderer>>, + #[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(&overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.borrow_overlay().as_ref().map(f) + 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 overlay::Element<'_, Message, Renderer>) -> T, + f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T, ) -> Option<T> { - self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + self.with_overlay_mut(|overlay| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) } } @@ -361,9 +366,7 @@ where position: Point, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) + overlay.layout(renderer, bounds, position) }) .unwrap_or_default() } -- cgit From 87db76a11ffd0e3e2350c2b2531fde1c56ffeea6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 14 Jun 2023 11:25:05 +0200 Subject: Make `overlay::Menu` publish messages on selection --- widget/src/overlay/menu.rs | 40 +++++++++++++++++++++++----------------- widget/src/pick_list.rs | 44 +++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) (limited to 'widget/src') diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b699def0..ccf4dfb5 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -19,7 +19,7 @@ pub use iced_style::menu::{Appearance, StyleSheet}; /// A list of selectable options. #[allow(missing_debug_implementations)] -pub struct Menu<'a, T, Renderer = crate::Renderer> +pub struct Menu<'a, T, Message, Renderer = crate::Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -27,7 +27,7 @@ where state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + on_selected: Box<dyn FnMut(T) -> Message + 'a>, width: f32, padding: Padding, text_size: Option<f32>, @@ -37,9 +37,10 @@ where style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, T, Renderer> Menu<'a, T, Renderer> +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, @@ -50,13 +51,13 @@ where state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + on_selected: impl FnMut(T) -> Message + 'a, ) -> Self { Menu { state, options, hovered_option, - last_selection, + on_selected: Box::new(on_selected), width: 0.0, padding: Padding::ZERO, text_size: None, @@ -121,7 +122,7 @@ where /// 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<Message: 'a>( + pub fn overlay( self, position: Point, target_height: f32, @@ -174,7 +175,10 @@ where Renderer::Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet, { - pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self + pub fn new<T>( + menu: Menu<'a, T, Message, Renderer>, + target_height: f32, + ) -> Self where T: Clone + ToString, { @@ -182,7 +186,7 @@ where state, options, hovered_option, - last_selection, + on_selected, width, padding, font, @@ -195,7 +199,7 @@ where let container = Container::new(Scrollable::new(List { options, hovered_option, - last_selection, + on_selected, font, text_size, text_line_height, @@ -306,14 +310,14 @@ where } } -struct List<'a, T, Renderer> +struct List<'a, T, Message, Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { options: &'a [T], hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + on_selected: Box<dyn FnMut(T) -> Message + 'a>, padding: Padding, text_size: Option<f32>, text_line_height: text::LineHeight, @@ -323,7 +327,7 @@ where } impl<'a, T, Message, Renderer> Widget<Message, Renderer> - for List<'a, T, Renderer> + for List<'a, T, Message, Renderer> where T: Clone + ToString, Renderer: text::Renderer, @@ -372,14 +376,15 @@ where cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, + 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) { - *self.last_selection = Some(option.clone()); + shell.publish((self.on_selected)(option.clone())); + return event::Status::Captured; } } } @@ -417,7 +422,8 @@ where if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); + shell.publish((self.on_selected)(option.clone())); + return event::Status::Captured; } } } @@ -521,7 +527,7 @@ where } } -impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>> +impl<'a, T, Message, Renderer> From<List<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where T: ToString + Clone, @@ -529,7 +535,7 @@ where Renderer: 'a + text::Renderer, Renderer::Theme: StyleSheet, { - fn from(list: List<'a, T, Renderer>) -> Self { + fn from(list: List<'a, T, Message, Renderer>) -> Self { Element::new(list) } } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index dc53a9c8..832aae6b 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -157,11 +157,11 @@ where From<<Renderer::Theme as StyleSheet>::Style>, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<State<T>>() + tree::Tag::of::<State>() } fn state(&self) -> tree::State { - tree::State::new(State::<T>::new()) + tree::State::new(State::new()) } fn width(&self) -> Length { @@ -209,7 +209,7 @@ where self.on_selected.as_ref(), self.selected.as_ref(), &self.options, - || tree.state.downcast_mut::<State<T>>(), + || tree.state.downcast_mut::<State>(), ) } @@ -249,7 +249,7 @@ where self.selected.as_ref(), &self.handle, &self.style, - || tree.state.downcast_ref::<State<T>>(), + || tree.state.downcast_ref::<State>(), ) } @@ -259,7 +259,7 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - let state = tree.state.downcast_mut::<State<T>>(); + let state = tree.state.downcast_mut::<State>(); overlay( layout, @@ -269,6 +269,7 @@ where self.text_shaping, self.font.unwrap_or_else(|| renderer.default_font()), &self.options, + &self.on_selected, self.style.clone(), ) } @@ -295,15 +296,14 @@ where /// The local state of a [`PickList`]. #[derive(Debug)] -pub struct State<T> { +pub struct State { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, - last_selection: Option<T>, } -impl<T> State<T> { +impl State { /// Creates a new [`State`] for a [`PickList`]. pub fn new() -> Self { Self { @@ -311,12 +311,11 @@ impl<T> State<T> { keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), - last_selection: Option::default(), } } } -impl<T> Default for State<T> { +impl Default for State { fn default() -> Self { Self::new() } @@ -436,7 +435,7 @@ pub fn update<'a, T, Message>( on_selected: &dyn Fn(T) -> Message, selected: Option<&T>, options: &[T], - state: impl FnOnce() -> &'a mut State<T>, + state: impl FnOnce() -> &'a mut State, ) -> event::Status where T: PartialEq + Clone + 'a, @@ -446,7 +445,7 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - let event_status = if state.is_open { + 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; @@ -460,16 +459,6 @@ where event::Status::Captured } else { event::Status::Ignored - }; - - if let Some(last_selection) = state.last_selection.take() { - shell.publish((on_selected)(last_selection)); - - state.is_open = false; - - event::Status::Captured - } else { - event_status } } Event::Mouse(mouse::Event::WheelScrolled { @@ -544,12 +533,13 @@ pub fn mouse_interaction( /// Returns the current overlay of a [`PickList`]. pub fn overlay<'a, T, Message, Renderer>( layout: Layout<'_>, - state: &'a mut State<T>, + 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 @@ -570,7 +560,11 @@ where &mut state.menu, options, &mut state.hovered_option, - &mut state.last_selection, + |option| { + state.is_open = false; + + (on_selected)(option) + }, ) .width(bounds.width) .padding(padding) @@ -603,7 +597,7 @@ pub fn draw<'a, T, Renderer>( selected: Option<&T>, handle: &Handle<Renderer::Font>, style: &<Renderer::Theme as StyleSheet>::Style, - state: impl FnOnce() -> &'a State<T>, + state: impl FnOnce() -> &'a State, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, -- cgit From 3a5519d01216197024ba7166a16a971320bcd4fb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 21 Jun 2023 01:53:36 +0200 Subject: Update `ouroboros` dependency --- widget/src/lazy.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'widget/src') diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 4903d0ed..da287f06 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -279,11 +279,7 @@ where } #[self_referencing] -struct Inner<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a, -{ +struct Inner<'a, Message: 'a, Renderer: 'a> { cell: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>, element: Element<'static, Message, Renderer>, tree: &'a mut Tree, -- cgit From fa04f40524ab7a4ee23bcbc09bc4960c05b192db Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" <austinmreppert@protonmail.com> Date: Fri, 26 May 2023 20:27:17 -0400 Subject: Make vertical scroll properties optional Co-Authored-By: Austin M. Reppert <austinmreppert@gmail.com> --- widget/src/scrollable.rs | 130 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 40 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 010befac..1a326848 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -29,8 +29,7 @@ where id: Option<Id>, width: Length, height: Length, - vertical: Properties, - horizontal: Option<Properties>, + scrollbar_properties: ScrollbarProperties, content: Element<'a, Message, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, style: <Renderer::Theme as StyleSheet>::Style, @@ -47,8 +46,7 @@ where id: None, width: Length::Shrink, height: Length::Shrink, - vertical: Properties::default(), - horizontal: None, + scrollbar_properties: Default::default(), content: content.into(), on_scroll: None, style: Default::default(), @@ -73,15 +71,12 @@ where self } - /// Configures the vertical scrollbar of the [`Scrollable`] . - pub fn vertical_scroll(mut self, properties: Properties) -> Self { - self.vertical = properties; - self - } - - /// Configures the horizontal scrollbar of the [`Scrollable`] . - pub fn horizontal_scroll(mut self, properties: Properties) -> Self { - self.horizontal = Some(properties); + /// Configures the scrollbar(s) of the [`Scrollable`] . + pub fn scrollbar_properties( + mut self, + scrollbar_properties: ScrollbarProperties, + ) -> Self { + self.scrollbar_properties = scrollbar_properties; self } @@ -103,6 +98,43 @@ where } } +/// Properties of the scrollbar(s) within a [`Scrollable`]. +#[derive(Debug)] +pub enum ScrollbarProperties { + /// Vertical Scrollbar. + Vertical(Properties), + /// Horizontal Scrollbar. + Horizontal(Properties), + /// Both Vertical and Horizontal Scrollbars. + Both(Properties, Properties), +} + +impl ScrollbarProperties { + /// Returns the horizontal [`Properties`] of the [`ScrollbarProperties`]. + pub fn horizontal(&self) -> Option<&Properties> { + match self { + Self::Horizontal(properties) => Some(properties), + Self::Both(_, properties) => Some(properties), + _ => None, + } + } + + /// Returns the vertical [`Properties`] of the [`ScrollbarProperties`]. + pub fn vertical(&self) -> Option<&Properties> { + match self { + Self::Vertical(properties) => Some(properties), + Self::Both(properties, _) => Some(properties), + _ => None, + } + } +} + +impl Default for ScrollbarProperties { + fn default() -> Self { + Self::Vertical(Properties::default()) + } +} + /// Properties of a scrollbar within a [`Scrollable`]. #[derive(Debug)] pub struct Properties { @@ -186,7 +218,8 @@ where limits, self.width, self.height, - self.horizontal.is_some(), + self.scrollbar_properties.horizontal().is_some(), + self.scrollbar_properties.vertical().is_some(), |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -234,8 +267,7 @@ where cursor, clipboard, shell, - &self.vertical, - self.horizontal.as_ref(), + &self.scrollbar_properties, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -267,8 +299,7 @@ where theme, layout, cursor, - &self.vertical, - self.horizontal.as_ref(), + &self.scrollbar_properties, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -296,8 +327,7 @@ where tree.state.downcast_ref::<State>(), layout, cursor, - &self.vertical, - self.horizontal.as_ref(), + &self.scrollbar_properties, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -400,19 +430,24 @@ pub fn layout<Renderer>( width: Length, height: Length, horizontal_enabled: bool, + vertical_enabled: bool, 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, 0.0), + Size::new(limits.min().width, limits.min().height), Size::new( if horizontal_enabled { f32::INFINITY } else { limits.max().width }, - f32::MAX, + if vertical_enabled { + f32::MAX + } else { + limits.max().height + }, ), ); @@ -431,8 +466,7 @@ pub fn update<Message>( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, update_content: impl FnOnce( Event, @@ -449,7 +483,7 @@ pub fn update<Message>( let content_bounds = content.bounds(); let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + Scrollbars::new(state, &scrollbar_properties, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -531,6 +565,17 @@ pub fn update<Message>( cursor_position.y - scroll_box_touched_at.y, ); + let delta = Vector::new( + delta.x + * scrollbar_properties + .horizontal() + .map_or(0.0, |_| 1.0), + delta.y + * scrollbar_properties + .vertical() + .map_or(0.0, |_| 1.0), + ); + state.scroll(delta, bounds, content_bounds); state.scroll_area_touched_at = Some(cursor_position); @@ -713,8 +758,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -728,7 +772,7 @@ pub fn mouse_interaction( let content_bounds = content_layout.bounds(); let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + Scrollbars::new(state, scrollbar_properties, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -768,8 +812,7 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, style: &<Renderer::Theme as StyleSheet>::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -781,7 +824,7 @@ pub fn draw<Renderer>( let content_bounds = content_layout.bounds(); let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + Scrollbars::new(state, &scrollbar_properties, bounds, content_bounds); let cursor_over_scrollable = cursor.position_over(bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = @@ -1157,22 +1200,30 @@ impl Scrollbars { /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. fn new( state: &State, - vertical: &Properties, - horizontal: Option<&Properties>, + scrollbar_properties: &ScrollbarProperties, bounds: Rectangle, content_bounds: Rectangle, ) -> Self { let offset = state.offset(bounds, content_bounds); - let show_scrollbar_x = horizontal.and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) + let show_scrollbar_x = + scrollbar_properties.horizontal().and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); + + let show_scrollbar_y = scrollbar_properties.vertical().and_then(|v| { + if content_bounds.height > bounds.height { + Some(v) } else { None } }); - let y_scrollbar = if content_bounds.height > bounds.height { + let y_scrollbar = if let Some(vertical) = show_scrollbar_y { let Properties { width, margin, @@ -1240,9 +1291,8 @@ impl Scrollbars { // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar // is present - let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| { - vertical.width.max(vertical.scroller_width) + vertical.margin - }); + 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; -- cgit From 493571695a8853ee91309a92d04b8dbea29bab8d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Tue, 27 Jun 2023 22:30:54 +0200 Subject: Rename `ScrollbarProperties` to `Direction` in `scrollable` --- widget/src/scrollable.rs | 105 ++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 57 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 1a326848..82f71dff 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -29,7 +29,7 @@ where id: Option<Id>, width: Length, height: Length, - scrollbar_properties: ScrollbarProperties, + direction: Direction, content: Element<'a, Message, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, style: <Renderer::Theme as StyleSheet>::Style, @@ -46,7 +46,7 @@ where id: None, width: Length::Shrink, height: Length::Shrink, - scrollbar_properties: Default::default(), + direction: Default::default(), content: content.into(), on_scroll: None, style: Default::default(), @@ -71,12 +71,9 @@ where self } - /// Configures the scrollbar(s) of the [`Scrollable`] . - pub fn scrollbar_properties( - mut self, - scrollbar_properties: ScrollbarProperties, - ) -> Self { - self.scrollbar_properties = scrollbar_properties; + /// Sets the [`Direction`] of the [`Scrollable`] . + pub fn direction(mut self, direction: Direction) -> Self { + self.direction = direction; self } @@ -98,45 +95,50 @@ where } } -/// Properties of the scrollbar(s) within a [`Scrollable`]. -#[derive(Debug)] -pub enum ScrollbarProperties { - /// Vertical Scrollbar. +/// The direction of [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Direction { + /// Vertical scrolling Vertical(Properties), - /// Horizontal Scrollbar. + /// Horizontal scrolling Horizontal(Properties), - /// Both Vertical and Horizontal Scrollbars. - Both(Properties, Properties), + /// Both vertical and horizontal scrolling + Both { + /// The properties of the vertical scrollbar. + vertical: Properties, + /// The properties of the horizontal scrollbar. + horizontal: Properties, + }, } -impl ScrollbarProperties { - /// Returns the horizontal [`Properties`] of the [`ScrollbarProperties`]. +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(_, properties) => Some(properties), + Self::Both { horizontal, .. } => Some(horizontal), _ => None, } } - /// Returns the vertical [`Properties`] of the [`ScrollbarProperties`]. + /// Returns the [`Properties`] of the vertical scrollbar, if any. pub fn vertical(&self) -> Option<&Properties> { match self { Self::Vertical(properties) => Some(properties), - Self::Both(properties, _) => Some(properties), + Self::Both { vertical, .. } => Some(vertical), _ => None, } } } -impl Default for ScrollbarProperties { +impl Default for Direction { fn default() -> Self { Self::Vertical(Properties::default()) } } /// Properties of a scrollbar within a [`Scrollable`]. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Properties { width: f32, margin: f32, @@ -218,8 +220,7 @@ where limits, self.width, self.height, - self.scrollbar_properties.horizontal().is_some(), - self.scrollbar_properties.vertical().is_some(), + &self.direction, |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -267,7 +268,7 @@ where cursor, clipboard, shell, - &self.scrollbar_properties, + &self.direction, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -299,7 +300,7 @@ where theme, layout, cursor, - &self.scrollbar_properties, + &self.direction, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -327,7 +328,7 @@ where tree.state.downcast_ref::<State>(), layout, cursor, - &self.scrollbar_properties, + &self.direction, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -429,8 +430,7 @@ pub fn layout<Renderer>( limits: &layout::Limits, width: Length, height: Length, - horizontal_enabled: bool, - vertical_enabled: bool, + direction: &Direction, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits.width(width).height(height); @@ -438,12 +438,12 @@ pub fn layout<Renderer>( let child_limits = layout::Limits::new( Size::new(limits.min().width, limits.min().height), Size::new( - if horizontal_enabled { + if direction.horizontal().is_some() { f32::INFINITY } else { limits.max().width }, - if vertical_enabled { + if direction.vertical().is_some() { f32::MAX } else { limits.max().height @@ -466,7 +466,7 @@ pub fn update<Message>( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, update_content: impl FnOnce( Event, @@ -482,8 +482,7 @@ pub fn update<Message>( let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let scrollbars = - Scrollbars::new(state, &scrollbar_properties, 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); @@ -567,13 +566,8 @@ pub fn update<Message>( let delta = Vector::new( delta.x - * scrollbar_properties - .horizontal() - .map_or(0.0, |_| 1.0), - delta.y - * scrollbar_properties - .vertical() - .map_or(0.0, |_| 1.0), + * direction.horizontal().map_or(0.0, |_| 1.0), + delta.y * direction.vertical().map_or(0.0, |_| 1.0), ); state.scroll(delta, bounds, content_bounds); @@ -758,7 +752,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -771,8 +765,7 @@ pub fn mouse_interaction( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbars = - Scrollbars::new(state, scrollbar_properties, 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); @@ -812,7 +805,7 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, style: &<Renderer::Theme as StyleSheet>::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -823,8 +816,7 @@ pub fn draw<Renderer>( let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbars = - Scrollbars::new(state, &scrollbar_properties, bounds, content_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) = @@ -1200,22 +1192,21 @@ impl Scrollbars { /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. fn new( state: &State, - scrollbar_properties: &ScrollbarProperties, + direction: &Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Self { let offset = state.offset(bounds, content_bounds); - let show_scrollbar_x = - scrollbar_properties.horizontal().and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); + let show_scrollbar_x = direction.horizontal().and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); - let show_scrollbar_y = scrollbar_properties.vertical().and_then(|v| { + let show_scrollbar_y = direction.vertical().and_then(|v| { if content_bounds.height > bounds.height { Some(v) } else { -- cgit From 412e15b170a61f7d7369122d7d0b089491e1b0ea Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Tue, 27 Jun 2023 22:41:16 +0200 Subject: Require a `Direction` when computing `State::offset` in `scrollable` --- widget/src/scrollable.rs | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 82f71dff..5bc6914c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -358,10 +358,11 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = tree - .state - .downcast_ref::<State>() - .offset(bounds, content_bounds); + let offset = tree.state.downcast_ref::<State>().offset( + &self.direction, + bounds, + content_bounds, + ); overlay.translate(Vector::new(-offset.x, -offset.y)) }) @@ -493,7 +494,8 @@ pub fn update<Message>( if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => { mouse::Cursor::Available( - cursor_position + state.offset(bounds, content_bounds), + cursor_position + + state.offset(direction, bounds, content_bounds), ) } _ => mouse::Cursor::Unavailable, @@ -564,12 +566,6 @@ pub fn update<Message>( cursor_position.y - scroll_box_touched_at.y, ); - let delta = Vector::new( - delta.x - * direction.horizontal().map_or(0.0, |_| 1.0), - delta.y * direction.vertical().map_or(0.0, |_| 1.0), - ); - state.scroll(delta, bounds, content_bounds); state.scroll_area_touched_at = Some(cursor_position); @@ -775,7 +771,7 @@ pub fn mouse_interaction( { mouse::Interaction::Idle } else { - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -822,7 +818,7 @@ pub fn draw<Renderer>( let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -1161,16 +1157,25 @@ impl State { ); } - /// Returns the scrolling offset of the [`State`], given the bounds of the - /// [`Scrollable`] and its contents. + /// Returns the scrolling offset of the [`State`], given a [`Direction`], + /// the bounds of the [`Scrollable`] and its contents. pub fn offset( &self, + direction: &Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Vector { Vector::new( - self.offset_x.absolute(bounds.width, content_bounds.width), - self.offset_y.absolute(bounds.height, content_bounds.height), + if direction.horizontal().is_some() { + self.offset_x.absolute(bounds.width, content_bounds.width) + } else { + 0.0 + }, + if direction.vertical().is_some() { + self.offset_y.absolute(bounds.height, content_bounds.height) + } else { + 0.0 + }, ) } @@ -1196,7 +1201,7 @@ impl Scrollbars { bounds: Rectangle, content_bounds: Rectangle, ) -> Self { - let offset = state.offset(bounds, content_bounds); + let offset = state.offset(direction, bounds, content_bounds); let show_scrollbar_x = direction.horizontal().and_then(|h| { if content_bounds.width > bounds.width { -- cgit From 1c26440f0bd8f7a002946524dd4d522ba9fb7f29 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Tue, 27 Jun 2023 22:46:04 +0200 Subject: Use `Option::filter` instead of `and_then` in `scrollable` --- widget/src/scrollable.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5bc6914c..473124ca 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1203,21 +1203,13 @@ impl Scrollbars { ) -> Self { let offset = state.offset(direction, bounds, content_bounds); - let show_scrollbar_x = direction.horizontal().and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); + let show_scrollbar_x = direction + .horizontal() + .filter(|_| content_bounds.width > bounds.width); - let show_scrollbar_y = direction.vertical().and_then(|v| { - if content_bounds.height > bounds.height { - Some(v) - } else { - None - } - }); + let show_scrollbar_y = direction + .vertical() + .filter(|_| content_bounds.height > bounds.height); let y_scrollbar = if let Some(vertical) = show_scrollbar_y { let Properties { -- cgit From 0ae1baa37bd7b6607f79b33b8a6d8c5daafde0b2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 22 Jun 2023 00:38:36 +0200 Subject: Introduce custom backend-specific primitives --- widget/src/canvas/program.rs | 6 +++--- widget/src/qr_code.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'widget/src') diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index 929ee285..b3f6175e 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,7 +1,7 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; use crate::core::Rectangle; -use crate::graphics::geometry::{self, Geometry}; +use crate::graphics::geometry; /// The state and logic of a [`Canvas`]. /// @@ -51,7 +51,7 @@ where theme: &Renderer::Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Geometry>; + ) -> Vec<Renderer::Geometry>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -93,7 +93,7 @@ where theme: &Renderer::Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Geometry> { + ) -> Vec<Renderer::Geometry> { T::draw(self, state, renderer, theme, bounds, cursor) } diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 06be93c0..51a541fd 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -7,6 +7,7 @@ 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; @@ -121,7 +122,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { let translation = Vector::new(bounds.x, bounds.y); renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(geometry.0); + renderer.draw(vec![geometry]); }); } } -- cgit From 0cc85c7820136f3ad858c287e8e35884604e7bd0 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" <austinmreppert@gmail.com> Date: Thu, 29 Jun 2023 02:14:21 -0400 Subject: Add touch support for toggler. (#1935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add touch support for toggler. * Fix formatting. * Fix consistency of imports in `iced_widget::toggler` --------- Co-authored-by: Austin M. Reppert <austinmreppert@protonmail.com> Co-authored-by: Héctor Ramón Jiménez <hector0193@gmail.com> --- widget/src/toggler.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 8b51f2d0..1b31765f 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -5,6 +5,7 @@ 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, @@ -208,7 +209,8 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { -- cgit From 4f066b516bd7c5a8a3a55f01d09d650e10567839 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Mon, 12 Jun 2023 21:04:43 -0700 Subject: Add scrollable alignment option --- widget/src/scrollable.rs | 109 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 8 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 473124ca..b6111975 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -143,6 +143,7 @@ pub struct Properties { width: f32, margin: f32, scroller_width: f32, + alignment: Alignment, } impl Default for Properties { @@ -151,6 +152,7 @@ impl Default for Properties { width: 10.0, margin: 0.0, scroller_width: 10.0, + alignment: Alignment::Start, } } } @@ -178,6 +180,31 @@ impl Properties { 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 Alignment { + fn aligned(self, offset: f32, viewport: f32, content: f32) -> f32 { + match self { + Alignment::Start => offset, + Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> @@ -485,6 +512,15 @@ pub fn update<Message>( let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); + 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 (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -535,7 +571,11 @@ pub fn update<Message>( mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; - state.scroll(delta, bounds, content_bounds); + state.scroll( + aligned_delta(delta, vertical_alignment, horizontal_alignment), + bounds, + content_bounds, + ); notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); @@ -566,7 +606,15 @@ pub fn update<Message>( cursor_position.y - scroll_box_touched_at.y, ); - state.scroll(delta, bounds, content_bounds); + state.scroll( + aligned_delta( + delta, + vertical_alignment, + horizontal_alignment, + ), + bounds, + content_bounds, + ); state.scroll_area_touched_at = Some(cursor_position); @@ -610,6 +658,7 @@ pub fn update<Message>( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, + vertical_alignment, ), bounds, content_bounds, @@ -643,6 +692,7 @@ pub fn update<Message>( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, + vertical_alignment, ), bounds, content_bounds, @@ -685,6 +735,7 @@ pub fn update<Message>( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, + horizontal_alignment, ), bounds, content_bounds, @@ -718,6 +769,7 @@ pub fn update<Message>( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, + horizontal_alignment, ), bounds, content_bounds, @@ -1166,13 +1218,22 @@ impl State { content_bounds: Rectangle, ) -> Vector { Vector::new( - if direction.horizontal().is_some() { - self.offset_x.absolute(bounds.width, content_bounds.width) + if let Some(horizontal) = direction.horizontal() { + horizontal.alignment.aligned( + self.offset_x.absolute(bounds.width, content_bounds.width), + bounds.width, + content_bounds.width, + ) } else { 0.0 }, - if direction.vertical().is_some() { - self.offset_y.absolute(bounds.height, content_bounds.height) + if let Some(vertical) = direction.vertical() { + vertical.alignment.aligned( + self.offset_y + .absolute(bounds.height, content_bounds.height), + bounds.height, + content_bounds.height, + ) } else { 0.0 }, @@ -1216,6 +1277,7 @@ impl Scrollbars { width, margin, scroller_width, + .. } = *vertical; // Adjust the height of the vertical scrollbar if the horizontal scrollbar @@ -1275,6 +1337,7 @@ impl Scrollbars { width, margin, scroller_width, + .. } = *horizontal; // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar @@ -1387,9 +1450,27 @@ impl Scrollbars { } } +fn aligned_delta( + delta: Vector, + vertical_alignment: Alignment, + horizontal_alignment: Alignment, +) -> Vector { + let align = |alignment: Alignment, delta: f32| match alignment { + Alignment::Start => delta, + Alignment::End => -delta, + }; + + Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ) +} + pub(super) mod internals { use crate::core::{Point, Rectangle}; + use super::Alignment; + /// The scrollbar of a [`Scrollable`]. #[derive(Debug, Copy, Clone)] pub struct Scrollbar { @@ -1415,8 +1496,9 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, + alignment: Alignment, ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { // cursor position is unavailable! Set to either end or beginning of scrollbar depending // on where the thumb currently is in the track (self.scroller.bounds.y / self.total_bounds.height).round() @@ -1425,6 +1507,11 @@ pub(super) mod internals { - self.bounds.y - self.scroller.bounds.height * grabbed_at) / (self.bounds.height - self.scroller.bounds.height) + }; + + match alignment { + Alignment::Start => pct, + Alignment::End => 1.0 - pct, } } @@ -1433,14 +1520,20 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, + alignment: Alignment, ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { (self.scroller.bounds.x / self.total_bounds.width).round() } else { (cursor_position.x - self.bounds.x - self.scroller.bounds.width * grabbed_at) / (self.bounds.width - self.scroller.bounds.width) + }; + + match alignment { + Alignment::Start => pct, + Alignment::End => 1.0 - pct, } } } -- cgit From 905c307f0b78d9957a82a95b0ba537be23fa4035 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Tue, 13 Jun 2023 08:22:33 -0700 Subject: Make viewport bounds public --- widget/src/scrollable.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index b6111975..79e5fe3e 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1099,8 +1099,10 @@ impl Offset { pub struct Viewport { offset_x: Offset, offset_y: Offset, - bounds: Rectangle, - content_bounds: Rectangle, + /// The viewport bounds of the [`Scrollable`]. + pub bounds: Rectangle, + /// The content bounds of the [`Scrollable`]. + pub content_bounds: Rectangle, } impl Viewport { -- cgit From d79cedd8da99345947f5b9fcb1ebc3a145cc4112 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Sat, 24 Jun 2023 18:01:14 -0700 Subject: Add alignment to Viewport --- widget/src/scrollable.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 79e5fe3e..3912f445 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -577,7 +577,15 @@ pub fn update<Message>( content_bounds, ); - notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + horizontal_alignment, + vertical_alignment, + shell, + ); return event::Status::Captured; } @@ -623,6 +631,8 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); } @@ -669,6 +679,8 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); @@ -705,6 +717,8 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); } @@ -746,6 +760,8 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); } @@ -782,6 +798,8 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, shell, ); @@ -997,6 +1015,8 @@ fn notify_on_scroll<Message>( on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, + horizontal_alignment: Alignment, + vertical_alignment: Alignment, shell: &mut Shell<'_, Message>, ) { if let Some(on_scroll) = on_scroll { @@ -1011,6 +1031,8 @@ fn notify_on_scroll<Message>( offset_y: state.offset_y, bounds, content_bounds, + horizontal_alignment, + vertical_alignment, }; // Don't publish redundant viewports to shell @@ -1103,6 +1125,10 @@ pub struct Viewport { pub bounds: Rectangle, /// The content bounds of the [`Scrollable`]. pub content_bounds: Rectangle, + /// The horizontal [`Alignment`] of the [`Scrollable`]. + pub horizontal_alignment: Alignment, + /// The vertical [`Alignment`] of the [`Scrollable`]. + pub vertical_alignment: Alignment, } impl Viewport { -- cgit From e5c9dd54b3f51e913f39b38e8907c321c8bfd040 Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Fri, 19 May 2023 11:24:52 +0100 Subject: Add ability to drag pane to the pane grid edges & optional style for dragged pane --- widget/src/pane_grid.rs | 281 +++++++++++++++++++++++++++++------------- widget/src/pane_grid/node.rs | 10 ++ widget/src/pane_grid/state.rs | 89 +++++++++++-- 3 files changed, 281 insertions(+), 99 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 040d6bb3..0a8500dc 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -581,39 +581,49 @@ pub fn update<'a, Message, T: Draggable>( | Event::Touch(touch::Event::FingerLost { .. }) => { if let Some((pane, _)) = action.picked_pane() { if let Some(on_drag) = on_drag { - let dropped_region = - cursor.position().and_then(|cursor_position| { - contents + if let Some(cursor_position) = cursor.position() { + let event = if let Some(edge) = + in_edge(layout, cursor_position) + { + DragEvent::Dropped { + pane, + target: Target::PaneGrid(edge), + } + } else { + let dropped_region = contents .zip(layout.children()) .filter_map(|(target, layout)| { layout_region(layout, cursor_position) .map(|region| (target, region)) }) - .next() - }); - - let event = match dropped_region { - Some(((target, _), region)) if pane != target => { - DragEvent::Dropped { - pane, - target, - region, + .next(); + + match dropped_region { + Some(((target, _), region)) + if pane != target => + { + DragEvent::Dropped { + pane, + target: Target::Pane { + pane: target, + region, + }, + } + } + _ => DragEvent::Canceled { pane }, } - } - _ => DragEvent::Canceled { pane }, - }; + }; - shell.publish(on_drag(event)); + shell.publish(on_drag(event)); + } } - *action = state::Action::Idle; - event_status = event::Status::Captured; } else if action.picked_split().is_some() { - *action = state::Action::Idle; - event_status = event::Status::Captured; } + + *action = state::Action::Idle; } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { @@ -671,13 +681,13 @@ fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> { } let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) { - Region::Left + Region::Edge(Edge::Left) } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) { - Region::Right + Region::Edge(Edge::Right) } else if cursor_position.y < (bounds.y + bounds.height / 3.0) { - Region::Top + Region::Edge(Edge::Top) } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) { - Region::Bottom + Region::Edge(Edge::Bottom) } else { Region::Center }; @@ -833,28 +843,32 @@ pub fn draw<Renderer, T>( let mut render_picked_pane = None; - for ((id, pane), layout) in contents.zip(layout.children()) { + let cursor_in_edge = cursor + .position() + .and_then(|cursor_position| in_edge(layout, cursor_position)); + + 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, layout)); + render_picked_pane = Some((pane, origin, pane_layout)); } Some((dragging, _)) if id != dragging => { draw_pane( pane, renderer, default_style, - layout, + pane_layout, pane_cursor, viewport, ); - if picked_pane.is_some() { + if picked_pane.is_some() && cursor_in_edge.is_none() { if let Some(region) = cursor.position().and_then(|cursor_position| { - layout_region(layout, cursor_position) + layout_region(pane_layout, cursor_position) }) { - let bounds = layout_region_bounds(layout, region); + let bounds = layout_region_bounds(pane_layout, region); let hovered_region_style = theme.hovered_region(style); renderer.fill_quad( @@ -875,7 +889,7 @@ pub fn draw<Renderer, T>( pane, renderer, default_style, - layout, + pane_layout, pane_cursor, viewport, ); @@ -883,6 +897,23 @@ pub fn draw<Renderer, T>( } } + if picked_pane.is_some() { + if let Some(edge) = cursor_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.into(), + 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() { @@ -907,71 +938,131 @@ pub fn draw<Renderer, T>( } } - 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, + 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, }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - highlight.color, - ); + highlight.color, + ); + } } } } -fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle { +const THICKNESS_RATIO: f32 = 25.0; + +fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> { let bounds = layout.bounds(); - match region { - Region::Center => bounds, - Region::Top => Rectangle { - height: bounds.height / 2.0, + 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 }, - Region::Left => Rectangle { - width: bounds.width / 2.0, + Edge::Left => Rectangle { + width: thickness, ..bounds }, - Region::Right => Rectangle { - x: bounds.x + bounds.width / 2.0, - width: bounds.width / 2.0, + Edge::Right => Rectangle { + x: bounds.x + bounds.width - thickness, + width: thickness, ..bounds }, - Region::Bottom => Rectangle { - y: bounds.y + bounds.height / 2.0, - height: bounds.height / 2.0, + 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 { @@ -986,11 +1077,8 @@ pub enum DragEvent { /// The picked [`Pane`]. pane: Pane, - /// The [`Pane`] where the picked one was dropped on. - target: Pane, - - /// The [`Region`] of the target [`Pane`] where the picked one was dropped on. - region: Region, + /// The [`Target`] where the picked [`Pane`] was dropped on. + target: Target, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] @@ -1001,19 +1089,40 @@ pub enum DragEvent { }, } +/// The [`Target`] area a pane can be dropped on. +#[derive(Debug, Clone, Copy)] +pub enum Target { + /// The [`Edge`} of the full [`PaneGrid`]. + PaneGrid(Edge), + /// A single [`Pane`] of the [`PaneGrid`]. + Pane { + /// The targetted [`Pane`]. + pane: Pane, + /// The targetted area of the [`Pane`]. + region: Region, + }, +} + /// The region of a [`Pane`]. #[derive(Debug, Clone, Copy, Default)] pub enum Region { /// Center region. #[default] Center, - /// Top region. + /// Edge region. + Edge(Edge), +} + +/// The edges of an area. +#[derive(Debug, Clone, Copy)] +pub enum Edge { + /// Top edge. Top, - /// Left region. + /// Left edge. Left, - /// Right region. + /// Right edge. Right, - /// Bottom region. + /// Bottom edge. Bottom, } diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs index 3976acd8..6de5920f 100644 --- a/widget/src/pane_grid/node.rs +++ b/widget/src/pane_grid/node.rs @@ -120,6 +120,16 @@ impl Node { }; } + 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); diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 1f034ca3..34781a90 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -3,7 +3,7 @@ //! [`PaneGrid`]: crate::widget::PaneGrid use crate::core::{Point, Size}; use crate::pane_grid::{ - Axis, Configuration, Direction, Node, Pane, Region, Split, + Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, }; use std::collections::HashMap; @@ -173,18 +173,20 @@ impl<T> State<T> { pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) { match region { Region::Center => self.swap(pane, target), - Region::Top => { - self.split_and_swap(Axis::Horizontal, target, pane, true) - } - Region::Bottom => { - self.split_and_swap(Axis::Horizontal, target, pane, false) - } - Region::Left => { - self.split_and_swap(Axis::Vertical, target, pane, true) - } - Region::Right => { - self.split_and_swap(Axis::Vertical, target, pane, false) - } + 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) + } + }, } } @@ -204,6 +206,67 @@ impl<T> State<T> { } } + /// 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_major_node(axis, state, swap); + } + } + + fn split_major_node( + &mut self, + axis: Axis, + state: T, + swap: bool, + ) -> Option<(Pane, Split)> { + let 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 swap { + major_node.split_inverse(new_split, axis, new_pane) + } else { + major_node.split(new_split, axis, new_pane) + }; + + let _ = self.panes.insert(new_pane, state); + let _ = self.maximized.take(); + + Some((new_pane, new_split)) + } + /// Swaps the position of the provided panes in the [`State`]. /// /// If you want to swap panes on drag and drop in your [`PaneGrid`], you -- cgit From 995c7c1ca9793536ad9b9d1cac94ae7b5b9a8f0a Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Mon, 22 May 2023 13:24:48 +0100 Subject: Reuse code --- widget/src/pane_grid/state.rs | 57 +++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 35 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 34781a90..332b6837 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -145,7 +145,22 @@ impl<T> State<T> { pane: &Pane, state: T, ) -> Option<(Pane, Split)> { - let node = self.internal.layout.find(pane)?; + self.split_node(axis, Some(pane), state, false) + } + + 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)?; @@ -159,7 +174,11 @@ impl<T> State<T> { Split(self.internal.last_id) }; - node.split(new_split, axis, new_pane); + 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(); @@ -231,42 +250,10 @@ impl<T> State<T> { swap: bool, ) { if let Some((state, _)) = self.close(pane) { - let _ = self.split_major_node(axis, state, swap); + let _ = self.split_node(axis, None, state, swap); } } - fn split_major_node( - &mut self, - axis: Axis, - state: T, - swap: bool, - ) -> Option<(Pane, Split)> { - let 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 swap { - major_node.split_inverse(new_split, axis, new_pane) - } else { - major_node.split(new_split, axis, new_pane) - }; - - let _ = self.panes.insert(new_pane, state); - let _ = self.maximized.take(); - - Some((new_pane, new_split)) - } - /// Swaps the position of the provided panes in the [`State`]. /// /// If you want to swap panes on drag and drop in your [`PaneGrid`], you -- cgit From f83ee1e9f99d1ec5621b4dba35ce0d0fced95442 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 6 Jul 2023 07:37:25 +0200 Subject: Call `in_edge` only when `picked_pane.is_some()` --- widget/src/pane_grid.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 0a8500dc..213460a5 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -843,9 +843,13 @@ pub fn draw<Renderer, T>( let mut render_picked_pane = None; - let cursor_in_edge = cursor - .position() - .and_then(|cursor_position| in_edge(layout, cursor_position)); + 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 { @@ -862,7 +866,7 @@ pub fn draw<Renderer, T>( viewport, ); - if picked_pane.is_some() && cursor_in_edge.is_none() { + 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) @@ -897,21 +901,19 @@ pub fn draw<Renderer, T>( } } - if picked_pane.is_some() { - if let Some(edge) = cursor_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.into(), - border_width: hovered_region_style.border_width, - border_color: hovered_region_style.border_color, - }, - theme.hovered_region(style).background, - ); - } + 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.into(), + border_width: hovered_region_style.border_width, + border_color: hovered_region_style.border_color, + }, + theme.hovered_region(style).background, + ); } // Render picked pane last -- cgit From ecce8bbcee45dddedef8a33bf3dc086d76c27b39 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 6 Jul 2023 07:37:51 +0200 Subject: Simplify `Target` enum in `widget::pane_grid` --- widget/src/pane_grid.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 213460a5..23ab4181 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -587,7 +587,7 @@ pub fn update<'a, Message, T: Draggable>( { DragEvent::Dropped { pane, - target: Target::PaneGrid(edge), + target: Target::Edge(edge), } } else { let dropped_region = contents @@ -604,10 +604,7 @@ pub fn update<'a, Message, T: Draggable>( { DragEvent::Dropped { pane, - target: Target::Pane { - pane: target, - region, - }, + target: Target::Pane(target, region), } } _ => DragEvent::Canceled { pane }, @@ -1094,15 +1091,10 @@ pub enum DragEvent { /// The [`Target`] area a pane can be dropped on. #[derive(Debug, Clone, Copy)] pub enum Target { - /// The [`Edge`} of the full [`PaneGrid`]. - PaneGrid(Edge), + /// An [`Edge`] of the full [`PaneGrid`]. + Edge(Edge), /// A single [`Pane`] of the [`PaneGrid`]. - Pane { - /// The targetted [`Pane`]. - pane: Pane, - /// The targetted area of the [`Pane`]. - region: Region, - }, + Pane(Pane, Region), } /// The region of a [`Pane`]. -- cgit From c5a623f32b3d972501bb02d87d296381b66f9481 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 6 Jul 2023 07:45:47 +0200 Subject: Introduce `drop` helper to `pane_grid::State` --- widget/src/pane_grid/state.rs | 58 +++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 24 deletions(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 332b6837..6fd15890 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -3,7 +3,7 @@ //! [`PaneGrid`]: crate::widget::PaneGrid use crate::core::{Point, Size}; use crate::pane_grid::{ - Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, + Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, }; use std::collections::HashMap; @@ -148,6 +148,39 @@ impl<T> State<T> { 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, @@ -186,29 +219,6 @@ impl<T> State<T> { Some((new_pane, new_split)) } - /// 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) - } - }, - } - } - fn split_and_swap( &mut self, axis: Axis, -- cgit From 0964f12db7002f535f8eeda1791ea8962be6e71a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 6 Jul 2023 08:26:46 +0200 Subject: Remove useless conversion in `widget::pane_grid` --- widget/src/pane_grid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src') diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 23ab4181..31bb0e86 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -905,7 +905,7 @@ pub fn draw<Renderer, T>( renderer.fill_quad( renderer::Quad { bounds, - border_radius: hovered_region_style.border_radius.into(), + border_radius: hovered_region_style.border_radius, border_width: hovered_region_style.border_width, border_color: hovered_region_style.border_color, }, -- cgit From 4c2aa071a09df076fb92757034c941ed2e680ba7 Mon Sep 17 00:00:00 2001 From: Nick Senger <dev@nsenger.com> Date: Sat, 8 Jul 2023 16:06:26 -0700 Subject: fix: request redraw in component overlay --- widget/src/lazy/component.rs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'widget/src') diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index f955d9dd..c7814966 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -624,6 +624,10 @@ where 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(); -- cgit From 65be3f8b9622306a293984fa29332aceafce5f7b Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Thu, 2 Feb 2023 15:17:42 -0800 Subject: Use overlay for `Tooltip` widget --- widget/src/tooltip.rs | 257 +++++++++++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 97 deletions(-) (limited to 'widget/src') diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index d425de01..e7a9e870 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -1,16 +1,15 @@ //! Display a widget over another. use crate::container; -use crate::core; 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::Tree; +use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, + Vector, }; use crate::Text; @@ -107,14 +106,22 @@ where Renderer: text::Renderer, Renderer::Theme: container::StyleSheet + crate::text::StyleSheet, { - fn children(&self) -> Vec<Tree> { - vec![Tree::new(&self.content)] + fn children(&self) -> Vec<widget::Tree> { + vec![widget::Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { + 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() } @@ -133,7 +140,7 @@ where fn on_event( &mut self, - tree: &mut Tree, + tree: &mut widget::Tree, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -141,6 +148,13 @@ where 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, @@ -154,7 +168,7 @@ where fn mouse_interaction( &self, - tree: &Tree, + tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -171,7 +185,7 @@ where fn draw( &self, - tree: &Tree, + tree: &widget::Tree, renderer: &mut Renderer, theme: &Renderer::Theme, inherited_style: &renderer::Style, @@ -188,50 +202,50 @@ where cursor, viewport, ); - - let tooltip = &self.tooltip; - - draw( - renderer, - theme, - inherited_style, - layout, - cursor, - viewport, - self.position, - self.gap, - self.padding, - self.snap_within_viewport, - &self.style, - |renderer, limits| { - Widget::<(), Renderer>::layout(tooltip, renderer, limits) - }, - |renderer, defaults, layout, viewport| { - Widget::<(), Renderer>::draw( - tooltip, - &Tree::empty(), - renderer, - theme, - defaults, - layout, - cursor, - viewport, - ); - }, - ); } fn overlay<'b>( &'b mut self, - tree: &'b mut Tree, + tree: &'b mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - self.content.as_widget_mut().overlay( + 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 + } } } @@ -264,72 +278,93 @@ pub enum Position { Right, } -/// Draws a [`Tooltip`]. -pub fn draw<Renderer>( - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, +#[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, - snap_within_viewport: bool, - style: &<Renderer::Theme as container::StyleSheet>::Style, - layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, - draw_text: impl FnOnce(&mut Renderer, &renderer::Style, Layout<'_>, &Rectangle), -) where - Renderer: core::Renderer, - Renderer::Theme: container::StyleSheet, -{ - use container::StyleSheet; - - let bounds = layout.bounds(); + style: &'b <Renderer::Theme as container::StyleSheet>::Style, +} - if let Some(cursor_position) = cursor.position_over(bounds) { - let style = theme.appearance(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 defaults = renderer::Style { - text_color: style.text_color.unwrap_or(inherited_style.text_color), - }; + let gap = f32::from(self.gap); - let text_layout = layout_text( + let text_layout = Widget::<(), Renderer>::layout( + self.tooltip, renderer, &layout::Limits::new( Size::ZERO, - snap_within_viewport + self.snap_within_viewport .then(|| viewport.size()) .unwrap_or(Size::INFINITY), ) - .pad(Padding::new(padding)), + .pad(Padding::new(self.padding)), ); + let padding = f32::from(self.padding); + let text_bounds = text_layout.bounds(); - let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0; - let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0; + 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 position { + let offset = match self.position { Position::Top => Vector::new( x_center, - bounds.y - text_bounds.height - gap - padding, + self.content_bounds.y - text_bounds.height - gap - padding, ), Position::Bottom => Vector::new( x_center, - bounds.y + bounds.height + gap + padding, + self.content_bounds.y + + self.content_bounds.height + + gap + + padding, ), Position::Left => Vector::new( - bounds.x - text_bounds.width - gap - padding, + self.content_bounds.x - text_bounds.width - gap - padding, y_center, ), Position::Right => Vector::new( - bounds.x + bounds.width + gap + padding, + self.content_bounds.x + + self.content_bounds.width + + gap + + padding, y_center, ), Position::FollowCursor => Vector::new( - cursor_position.x, - cursor_position.y - text_bounds.height, + self.cursor_position.x, + self.cursor_position.y - text_bounds.height, ), }; @@ -341,7 +376,7 @@ pub fn draw<Renderer>( } }; - if snap_within_viewport { + if self.snap_within_viewport { if tooltip_bounds.x < viewport.x { tooltip_bounds.x = viewport.x; } else if viewport.x + viewport.width @@ -361,21 +396,49 @@ pub fn draw<Renderer>( } } - renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| { - container::draw_background(renderer, &style, tooltip_bounds); - - draw_text( - renderer, - &defaults, - Layout::with_offset( - Vector::new( - tooltip_bounds.x + padding, - tooltip_bounds.y + padding, - ), - &text_layout, - ), - viewport, - ) - }); + layout::Node::with_children( + tooltip_bounds.size(), + vec![text_layout.translate(Vector::new(padding, 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 } } -- cgit From 896a90decbd9ab57488620f27ae5c03ee0b9dab3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 12 Jul 2023 03:48:26 +0200 Subject: Remove useless conversions in `widget::tooltip` --- widget/src/tooltip.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) (limited to 'widget/src') diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index e7a9e870..2dc3da01 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -316,8 +316,6 @@ where ) -> layout::Node { let viewport = Rectangle::with_size(bounds); - let gap = f32::from(self.gap); - let text_layout = Widget::<(), Renderer>::layout( self.tooltip, renderer, @@ -330,8 +328,6 @@ where .pad(Padding::new(self.padding)), ); - let padding = f32::from(self.padding); - let text_bounds = text_layout.bounds(); let x_center = self.content_bounds.x + (self.content_bounds.width - text_bounds.width) / 2.0; @@ -342,24 +338,30 @@ where let offset = match self.position { Position::Top => Vector::new( x_center, - self.content_bounds.y - text_bounds.height - gap - padding, + 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 - + gap - + padding, + + self.gap + + self.padding, ), Position::Left => Vector::new( - self.content_bounds.x - text_bounds.width - gap - padding, + 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 - + gap - + padding, + + self.gap + + self.padding, y_center, ), Position::FollowCursor => Vector::new( @@ -369,10 +371,10 @@ where }; Rectangle { - x: offset.x - padding, - y: offset.y - padding, - width: text_bounds.width + padding * 2.0, - height: text_bounds.height + padding * 2.0, + 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, } }; @@ -398,7 +400,7 @@ where layout::Node::with_children( tooltip_bounds.size(), - vec![text_layout.translate(Vector::new(padding, padding))], + vec![text_layout.translate(Vector::new(self.padding, self.padding))], ) .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y)) } -- cgit From 44460f7b8b9e90c85bd94c19dd15d418e641f8cd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 12 Jul 2023 09:26:13 +0200 Subject: Remove public fields from `Viewport` Let's tackle use cases directly instead! --- widget/src/scrollable.rs | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 3912f445..b6111975 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -577,15 +577,7 @@ pub fn update<Message>( content_bounds, ); - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - horizontal_alignment, - vertical_alignment, - shell, - ); + notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); return event::Status::Captured; } @@ -631,8 +623,6 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); } @@ -679,8 +669,6 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); @@ -717,8 +705,6 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); } @@ -760,8 +746,6 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); } @@ -798,8 +782,6 @@ pub fn update<Message>( on_scroll, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, shell, ); @@ -1015,8 +997,6 @@ fn notify_on_scroll<Message>( on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, - horizontal_alignment: Alignment, - vertical_alignment: Alignment, shell: &mut Shell<'_, Message>, ) { if let Some(on_scroll) = on_scroll { @@ -1031,8 +1011,6 @@ fn notify_on_scroll<Message>( offset_y: state.offset_y, bounds, content_bounds, - horizontal_alignment, - vertical_alignment, }; // Don't publish redundant viewports to shell @@ -1121,14 +1099,8 @@ impl Offset { pub struct Viewport { offset_x: Offset, offset_y: Offset, - /// The viewport bounds of the [`Scrollable`]. - pub bounds: Rectangle, - /// The content bounds of the [`Scrollable`]. - pub content_bounds: Rectangle, - /// The horizontal [`Alignment`] of the [`Scrollable`]. - pub horizontal_alignment: Alignment, - /// The vertical [`Alignment`] of the [`Scrollable`]. - pub vertical_alignment: Alignment, + bounds: Rectangle, + content_bounds: Rectangle, } impl Viewport { -- cgit From 2b2f9c07d8962f8a146794c2fd8f1709f104f4f5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 12 Jul 2023 09:49:14 +0200 Subject: Increase type-safety of `alignment` in `scrollable` --- widget/src/scrollable.rs | 100 ++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 63 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index b6111975..17d55e7c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -295,7 +295,7 @@ where cursor, clipboard, shell, - &self.direction, + self.direction, &self.on_scroll, |event, layout, cursor, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -327,7 +327,7 @@ where theme, layout, cursor, - &self.direction, + self.direction, &self.style, |renderer, layout, cursor, viewport| { self.content.as_widget().draw( @@ -355,7 +355,7 @@ where tree.state.downcast_ref::<State>(), layout, cursor, - &self.direction, + self.direction, |layout, cursor, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -386,7 +386,7 @@ where let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = tree.state.downcast_ref::<State>().offset( - &self.direction, + self.direction, bounds, content_bounds, ); @@ -494,7 +494,7 @@ pub fn update<Message>( cursor: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - direction: &Direction, + direction: Direction, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, update_content: impl FnOnce( Event, @@ -512,15 +512,6 @@ pub fn update<Message>( let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - 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 (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); @@ -571,11 +562,7 @@ pub fn update<Message>( mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; - state.scroll( - aligned_delta(delta, vertical_alignment, horizontal_alignment), - bounds, - content_bounds, - ); + state.scroll(delta, direction, bounds, content_bounds); notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); @@ -606,15 +593,7 @@ pub fn update<Message>( cursor_position.y - scroll_box_touched_at.y, ); - state.scroll( - aligned_delta( - delta, - vertical_alignment, - horizontal_alignment, - ), - bounds, - content_bounds, - ); + state.scroll(delta, direction, bounds, content_bounds); state.scroll_area_touched_at = Some(cursor_position); @@ -658,7 +637,6 @@ pub fn update<Message>( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, - vertical_alignment, ), bounds, content_bounds, @@ -692,7 +670,6 @@ pub fn update<Message>( scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, - vertical_alignment, ), bounds, content_bounds, @@ -735,7 +712,6 @@ pub fn update<Message>( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, - horizontal_alignment, ), bounds, content_bounds, @@ -769,7 +745,6 @@ pub fn update<Message>( scrollbar.scroll_percentage_x( scroller_grabbed_at, cursor_position, - horizontal_alignment, ), bounds, content_bounds, @@ -800,7 +775,7 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor: mouse::Cursor, - direction: &Direction, + direction: Direction, content_interaction: impl FnOnce( Layout<'_>, mouse::Cursor, @@ -853,7 +828,7 @@ pub fn draw<Renderer>( theme: &Renderer::Theme, layout: Layout<'_>, cursor: mouse::Cursor, - direction: &Direction, + direction: Direction, style: &<Renderer::Theme as StyleSheet>::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), ) where @@ -1138,9 +1113,30 @@ impl State { 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) @@ -1213,7 +1209,7 @@ impl State { /// the bounds of the [`Scrollable`] and its contents. pub fn offset( &self, - direction: &Direction, + direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Vector { @@ -1258,7 +1254,7 @@ impl Scrollbars { /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. fn new( state: &State, - direction: &Direction, + direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) -> Self { @@ -1327,6 +1323,7 @@ impl Scrollbars { scroller: internals::Scroller { bounds: scroller_bounds, }, + alignment: vertical.alignment, }) } else { None @@ -1387,6 +1384,7 @@ impl Scrollbars { scroller: internals::Scroller { bounds: scroller_bounds, }, + alignment: horizontal.alignment, }) } else { None @@ -1450,39 +1448,17 @@ impl Scrollbars { } } -fn aligned_delta( - delta: Vector, - vertical_alignment: Alignment, - horizontal_alignment: Alignment, -) -> Vector { - let align = |alignment: Alignment, delta: f32| match alignment { - Alignment::Start => delta, - Alignment::End => -delta, - }; - - Vector::new( - align(horizontal_alignment, delta.x), - align(vertical_alignment, delta.y), - ) -} - pub(super) mod internals { use crate::core::{Point, Rectangle}; use super::Alignment; - /// The scrollbar of a [`Scrollable`]. #[derive(Debug, Copy, Clone)] pub struct Scrollbar { - /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, - /// and the scrollbar margin. pub total_bounds: Rectangle, - - /// The bounds of just the [`Scrollbar`]. pub bounds: Rectangle, - - /// The state of this scrollbar's [`Scroller`]. pub scroller: Scroller, + pub alignment: Alignment, } impl Scrollbar { @@ -1496,7 +1472,6 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, - alignment: Alignment, ) -> f32 { let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { // cursor position is unavailable! Set to either end or beginning of scrollbar depending @@ -1509,7 +1484,7 @@ pub(super) mod internals { / (self.bounds.height - self.scroller.bounds.height) }; - match alignment { + match self.alignment { Alignment::Start => pct, Alignment::End => 1.0 - pct, } @@ -1520,7 +1495,6 @@ pub(super) mod internals { &self, grabbed_at: f32, cursor_position: Point, - alignment: Alignment, ) -> f32 { let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { (self.scroller.bounds.x / self.total_bounds.width).round() @@ -1531,7 +1505,7 @@ pub(super) mod internals { / (self.bounds.width - self.scroller.bounds.width) }; - match alignment { + match self.alignment { Alignment::Start => pct, Alignment::End => 1.0 - pct, } -- cgit From ca2afb04952f177667c4431a102161be9223340f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 12 Jul 2023 10:04:26 +0200 Subject: Disambiguate `offset` from `translation` in `scrollable` --- widget/src/scrollable.rs | 77 +++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 37 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 17d55e7c..45fbd72b 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -198,15 +198,6 @@ pub enum Alignment { End, } -impl Alignment { - fn aligned(self, offset: f32, viewport: f32, content: f32) -> f32 { - match self { - Alignment::Start => offset, - Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), - } - } -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where @@ -385,13 +376,12 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = tree.state.downcast_ref::<State>().offset( - self.direction, - bounds, - content_bounds, - ); + let translation = tree + .state + .downcast_ref::<State>() + .translation(self.direction, bounds, content_bounds); - overlay.translate(Vector::new(-offset.x, -offset.y)) + overlay.translate(Vector::new(-translation.x, -translation.y)) }) } } @@ -522,7 +512,7 @@ pub fn update<Message>( { mouse::Cursor::Available( cursor_position - + state.offset(direction, bounds, content_bounds), + + state.translation(direction, bounds, content_bounds), ) } _ => mouse::Cursor::Unavailable, @@ -798,13 +788,13 @@ pub fn mouse_interaction( { mouse::Interaction::Idle } else { - let offset = state.offset(direction, bounds, content_bounds); + 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 + offset) + mouse::Cursor::Available(cursor_position + translation) } _ => mouse::Cursor::Unavailable, }; @@ -813,8 +803,8 @@ pub fn mouse_interaction( content_layout, cursor, &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, + y: bounds.y + translation.y, + x: bounds.x + translation.x, ..bounds }, ) @@ -845,13 +835,13 @@ pub fn draw<Renderer>( let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); - let offset = state.offset(direction, bounds, content_bounds); + 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 + offset) + mouse::Cursor::Available(cursor_position + translation) } _ => mouse::Cursor::Unavailable, }; @@ -860,15 +850,15 @@ pub fn draw<Renderer>( if scrollbars.active() { renderer.with_layer(bounds, |renderer| { renderer.with_translation( - Vector::new(-offset.x, -offset.y), + Vector::new(-translation.x, -translation.y), |renderer| { draw_content( renderer, content_layout, cursor, &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, + y: bounds.y + translation.y, + x: bounds.x + translation.x, ..bounds }, ); @@ -959,8 +949,8 @@ pub fn draw<Renderer>( content_layout, cursor, &Rectangle { - x: bounds.x + offset.x, - y: bounds.y + offset.y, + x: bounds.x + translation.x, + y: bounds.y + translation.y, ..bounds }, ); @@ -1067,6 +1057,20 @@ impl Offset { } } } + + fn absolute_from_start( + 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`]. @@ -1205,9 +1209,9 @@ impl State { ); } - /// Returns the scrolling offset of the [`State`], given a [`Direction`], + /// Returns the scrolling translation of the [`State`], given a [`Direction`], /// the bounds of the [`Scrollable`] and its contents. - pub fn offset( + fn translation( &self, direction: Direction, bounds: Rectangle, @@ -1215,20 +1219,19 @@ impl State { ) -> Vector { Vector::new( if let Some(horizontal) = direction.horizontal() { - horizontal.alignment.aligned( - self.offset_x.absolute(bounds.width, content_bounds.width), + self.offset_x.absolute_from_start( bounds.width, content_bounds.width, + horizontal.alignment, ) } else { 0.0 }, if let Some(vertical) = direction.vertical() { - vertical.alignment.aligned( - self.offset_y - .absolute(bounds.height, content_bounds.height), + self.offset_y.absolute_from_start( bounds.height, content_bounds.height, + vertical.alignment, ) } else { 0.0 @@ -1258,7 +1261,7 @@ impl Scrollbars { bounds: Rectangle, content_bounds: Rectangle, ) -> Self { - let offset = state.offset(direction, bounds, content_bounds); + let translation = state.translation(direction, bounds, content_bounds); let show_scrollbar_x = direction .horizontal() @@ -1305,7 +1308,7 @@ impl Scrollbars { 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 = offset.y * ratio; + let scroller_offset = translation.y * ratio; let scroller_bounds = Rectangle { x: bounds.x + bounds.width @@ -1366,7 +1369,7 @@ impl Scrollbars { 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 = offset.x * ratio; + let scroller_offset = translation.x * ratio; let scroller_bounds = Rectangle { x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) -- cgit From d07bac36ab528ea5c3f4a09bb0ae010ae1b5c6da Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 12 Jul 2023 10:05:46 +0200 Subject: Rename `absolute_from_start` to `translation` in `scrollable` --- widget/src/scrollable.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 45fbd72b..9a43a978 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1058,7 +1058,7 @@ impl Offset { } } - fn absolute_from_start( + fn translation( self, viewport: f32, content: f32, @@ -1219,7 +1219,7 @@ impl State { ) -> Vector { Vector::new( if let Some(horizontal) = direction.horizontal() { - self.offset_x.absolute_from_start( + self.offset_x.translation( bounds.width, content_bounds.width, horizontal.alignment, @@ -1228,7 +1228,7 @@ impl State { 0.0 }, if let Some(vertical) = direction.vertical() { - self.offset_y.absolute_from_start( + self.offset_y.translation( bounds.height, content_bounds.height, vertical.alignment, -- cgit From ce23e08d0d921040d1e6fb693149e638d291bd16 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Wed, 12 Jul 2023 10:13:15 +0200 Subject: Remove unnecessary cursor unavailability logic in `scrollable` --- widget/src/scrollable.rs | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) (limited to 'widget/src') diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 9a43a978..88746ac4 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1476,20 +1476,14 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { - // cursor position is unavailable! Set to either end or beginning of scrollbar depending - // on where the thumb currently is in the track - (self.scroller.bounds.y / self.total_bounds.height).round() - } else { - (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height) - }; + 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 => pct, - Alignment::End => 1.0 - pct, + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, } } @@ -1499,18 +1493,14 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let pct = if cursor_position.x < 0.0 && cursor_position.y < 0.0 { - (self.scroller.bounds.x / self.total_bounds.width).round() - } else { - (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width) - }; + 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 => pct, - Alignment::End => 1.0 - pct, + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, } } } -- cgit