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/Cargo.toml                     |   37 +
 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 ++++++++++++
 49 files changed, 14380 insertions(+)
 create mode 100644 widget/Cargo.toml
 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')

diff --git a/widget/Cargo.toml b/widget/Cargo.toml
new file mode 100644
index 00000000..fb617079
--- /dev/null
+++ b/widget/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "iced_widget"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+lazy = ["ouroboros"]
+image = ["iced_renderer/image"]
+svg = ["iced_renderer/svg"]
+canvas = ["iced_renderer/geometry"]
+qr_code = ["canvas", "qrcode"]
+
+[dependencies]
+unicode-segmentation = "1.6"
+num-traits = "0.2"
+thiserror = "1"
+
+[dependencies.iced_native]
+version = "0.9"
+path = "../native"
+
+[dependencies.iced_renderer]
+version = "0.1"
+path = "../renderer"
+
+[dependencies.iced_style]
+version = "0.7"
+path = "../style"
+
+[dependencies.ouroboros]
+version = "0.13"
+optional = true
+
+[dependencies.qrcode]
+version = "0.12"
+optional = true
+default-features = false
diff --git a/widget/src/button.rs b/widget/src/button.rs
new file mode 100644
index 00000000..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);
+/// ```
+///
+/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
+#[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.
+//!
+//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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.
+///
+/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](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, &region_a, regions);
+                b.compute_regions(spacing, &region_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, &region_a, splits);
+                b.compute_splits(spacing, &region_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);
+/// ```
+///
+/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
+#[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);
+/// ```
+///
+/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true)
+#[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);
+/// ```
+///
+/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
+#[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);
+/// ```
+/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true)
+#[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')

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/Cargo.toml        | 4 ++--
 widget/src/helpers.rs    | 2 +-
 widget/src/lib.rs        | 4 ++--
 widget/src/scrollable.rs | 2 +-
 widget/src/text_input.rs | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

(limited to 'widget')

diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index fb617079..4c23f3e8 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -15,9 +15,9 @@ unicode-segmentation = "1.6"
 num-traits = "0.2"
 thiserror = "1"
 
-[dependencies.iced_native]
+[dependencies.iced_runtime]
 version = "0.9"
-path = "../native"
+path = "../runtime"
 
 [dependencies.iced_renderer]
 version = "0.1"
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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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/Cargo.toml  | 2 +-
 widget/src/lazy.rs | 6 +-----
 2 files changed, 2 insertions(+), 6 deletions(-)

(limited to 'widget')

diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 40e4db37..14aae72e 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -28,7 +28,7 @@ version = "0.8"
 path = "../style"
 
 [dependencies.ouroboros]
-version = "0.13"
+version = "0.17"
 optional = true
 
 [dependencies.qrcode]
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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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')

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