summaryrefslogtreecommitdiffstats
path: root/native/src/widget
diff options
context:
space:
mode:
Diffstat (limited to 'native/src/widget')
-rw-r--r--native/src/widget/button.rs83
-rw-r--r--native/src/widget/checkbox.rs79
-rw-r--r--native/src/widget/column.rs74
-rw-r--r--native/src/widget/container.rs68
-rw-r--r--native/src/widget/image.rs53
-rw-r--r--native/src/widget/pane_grid.rs639
-rw-r--r--native/src/widget/pane_grid/axis.rs238
-rw-r--r--native/src/widget/pane_grid/configuration.rs26
-rw-r--r--native/src/widget/pane_grid/content.rs213
-rw-r--r--native/src/widget/pane_grid/direction.rs12
-rw-r--r--native/src/widget/pane_grid/node.rs258
-rw-r--r--native/src/widget/pane_grid/pane.rs5
-rw-r--r--native/src/widget/pane_grid/split.rs5
-rw-r--r--native/src/widget/pane_grid/state.rs301
-rw-r--r--native/src/widget/pane_grid/title_bar.rs253
-rw-r--r--native/src/widget/pick_list.rs343
-rw-r--r--native/src/widget/progress_bar.rs23
-rw-r--r--native/src/widget/radio.rs122
-rw-r--r--native/src/widget/row.rs79
-rw-r--r--native/src/widget/rule.rs116
-rw-r--r--native/src/widget/scrollable.rs245
-rw-r--r--native/src/widget/slider.rs182
-rw-r--r--native/src/widget/space.rs16
-rw-r--r--native/src/widget/svg.rs98
-rw-r--r--native/src/widget/text.rs97
-rw-r--r--native/src/widget/text_input.rs837
-rw-r--r--native/src/widget/text_input/cursor.rs186
-rw-r--r--native/src/widget/text_input/editor.rs77
-rw-r--r--native/src/widget/text_input/value.rs125
29 files changed, 3880 insertions, 973 deletions
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs
index 81dbe7c5..7d5eb30c 100644
--- a/native/src/widget/button.rs
+++ b/native/src/widget/button.rs
@@ -1,13 +1,12 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
-//!
-//! [`Button`]: struct.Button.html
-//! [`State`]: struct.State.html
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Rectangle, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
use std::hash::Hash;
@@ -19,6 +18,7 @@ use std::hash::Hash;
/// # type Button<'a, Message> =
/// # iced_native::Button<'a, Message, iced_native::renderer::Null>;
/// #
+/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
@@ -42,13 +42,11 @@ pub struct Button<'a, Message, Renderer: self::Renderer> {
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
+ Message: Clone,
Renderer: self::Renderer,
{
/// Creates a new [`Button`] with some local [`State`] and the given
/// content.
- ///
- /// [`Button`]: struct.Button.html
- /// [`State`]: struct.State.html
pub fn new<E>(state: &'a mut State, content: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -67,56 +65,42 @@ where
}
/// Sets the width of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the minimum width of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn min_width(mut self, min_width: u32) -> Self {
self.min_width = min_width;
self
}
/// Sets the minimum height of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn min_height(mut self, min_height: u32) -> Self {
self.min_height = min_height;
self
}
/// Sets the padding of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn padding(mut self, padding: u16) -> Self {
self.padding = padding;
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
- ///
- /// [`Button`]: struct.Button.html
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
}
/// Sets the style of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -124,8 +108,6 @@ where
}
/// The local state of a [`Button`].
-///
-/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_pressed: bool,
@@ -133,8 +115,6 @@ pub struct State {
impl State {
/// Creates a new [`State`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
@@ -143,8 +123,8 @@ impl State {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Button<'a, Message, Renderer>
where
- Renderer: self::Renderer,
Message: Clone,
+ Renderer: self::Renderer,
{
fn width(&self) -> Length {
self.width
@@ -183,12 +163,9 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -196,26 +173,29 @@ where
if self.on_press.is_some() {
let bounds = layout.bounds();
- self.state.is_pressed = bounds.contains(cursor_position);
+ if bounds.contains(cursor_position) {
+ self.state.is_pressed = true;
+
+ return event::Status::Captured;
+ }
}
}
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Released,
- })
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Ended,
..
}) => {
if let Some(on_press) = self.on_press.clone() {
let bounds = layout.bounds();
- let is_clicked = self.state.is_pressed
- && bounds.contains(cursor_position);
- self.state.is_pressed = false;
+ if self.state.is_pressed {
+ self.state.is_pressed = false;
+
+ if bounds.contains(cursor_position) {
+ messages.push(on_press);
+ }
- if is_clicked {
- messages.push(on_press);
+ return event::Status::Captured;
}
}
}
@@ -227,6 +207,8 @@ where
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -235,6 +217,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
defaults,
@@ -249,6 +232,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.content.hash_layout(state);
}
@@ -259,20 +245,15 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Button`] in your user interface.
///
-/// [`Button`]: struct.Button.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// The default padding of a [`Button`].
- ///
- /// [`Button`]: struct.Button.html
const DEFAULT_PADDING: u16;
/// The style supported by this renderer.
type Style: Default;
/// Draws a [`Button`].
- ///
- /// [`Button`]: struct.Button.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
@@ -289,8 +270,8 @@ pub trait Renderer: crate::Renderer + Sized {
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static + Clone,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer,
{
fn from(
button: Button<'a, Message, Renderer>,
diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs
index 7b2345de..92175b25 100644
--- a/native/src/widget/checkbox.rs
+++ b/native/src/widget/checkbox.rs
@@ -1,11 +1,15 @@
//! Show toggle controls using checkboxes.
use std::hash::Hash;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::row;
+use crate::text;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, row, text, Align, Clipboard, Element, Event, Font, Hasher,
- HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text,
- VerticalAlignment, Widget,
+ Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,
+ Point, Rectangle, Row, Text, VerticalAlignment, Widget,
};
/// A box that can be checked.
@@ -33,7 +37,8 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> {
width: Length,
size: u16,
spacing: u16,
- text_size: u16,
+ text_size: Option<u16>,
+ font: Renderer::Font,
style: Renderer::Style,
}
@@ -48,59 +53,56 @@ impl<Message, Renderer: self::Renderer + text::Renderer>
/// * 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`.
- ///
- /// [`Checkbox`]: struct.Checkbox.html
- pub fn new<F>(is_checked: bool, label: &str, f: F) -> Self
+ pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self
where
F: 'static + Fn(bool) -> Message,
{
Checkbox {
is_checked,
on_toggle: Box::new(f),
- label: String::from(label),
+ label: label.into(),
width: Length::Shrink,
size: <Renderer as self::Renderer>::DEFAULT_SIZE,
spacing: Renderer::DEFAULT_SPACING,
- text_size: <Renderer as text::Renderer>::DEFAULT_SIZE,
+ text_size: None,
+ font: Renderer::Font::default(),
style: Renderer::Style::default(),
}
}
/// Sets the size of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn size(mut self, size: u16) -> Self {
self.size = size;
self
}
/// Sets the width of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the spacing between the [`Checkbox`] and the text.
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
/// Sets the text size of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn text_size(mut self, text_size: u16) -> Self {
- self.text_size = text_size;
+ self.text_size = Some(text_size);
self
}
- /// Sets the style of the [`Checkbox`].
+ /// Sets the [`Font`] of the text of the [`Checkbox`].
///
- /// [`Checkbox`]: struct.Checkbox.html
+ /// [`Font`]: crate::widget::text::Renderer::Font
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
+ /// Sets the style of the [`Checkbox`].
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -136,8 +138,9 @@ where
)
.push(
Text::new(&self.label)
+ .font(self.font)
.width(self.width)
- .size(self.text_size),
+ .size(self.text_size.unwrap_or(renderer.default_size())),
)
.layout(renderer, limits)
}
@@ -150,12 +153,9 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -164,10 +164,14 @@ where
if mouse_over {
messages.push((self.on_toggle)(!self.is_checked));
+
+ return event::Status::Captured;
}
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -176,6 +180,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
let bounds = layout.bounds();
let mut children = layout.children();
@@ -189,8 +194,8 @@ where
defaults,
label_layout.bounds(),
&self.label,
- self.text_size,
- Font::Default,
+ self.text_size.unwrap_or(renderer.default_size()),
+ self.font,
None,
HorizontalAlignment::Left,
VerticalAlignment::Center,
@@ -209,6 +214,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.label.hash(state);
}
}
@@ -218,20 +226,15 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Checkbox`] in your user interface.
///
-/// [`Checkbox`]: struct.Checkbox.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::Renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
/// The default size of a [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
const DEFAULT_SIZE: u16;
/// The default spacing of a [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
const DEFAULT_SPACING: u16;
/// Draws a [`Checkbox`].
@@ -241,8 +244,6 @@ pub trait Renderer: crate::Renderer {
/// * whether the [`Checkbox`] is selected or not
/// * whether the mouse is over the [`Checkbox`] or not
/// * the drawn label of the [`Checkbox`]
- ///
- /// [`Checkbox`]: struct.Checkbox.html
fn draw(
&mut self,
bounds: Rectangle,
@@ -256,8 +257,8 @@ pub trait Renderer: crate::Renderer {
impl<'a, Message, Renderer> From<Checkbox<Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + text::Renderer + row::Renderer,
- Message: 'static,
+ Renderer: 'a + self::Renderer + text::Renderer + row::Renderer,
+ Message: 'a,
{
fn from(
checkbox: Checkbox<Message, Renderer>,
diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs
index a7a6f242..e0e88d31 100644
--- a/native/src/widget/column.rs
+++ b/native/src/widget/column.rs
@@ -1,18 +1,16 @@
//! Distribute content vertically.
use std::hash::Hash;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
use std::u32;
/// A container that distributes its contents vertically.
-///
-/// A [`Column`] will try to fill the horizontal space of its container.
-///
-/// [`Column`]: struct.Column.html
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message, Renderer> {
spacing: u16,
@@ -27,15 +25,11 @@ pub struct Column<'a, Message, Renderer> {
impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
/// Creates an empty [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Column`] with the given elements.
- ///
- /// [`Column`]: struct.Column.html
pub fn with_children(
children: Vec<Element<'a, Message, Renderer>>,
) -> Self {
@@ -62,56 +56,42 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
}
/// Sets the padding of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Column`] in pixels.
- ///
- /// [`Column`]: struct.Column.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
- ///
- /// [`Column`]: struct.Column.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Adds an element to the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -164,9 +144,11 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
- self.children.iter_mut().zip(layout.children()).for_each(
- |(child, layout)| {
+ ) -> event::Status {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .map(|(child, layout)| {
child.widget.on_event(
event.clone(),
layout,
@@ -175,8 +157,8 @@ where
renderer,
clipboard,
)
- },
- );
+ })
+ .fold(event::Status::Ignored, event::Status::merge)
}
fn draw(
@@ -185,23 +167,44 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
- renderer.draw(defaults, &self.children, layout, cursor_position)
+ renderer.draw(
+ defaults,
+ &self.children,
+ layout,
+ cursor_position,
+ viewport,
+ )
}
fn hash_layout(&self, state: &mut Hasher) {
- 0.hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.align_items.hash(state);
self.spacing.hash(state);
+ self.padding.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|(child, layout)| child.widget.overlay(layout))
+ .next()
+ }
}
/// The renderer of a [`Column`].
@@ -209,8 +212,7 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Column`] in your user interface.
///
-/// [`Column`]: struct.Column.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// Draws a [`Column`].
///
@@ -218,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized {
/// - the children of the [`Column`]
/// - the [`Layout`] of the [`Column`] and its children
/// - the cursor position
- ///
- /// [`Column`]: struct.Row.html
- /// [`Layout`]: ../layout/struct.Layout.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
content: &[Element<'_, Message, Self>],
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
}
@@ -234,7 +234,7 @@ impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(
column: Column<'a, Message, Renderer>,
diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs
index 3459a832..65764148 100644
--- a/native/src/widget/container.rs
+++ b/native/src/widget/container.rs
@@ -1,9 +1,11 @@
//! Decorate content and apply alignment.
use std::hash::Hash;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Rectangle, Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
use std::u32;
@@ -13,6 +15,7 @@ use std::u32;
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct Container<'a, Message, Renderer: self::Renderer> {
+ padding: u16,
width: Length,
height: Length,
max_width: u32,
@@ -28,13 +31,12 @@ where
Renderer: self::Renderer,
{
/// Creates an empty [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn new<T>(content: T) -> Self
where
T: Into<Element<'a, Message, Renderer>>,
{
Container {
+ padding: 0,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -46,73 +48,61 @@ where
}
}
+ /// Sets the padding of the [`Container`].
+ pub fn padding(mut self, units: u16) -> Self {
+ self.padding = units;
+ self
+ }
+
/// Sets the width of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Container`] in pixels.
- ///
- /// [`Container`]: struct.Container.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
-
+
/// Sets the content alignment for the horizontal axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn align_x(mut self, alignment: Align) -> Self {
self.horizontal_alignment = alignment;
self
}
/// Sets the content alignment for the vertical axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn align_y(mut self, alignment: Align) -> Self {
self.vertical_alignment = alignment;
self
}
/// Centers the contents in the horizontal axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn center_x(mut self) -> Self {
self.horizontal_alignment = Align::Center;
self
}
/// Centers the contents in the vertical axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn center_y(mut self) -> Self {
self.vertical_alignment = Align::Center;
self
}
/// Sets the style of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -137,19 +127,23 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
+ let padding = f32::from(self.padding);
+
let limits = limits
.loose()
.max_width(self.max_width)
.max_height(self.max_height)
.width(self.width)
- .height(self.height);
+ .height(self.height)
+ .pad(padding);
let mut content = self.content.layout(renderer, &limits.loose());
let size = limits.resolve(content.size());
+ content.move_to(Point::new(padding, padding));
content.align(self.horizontal_alignment, self.vertical_alignment, size);
- layout::Node::with_children(size, vec![content])
+ layout::Node::with_children(size.pad(padding), vec![content])
}
fn on_event(
@@ -160,7 +154,7 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
self.content.widget.on_event(
event,
layout.children().next().unwrap(),
@@ -177,11 +171,13 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
defaults,
layout.bounds(),
cursor_position,
+ viewport,
&self.style,
&self.content,
layout.children().next().unwrap(),
@@ -189,7 +185,10 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
- 0.hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.padding.hash(state);
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
@@ -197,6 +196,13 @@ where
self.content.hash_layout(state);
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.content.overlay(layout.children().next().unwrap())
+ }
}
/// The renderer of a [`Container`].
@@ -204,20 +210,18 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Container`] in your user interface.
///
-/// [`Container`]: struct.Container.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
/// Draws a [`Container`].
- ///
- /// [`Container`]: struct.Container.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
cursor_position: Point,
+ viewport: &Rectangle,
style: &Self::Style,
content: &Element<'_, Message, Self>,
content_layout: Layout<'_>,
@@ -228,7 +232,7 @@ impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(
column: Container<'a, Message, Renderer>,
diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs
index fbe38bfc..51d7ba26 100644
--- a/native/src/widget/image.rs
+++ b/native/src/widget/image.rs
@@ -1,5 +1,6 @@
//! Display images in your user interface.
-use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
+use crate::layout;
+use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};
use std::{
hash::{Hash, Hasher as _},
@@ -27,8 +28,6 @@ pub struct Image {
impl Image {
/// Creates a new [`Image`] with the given path.
- ///
- /// [`Image`]: struct.Image.html
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Image {
handle: handle.into(),
@@ -38,16 +37,12 @@ impl Image {
}
/// Sets the width of the [`Image`] boundaries.
- ///
- /// [`Image`]: struct.Image.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Image`] boundaries.
- ///
- /// [`Image`]: struct.Image.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
@@ -97,11 +92,15 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(self.handle.clone(), layout)
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.handle.hash(state);
self.width.hash(state);
self.height.hash(state);
@@ -109,8 +108,6 @@ where
}
/// An [`Image`] handle.
-///
-/// [`Image`]: struct.Image.html
#[derive(Debug, Clone)]
pub struct Handle {
id: u64,
@@ -120,18 +117,16 @@ pub struct Handle {
impl Handle {
/// Creates an image [`Handle`] pointing to the image of the given path.
///
- /// [`Handle`]: struct.Handle.html
+ /// Makes an educated guess about the image format by examining the data in the file.
pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
Self::from_data(Data::Path(path.into()))
}
/// Creates an image [`Handle`] containing the image pixels directly. This
- /// function expects the input data to be provided as a `Vec<u8>` of BGRA
+ /// function expects the input data to be provided as a `Vec<u8>` of BGRA
/// pixels.
///
/// This is useful if you have already decoded your image.
- ///
- /// [`Handle`]: struct.Handle.html
pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle {
Self::from_data(Data::Pixels {
width,
@@ -142,10 +137,10 @@ impl Handle {
/// Creates an image [`Handle`] containing the image data directly.
///
+ /// Makes an educated guess about the image format by examining the given data.
+ ///
/// This is useful if you already have your image loaded in-memory, maybe
/// because you downloaded or generated it procedurally.
- ///
- /// [`Handle`]: struct.Handle.html
pub fn from_memory(bytes: Vec<u8>) -> Handle {
Self::from_data(Data::Bytes(bytes))
}
@@ -161,29 +156,22 @@ impl Handle {
}
/// Returns the unique identifier of the [`Handle`].
- ///
- /// [`Handle`]: struct.Handle.html
pub fn id(&self) -> u64 {
self.id
}
/// Returns a reference to the image [`Data`].
- ///
- /// [`Data`]: enum.Data.html
pub fn data(&self) -> &Data {
&self.data
}
}
-impl From<String> for Handle {
- fn from(path: String) -> Handle {
- Handle::from_path(path)
- }
-}
-
-impl From<&str> for Handle {
- fn from(path: &str) -> Handle {
- Handle::from_path(path)
+impl<T> From<T> for Handle
+where
+ T: Into<PathBuf>,
+{
+ fn from(path: T) -> Handle {
+ Handle::from_path(path.into())
}
}
@@ -194,8 +182,6 @@ impl Hash for Handle {
}
/// The data of an [`Image`].
-///
-/// [`Image`]: struct.Image.html
#[derive(Clone, Hash)]
pub enum Data {
/// File data
@@ -232,17 +218,12 @@ impl std::fmt::Debug for Data {
/// Your [renderer] will need to implement this trait before being able to use
/// an [`Image`] in your user interface.
///
-/// [`Image`]: struct.Image.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// Returns the dimensions of an [`Image`] located on the given path.
- ///
- /// [`Image`]: struct.Image.html
fn dimensions(&self, handle: &Handle) -> (u32, u32);
/// Draws an [`Image`].
- ///
- /// [`Image`]: struct.Image.html
fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output;
}
diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs
new file mode 100644
index 00000000..ff19cbc2
--- /dev/null
+++ b/native/src/widget/pane_grid.rs
@@ -0,0 +1,639 @@
+//! 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/hecrj/iced/tree/0.2/examples/pane_grid
+mod axis;
+mod configuration;
+mod content;
+mod direction;
+mod node;
+mod pane;
+mod split;
+mod state;
+mod title_bar;
+
+pub use axis::Axis;
+pub use configuration::Configuration;
+pub use content::Content;
+pub use direction::Direction;
+pub use node::Node;
+pub use pane::Pane;
+pub use split::Split;
+pub use state::State;
+pub use title_bar::TitleBar;
+
+use crate::container;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::row;
+use crate::text;
+use crate::{
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, 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_native::{pane_grid, Text};
+/// #
+/// # type PaneGrid<'a, Message> =
+/// # iced_native::PaneGrid<'a, Message, iced_native::renderer::Null>;
+/// #
+/// 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(&mut state, |pane, state| {
+/// pane_grid::Content::new(match state {
+/// PaneState::SomePane => Text::new("This is some pane"),
+/// PaneState::AnotherKindOfPane => Text::new("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: self::Renderer> {
+ state: &'a mut state::Internal,
+ elements: Vec<(Pane, Content<'a, Message, Renderer>)>,
+ width: Length,
+ height: Length,
+ spacing: u16,
+ on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
+ on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
+ on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
+}
+
+impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ /// Creates a [`PaneGrid`] with the given [`State`] and view function.
+ ///
+ /// The view function will be called to display each [`Pane`] present in the
+ /// [`State`].
+ pub fn new<T>(
+ state: &'a mut State<T>,
+ view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>,
+ ) -> Self {
+ let elements = {
+ state
+ .panes
+ .iter_mut()
+ .map(|(pane, pane_state)| (*pane, view(*pane, pane_state)))
+ .collect()
+ };
+
+ Self {
+ state: &mut state.internal,
+ elements,
+ width: Length::Fill,
+ height: Length::Fill,
+ spacing: 0,
+ on_click: None,
+ on_drag: None,
+ on_resize: None,
+ }
+ }
+
+ /// Sets the width of the [`PaneGrid`].
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height of the [`PaneGrid`].
+ pub fn height(mut self, height: Length) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Sets the spacing _between_ the panes of the [`PaneGrid`].
+ pub fn spacing(mut self, units: u16) -> Self {
+ self.spacing = units;
+ 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: u16, f: F) -> Self
+ where
+ F: 'a + Fn(ResizeEvent) -> Message,
+ {
+ self.on_resize = Some((leeway, Box::new(f)));
+ self
+ }
+}
+
+impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ fn click_pane(
+ &mut self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ ) {
+ let mut clicked_region =
+ self.elements.iter().zip(layout.children()).filter(
+ |(_, layout)| layout.bounds().contains(cursor_position),
+ );
+
+ if let Some(((pane, content), layout)) = clicked_region.next() {
+ if let Some(on_click) = &self.on_click {
+ messages.push(on_click(*pane));
+ }
+
+ if let Some(on_drag) = &self.on_drag {
+ if content.can_be_picked_at(layout, cursor_position) {
+ let pane_position = layout.position();
+
+ let origin = cursor_position
+ - Vector::new(pane_position.x, pane_position.y);
+
+ self.state.pick_pane(pane, origin);
+
+ messages.push(on_drag(DragEvent::Picked { pane: *pane }));
+ }
+ }
+ }
+ }
+
+ fn trigger_resize(
+ &mut self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ ) -> event::Status {
+ if let Some((_, on_resize)) = &self.on_resize {
+ if let Some((split, _)) = self.state.picked_split() {
+ let bounds = layout.bounds();
+
+ let splits = self.state.split_regions(
+ f32::from(self.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).max(0.1).min(0.9)
+ }
+ Axis::Vertical => {
+ let position =
+ cursor_position.x - bounds.x - rectangle.x;
+
+ (position / rectangle.width).max(0.1).min(0.9)
+ }
+ };
+
+ messages.push(on_resize(ResizeEvent { split, ratio }));
+
+ return event::Status::Captured;
+ }
+ }
+ }
+
+ event::Status::Ignored
+ }
+}
+
+/// 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,
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer + container::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);
+ let size = limits.resolve(Size::ZERO);
+
+ let regions = self.state.pane_regions(f32::from(self.spacing), size);
+
+ let children = self
+ .elements
+ .iter()
+ .filter_map(|(pane, element)| {
+ let region = regions.get(pane)?;
+ let size = Size::new(region.width, region.height);
+
+ let mut node =
+ element.layout(renderer, &layout::Limits::new(size, size));
+
+ node.move_to(Point::new(region.x, region.y));
+
+ Some(node)
+ })
+ .collect();
+
+ layout::Node::with_children(size, children)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ let mut event_status = event::Status::Ignored;
+
+ match event {
+ Event::Mouse(mouse_event) => match mouse_event {
+ mouse::Event::ButtonPressed(mouse::Button::Left) => {
+ let bounds = layout.bounds();
+
+ if bounds.contains(cursor_position) {
+ event_status = event::Status::Captured;
+
+ match self.on_resize {
+ Some((leeway, _)) => {
+ let relative_cursor = Point::new(
+ cursor_position.x - bounds.x,
+ cursor_position.y - bounds.y,
+ );
+
+ let splits = self.state.split_regions(
+ f32::from(self.spacing),
+ Size::new(bounds.width, bounds.height),
+ );
+
+ let clicked_split = hovered_split(
+ splits.iter(),
+ f32::from(self.spacing + leeway),
+ relative_cursor,
+ );
+
+ if let Some((split, axis)) = clicked_split {
+ self.state.pick_split(&split, axis);
+ } else {
+ self.click_pane(
+ layout,
+ cursor_position,
+ messages,
+ );
+ }
+ }
+ None => {
+ self.click_pane(
+ layout,
+ cursor_position,
+ messages,
+ );
+ }
+ }
+ }
+ }
+ mouse::Event::ButtonReleased(mouse::Button::Left) => {
+ if let Some((pane, _)) = self.state.picked_pane() {
+ if let Some(on_drag) = &self.on_drag {
+ let mut dropped_region = self
+ .elements
+ .iter()
+ .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: *target,
+ }
+ }
+ _ => DragEvent::Canceled { pane },
+ };
+
+ messages.push(on_drag(event));
+ }
+
+ self.state.idle();
+
+ event_status = event::Status::Captured;
+ } else if self.state.picked_split().is_some() {
+ self.state.idle();
+
+ event_status = event::Status::Captured;
+ }
+ }
+ mouse::Event::CursorMoved { .. } => {
+ event_status =
+ self.trigger_resize(layout, cursor_position, messages);
+ }
+ _ => {}
+ },
+ _ => {}
+ }
+
+ if self.state.picked_pane().is_none() {
+ self.elements
+ .iter_mut()
+ .zip(layout.children())
+ .map(|((_, pane), layout)| {
+ pane.on_event(
+ event.clone(),
+ layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ })
+ .fold(event_status, event::Status::merge)
+ } else {
+ event::Status::Captured
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ let picked_split = self
+ .state
+ .picked_split()
+ .or_else(|| match self.on_resize {
+ Some((leeway, _)) => {
+ let bounds = layout.bounds();
+
+ let relative_cursor = Point::new(
+ cursor_position.x - bounds.x,
+ cursor_position.y - bounds.y,
+ );
+
+ let splits = self
+ .state
+ .split_regions(f32::from(self.spacing), bounds.size());
+
+ hovered_split(
+ splits.iter(),
+ f32::from(self.spacing + leeway),
+ relative_cursor,
+ )
+ }
+ None => None,
+ })
+ .map(|(_, axis)| axis);
+
+ self::Renderer::draw(
+ renderer,
+ defaults,
+ &self.elements,
+ self.state.picked_pane(),
+ picked_split,
+ layout,
+ cursor_position,
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash;
+
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.width.hash(state);
+ self.height.hash(state);
+ self.state.hash_layout(state);
+
+ for (_, element) in &self.elements {
+ element.hash_layout(state);
+ }
+ }
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.elements
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|((_, pane), layout)| pane.overlay(layout))
+ .next()
+ }
+}
+
+/// The renderer of a [`PaneGrid`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`PaneGrid`] in your user interface.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer:
+ crate::Renderer + container::Renderer + text::Renderer + Sized
+{
+ /// Draws a [`PaneGrid`].
+ ///
+ /// It receives:
+ /// - the elements of the [`PaneGrid`]
+ /// - the [`Pane`] that is currently being dragged
+ /// - the [`Axis`] that is currently being resized
+ /// - the [`Layout`] of the [`PaneGrid`] and its elements
+ /// - the cursor position
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[(Pane, Content<'_, Message, Self>)],
+ dragging: Option<(Pane, Point)>,
+ resizing: Option<Axis>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Self::Output;
+
+ /// Draws a [`Pane`].
+ ///
+ /// It receives:
+ /// - the [`TitleBar`] of the [`Pane`], if any
+ /// - the [`Content`] of the [`Pane`]
+ /// - the [`Layout`] of the [`Pane`] and its elements
+ /// - the cursor position
+ fn draw_pane<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ style: &Self::Style,
+ title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>,
+ body: (&Element<'_, Message, Self>, Layout<'_>),
+ cursor_position: Point,
+ ) -> Self::Output;
+
+ /// Draws a [`TitleBar`].
+ ///
+ /// It receives:
+ /// - the bounds, style of the [`TitleBar`]
+ /// - the style of the [`TitleBar`]
+ /// - the title of the [`TitleBar`] with its size, font, and bounds
+ /// - the controls of the [`TitleBar`] with their [`Layout`+, if any
+ /// - the cursor position
+ fn draw_title_bar<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ style: &Self::Style,
+ title: &str,
+ title_size: u16,
+ title_font: Self::Font,
+ title_bounds: Rectangle,
+ controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
+ cursor_position: Point,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer + row::Renderer,
+ Message: 'a,
+{
+ fn from(
+ pane_grid: PaneGrid<'a, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(pane_grid)
+ }
+}
+
+/*
+ * Helpers
+ */
+fn hovered_split<'a>(
+ splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
+ spacing: f32,
+ cursor_position: Point,
+) -> Option<(Split, Axis)> {
+ splits
+ .filter_map(|(split, (axis, region, ratio))| {
+ let bounds =
+ axis.split_line_bounds(*region, *ratio, f32::from(spacing));
+
+ if bounds.contains(cursor_position) {
+ Some((*split, *axis))
+ } else {
+ None
+ }
+ })
+ .next()
+}
diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs
new file mode 100644
index 00000000..2320cb7c
--- /dev/null
+++ b/native/src/widget/pane_grid/axis.rs
@@ -0,0 +1,238 @@
+use crate::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 {
+ pub(super) 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
+ },
+ )
+ }
+ }
+ }
+
+ pub(super) 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/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs
new file mode 100644
index 00000000..4c43826e
--- /dev/null
+++ b/native/src/widget/pane_grid/configuration.rs
@@ -0,0 +1,26 @@
+use crate::pane_grid::Axis;
+
+/// The arrangement of a [`PaneGrid`].
+///
+/// [`PaneGrid`]: crate::pane_grid::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::pane_grid::Pane
+ Pane(T),
+}
diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs
new file mode 100644
index 00000000..c9981903
--- /dev/null
+++ b/native/src/widget/pane_grid/content.rs
@@ -0,0 +1,213 @@
+use crate::container;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
+use crate::pane_grid::{self, TitleBar};
+use crate::{Clipboard, Element, Hasher, Layout, Point, Size};
+
+/// The content of a [`Pane`].
+///
+/// [`Pane`]: crate::widget::pane_grid::Pane
+#[allow(missing_debug_implementations)]
+pub struct Content<'a, Message, Renderer: pane_grid::Renderer> {
+ title_bar: Option<TitleBar<'a, Message, Renderer>>,
+ body: Element<'a, Message, Renderer>,
+ style: Renderer::Style,
+}
+
+impl<'a, Message, Renderer> Content<'a, Message, Renderer>
+where
+ Renderer: pane_grid::Renderer,
+{
+ /// 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: Renderer::Style::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::Style>) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+impl<'a, Message, Renderer> Content<'a, Message, Renderer>
+where
+ Renderer: pane_grid::Renderer,
+{
+ /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
+ ///
+ /// [`Renderer`]: crate::widget::pane_grid::Renderer
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ 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();
+
+ renderer.draw_pane(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ Some((title_bar, title_bar_layout)),
+ (&self.body, body_layout),
+ cursor_position,
+ )
+ } else {
+ renderer.draw_pane(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ None,
+ (&self.body, layout),
+ cursor_position,
+ )
+ }
+ }
+
+ /// Returns whether the [`Content`] with the given [`Layout`] can be picked
+ /// at the provided cursor position.
+ pub fn can_be_picked_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
+ }
+ }
+
+ 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.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.layout(renderer, limits)
+ }
+ }
+
+ pub(crate) fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> 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(
+ event.clone(),
+ children.next().unwrap(),
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ );
+
+ children.next().unwrap()
+ } else {
+ layout
+ };
+
+ let body_status = self.body.on_event(
+ event,
+ body_layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ );
+
+ event_status.merge(body_status)
+ }
+
+ pub(crate) fn hash_layout(&self, state: &mut Hasher) {
+ if let Some(title_bar) = &self.title_bar {
+ title_bar.hash_layout(state);
+ }
+
+ self.body.hash_layout(state);
+ }
+
+ pub(crate) fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ let body_layout = if self.title_bar.is_some() {
+ let mut children = layout.children();
+
+ // Overlays only allowed in the pane body, for now at least.
+ let _title_bar_layout = children.next();
+
+ children.next()?
+ } else {
+ layout
+ };
+
+ self.body.overlay(body_layout)
+ }
+}
+
+impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>
+where
+ T: Into<Element<'a, Message, Renderer>>,
+ Renderer: pane_grid::Renderer + container::Renderer,
+{
+ fn from(element: T) -> Self {
+ Self::new(element)
+ }
+}
diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs
new file mode 100644
index 00000000..b31a8737
--- /dev/null
+++ b/native/src/widget/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/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs
new file mode 100644
index 00000000..319936fc
--- /dev/null
+++ b/native/src/widget/pane_grid/node.rs
@@ -0,0 +1,258 @@
+use crate::{
+ pane_grid::{Axis, Pane, Split},
+ Rectangle, Size,
+};
+
+use std::collections::HashMap;
+
+/// 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() {
+ match node {
+ Node::Split { id, a, b, .. } => {
+ 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,
+ ) -> HashMap<Pane, Rectangle> {
+ let mut regions = HashMap::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,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ let mut splits = HashMap::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)) {
+ match self {
+ Node::Split { a, b, .. } => {
+ 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 HashMap<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 HashMap<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/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs
new file mode 100644
index 00000000..39d9f3ef
--- /dev/null
+++ b/native/src/widget/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, Hash)]
+pub struct Pane(pub(super) usize);
diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs
new file mode 100644
index 00000000..16975abc
--- /dev/null
+++ b/native/src/widget/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, Hash)]
+pub struct Split(pub(super) usize);
diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs
new file mode 100644
index 00000000..666e1ca0
--- /dev/null
+++ b/native/src/widget/pane_grid/state.rs
@@ -0,0 +1,301 @@
+use crate::{
+ pane_grid::{Axis, Configuration, Direction, Node, Pane, Split},
+ Hasher, Point, Rectangle, Size,
+};
+
+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> {
+ pub(super) panes: HashMap<Pane, T>,
+ pub(super) internal: Internal,
+}
+
+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 (layout, last_id) =
+ Self::distribute_content(&mut panes, config.into(), 0);
+
+ State {
+ panes,
+ internal: Internal {
+ layout,
+ last_id,
+ action: Action::Idle,
+ },
+ }
+ }
+
+ /// Returns the total amount of panes in the [`State`].
+ pub fn len(&self) -> usize {
+ self.panes.len()
+ }
+
+ /// 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);
+
+ 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 let Some(sibling) = self.internal.layout.remove(pane) {
+ self.panes.remove(pane).map(|state| (state, sibling))
+ } else {
+ None
+ }
+ }
+
+ fn distribute_content(
+ panes: &mut HashMap<Pane, T>,
+ content: Configuration<T>,
+ next_id: usize,
+ ) -> (Node, usize) {
+ match content {
+ Configuration::Split { axis, ratio, a, b } => {
+ let (a, next_id) = Self::distribute_content(panes, *a, next_id);
+ let (b, next_id) = Self::distribute_content(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)
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Internal {
+ layout: Node,
+ last_id: usize,
+ action: Action,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Action {
+ Idle,
+ Dragging { pane: Pane, origin: Point },
+ Resizing { split: Split, axis: Axis },
+}
+
+impl Internal {
+ pub fn picked_pane(&self) -> Option<(Pane, Point)> {
+ match self.action {
+ Action::Dragging { pane, origin, .. } => Some((pane, origin)),
+ _ => None,
+ }
+ }
+
+ pub fn picked_split(&self) -> Option<(Split, Axis)> {
+ match self.action {
+ Action::Resizing { split, axis, .. } => Some((split, axis)),
+ _ => None,
+ }
+ }
+
+ pub fn pane_regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ self.layout.pane_regions(spacing, size)
+ }
+
+ pub fn split_regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ self.layout.split_regions(spacing, size)
+ }
+
+ pub fn pick_pane(&mut self, pane: &Pane, origin: Point) {
+ self.action = Action::Dragging {
+ pane: *pane,
+ origin,
+ };
+ }
+
+ pub fn pick_split(&mut self, split: &Split, axis: Axis) {
+ // TODO: Obtain `axis` from layout itself. Maybe we should implement
+ // `Node::find_split`
+ if self.picked_pane().is_some() {
+ return;
+ }
+
+ self.action = Action::Resizing {
+ split: *split,
+ axis,
+ };
+ }
+
+ pub fn idle(&mut self) {
+ self.action = Action::Idle;
+ }
+
+ pub fn hash_layout(&self, hasher: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.layout.hash(hasher);
+ }
+}
diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs
new file mode 100644
index 00000000..475cb9ae
--- /dev/null
+++ b/native/src/widget/pane_grid/title_bar.rs
@@ -0,0 +1,253 @@
+use crate::event::{self, Event};
+use crate::layout;
+use crate::pane_grid;
+use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size};
+
+/// The title bar of a [`Pane`].
+///
+/// [`Pane`]: crate::widget::pane_grid::Pane
+#[allow(missing_debug_implementations)]
+pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> {
+ title: String,
+ title_size: Option<u16>,
+ controls: Option<Element<'a, Message, Renderer>>,
+ padding: u16,
+ always_show_controls: bool,
+ style: Renderer::Style,
+}
+
+impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
+where
+ Renderer: pane_grid::Renderer,
+{
+ /// Creates a new [`TitleBar`] with the given title.
+ pub fn new(title: impl Into<String>) -> Self {
+ Self {
+ title: title.into(),
+ title_size: None,
+ controls: None,
+ padding: 0,
+ always_show_controls: false,
+ style: Renderer::Style::default(),
+ }
+ }
+
+ /// Sets the size of the title of the [`TitleBar`].
+ pub fn title_size(mut self, size: u16) -> Self {
+ self.title_size = Some(size);
+ self
+ }
+
+ /// 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(mut self, units: u16) -> Self {
+ self.padding = units;
+ self
+ }
+
+ /// Sets the style of the [`TitleBar`].
+ pub fn style(mut self, style: impl Into<Renderer::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: pane_grid::Renderer,
+{
+ /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
+ ///
+ /// [`Renderer`]: crate::widget::pane_grid::Renderer
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ show_controls: bool,
+ ) -> Renderer::Output {
+ let mut children = layout.children();
+ let padded = children.next().unwrap();
+
+ if let Some(controls) = &self.controls {
+ let mut children = padded.children();
+ let title_layout = children.next().unwrap();
+ let controls_layout = children.next().unwrap();
+
+ let (title_bounds, controls) =
+ if show_controls || self.always_show_controls {
+ (title_layout.bounds(), Some((controls, controls_layout)))
+ } else {
+ (
+ Rectangle {
+ width: padded.bounds().width,
+ ..title_layout.bounds()
+ },
+ None,
+ )
+ };
+
+ renderer.draw_title_bar(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ &self.title,
+ self.title_size.unwrap_or(renderer.default_size()),
+ Renderer::Font::default(),
+ title_bounds,
+ controls,
+ cursor_position,
+ )
+ } else {
+ renderer.draw_title_bar::<()>(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ &self.title,
+ self.title_size.unwrap_or(renderer.default_size()),
+ Renderer::Font::default(),
+ padded.bounds(),
+ None,
+ cursor_position,
+ )
+ }
+ }
+
+ /// 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();
+
+ if self.controls.is_some() {
+ let mut children = padded.children();
+ let _ = children.next().unwrap();
+ let controls_layout = children.next().unwrap();
+
+ !controls_layout.bounds().contains(cursor_position)
+ } else {
+ true
+ }
+ } else {
+ false
+ }
+ }
+
+ pub(crate) fn hash_layout(&self, hasher: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.title.hash(hasher);
+ self.title_size.hash(hasher);
+ self.padding.hash(hasher);
+ }
+
+ pub(crate) fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let padding = f32::from(self.padding);
+ let limits = limits.pad(padding);
+ let max_size = limits.max();
+
+ let title_size = self.title_size.unwrap_or(renderer.default_size());
+ let title_font = Renderer::Font::default();
+
+ let (title_width, title_height) = renderer.measure(
+ &self.title,
+ title_size,
+ title_font,
+ Size::new(f32::INFINITY, max_size.height),
+ );
+
+ let mut node = if let Some(controls) = &self.controls {
+ let mut controls_layout = controls
+ .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 mut title_layout = layout::Node::new(Size::new(
+ title_width.min(space_before_controls),
+ title_height,
+ ));
+
+ let title_size = title_layout.size();
+ let height = title_size.height.max(controls_size.height);
+
+ title_layout
+ .move_to(Point::new(0.0, (height - title_size.height) / 2.0));
+ 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::new(Size::new(max_size.width, title_height))
+ };
+
+ node.move_to(Point::new(padding, padding));
+
+ layout::Node::with_children(node.size().pad(padding), vec![node])
+ }
+
+ pub(crate) fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ if let Some(controls) = &mut self.controls {
+ let mut children = layout.children();
+ let padded = children.next().unwrap();
+
+ let mut children = padded.children();
+ let _ = children.next();
+ let controls_layout = children.next().unwrap();
+
+ controls.on_event(
+ event,
+ controls_layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ } else {
+ event::Status::Ignored
+ }
+ }
+}
diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs
new file mode 100644
index 00000000..58c0dfe1
--- /dev/null
+++ b/native/src/widget/pick_list.rs
@@ -0,0 +1,343 @@
+//! Display a dropdown list of selectable values.
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::overlay::menu::{self, Menu};
+use crate::scrollable;
+use crate::text;
+use crate::{
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
+};
+use std::borrow::Cow;
+
+/// A widget for selecting a single value from a list of options.
+#[allow(missing_debug_implementations)]
+pub struct PickList<'a, T, Message, Renderer: self::Renderer>
+where
+ [T]: ToOwned<Owned = Vec<T>>,
+{
+ menu: &'a mut menu::State,
+ is_open: &'a mut bool,
+ hovered_option: &'a mut Option<usize>,
+ last_selection: &'a mut Option<T>,
+ on_selected: Box<dyn Fn(T) -> Message>,
+ options: Cow<'a, [T]>,
+ selected: Option<T>,
+ width: Length,
+ padding: u16,
+ text_size: Option<u16>,
+ font: Renderer::Font,
+ style: <Renderer as self::Renderer>::Style,
+}
+
+/// The local state of a [`PickList`].
+#[derive(Debug, Clone)]
+pub struct State<T> {
+ menu: menu::State,
+ is_open: bool,
+ hovered_option: Option<usize>,
+ last_selection: Option<T>,
+}
+
+impl<T> Default for State<T> {
+ fn default() -> Self {
+ Self {
+ menu: menu::State::default(),
+ is_open: bool::default(),
+ hovered_option: Option::default(),
+ last_selection: Option::default(),
+ }
+ }
+}
+
+impl<'a, T: 'a, Message, Renderer: self::Renderer>
+ PickList<'a, T, Message, Renderer>
+where
+ T: ToString + Eq,
+ [T]: ToOwned<Owned = Vec<T>>,
+{
+ /// Creates a new [`PickList`] with the given [`State`], a list of options,
+ /// the current selected value, and the message to produce when an option is
+ /// selected.
+ pub fn new(
+ state: &'a mut State<T>,
+ options: impl Into<Cow<'a, [T]>>,
+ selected: Option<T>,
+ on_selected: impl Fn(T) -> Message + 'static,
+ ) -> Self {
+ let State {
+ menu,
+ is_open,
+ hovered_option,
+ last_selection,
+ } = state;
+
+ Self {
+ menu,
+ is_open,
+ hovered_option,
+ last_selection,
+ on_selected: Box::new(on_selected),
+ options: options.into(),
+ selected,
+ width: Length::Shrink,
+ text_size: None,
+ padding: Renderer::DEFAULT_PADDING,
+ font: Default::default(),
+ style: Default::default(),
+ }
+ }
+
+ /// Sets the width of the [`PickList`].
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the padding of the [`PickList`].
+ pub fn padding(mut self, padding: u16) -> Self {
+ self.padding = padding;
+ self
+ }
+
+ /// Sets the text size of the [`PickList`].
+ pub fn text_size(mut self, size: u16) -> Self {
+ self.text_size = Some(size);
+ self
+ }
+
+ /// Sets the font of the [`PickList`].
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
+ /// Sets the style of the [`PickList`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as self::Renderer>::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,
+ [T]: ToOwned<Owned = Vec<T>>,
+ Message: 'static,
+ Renderer: self::Renderer + scrollable::Renderer + 'a,
+{
+ fn width(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn height(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ use std::f32;
+
+ let limits = limits
+ .width(self.width)
+ .height(Length::Shrink)
+ .pad(f32::from(self.padding));
+
+ let text_size = self.text_size.unwrap_or(renderer.default_size());
+
+ let max_width = match self.width {
+ Length::Shrink => {
+ let labels = self.options.iter().map(ToString::to_string);
+
+ labels
+ .map(|label| {
+ let (width, _) = renderer.measure(
+ &label,
+ text_size,
+ Renderer::Font::default(),
+ Size::new(f32::INFINITY, f32::INFINITY),
+ );
+
+ width.round() as u32
+ })
+ .max()
+ .unwrap_or(100)
+ }
+ _ => 0,
+ };
+
+ let size = {
+ let intrinsic = Size::new(
+ max_width as f32
+ + f32::from(text_size)
+ + f32::from(self.padding),
+ f32::from(text_size),
+ );
+
+ limits.resolve(intrinsic).pad(f32::from(self.padding))
+ };
+
+ layout::Node::new(size)
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash as _;
+
+ match self.width {
+ Length::Shrink => {
+ self.options
+ .iter()
+ .map(ToString::to_string)
+ .for_each(|label| label.hash(state));
+ }
+ _ => {
+ self.width.hash(state);
+ }
+ }
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ _renderer: &Renderer,
+ _clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ let event_status = if *self.is_open {
+ // TODO: Encode cursor availability in the type system
+ *self.is_open =
+ cursor_position.x < 0.0 || cursor_position.y < 0.0;
+
+ event::Status::Captured
+ } else if layout.bounds().contains(cursor_position) {
+ let selected = self.selected.as_ref();
+
+ *self.is_open = true;
+ *self.hovered_option = self
+ .options
+ .iter()
+ .position(|option| Some(option) == selected);
+
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ };
+
+ if let Some(last_selection) = self.last_selection.take() {
+ messages.push((self.on_selected)(last_selection));
+
+ *self.is_open = false;
+
+ event::Status::Captured
+ } else {
+ event_status
+ }
+ }
+ _ => event::Status::Ignored,
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ _defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ self::Renderer::draw(
+ renderer,
+ layout.bounds(),
+ cursor_position,
+ self.selected.as_ref().map(ToString::to_string),
+ self.padding,
+ self.text_size.unwrap_or(renderer.default_size()),
+ self.font,
+ &self.style,
+ )
+ }
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ if *self.is_open {
+ let bounds = layout.bounds();
+
+ let mut menu = Menu::new(
+ &mut self.menu,
+ &self.options,
+ &mut self.hovered_option,
+ &mut self.last_selection,
+ )
+ .width(bounds.width.round() as u16)
+ .padding(self.padding)
+ .font(self.font)
+ .style(Renderer::menu_style(&self.style));
+
+ if let Some(text_size) = self.text_size {
+ menu = menu.text_size(text_size);
+ }
+
+ Some(menu.overlay(layout.position(), bounds.height))
+ } else {
+ None
+ }
+ }
+}
+
+/// The renderer of a [`PickList`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`PickList`] in your user interface.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer: text::Renderer + menu::Renderer {
+ /// The default padding of a [`PickList`].
+ const DEFAULT_PADDING: u16;
+
+ /// The [`PickList`] style supported by this renderer.
+ type Style: Default;
+
+ /// Returns the style of the [`Menu`] of the [`PickList`].
+ fn menu_style(
+ style: &<Self as Renderer>::Style,
+ ) -> <Self as menu::Renderer>::Style;
+
+ /// Draws a [`PickList`].
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ selected: Option<String>,
+ padding: u16,
+ text_size: u16,
+ font: Self::Font,
+ style: &<Self as Renderer>::Style,
+ ) -> Self::Output;
+}
+
+impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>>
+ for PickList<'a, T, Message, Renderer>
+where
+ T: Clone + ToString + Eq,
+ [T]: ToOwned<Owned = Vec<T>>,
+ Renderer: self::Renderer + 'a,
+ Message: 'static,
+{
+ fn into(self) -> Element<'a, Message, Renderer> {
+ Element::new(self)
+ }
+}
diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs
index 67d1ab83..d294f198 100644
--- a/native/src/widget/progress_bar.rs
+++ b/native/src/widget/progress_bar.rs
@@ -33,8 +33,6 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> {
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`ProgressBar`]
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
ProgressBar {
value: value.max(*range.start()).min(*range.end()),
@@ -46,24 +44,18 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> {
}
/// Sets the width of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn height(mut self, height: Length) -> Self {
self.height = Some(height);
self
}
/// Sets the style of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -104,6 +96,7 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
layout.bounds(),
@@ -114,6 +107,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
}
@@ -124,15 +120,12 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`ProgressBar`] in your user interface.
///
-/// [`ProgressBar`]: struct.ProgressBar.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
/// The default height of a [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
const DEFAULT_HEIGHT: u16;
/// Draws a [`ProgressBar`].
@@ -143,8 +136,6 @@ pub trait Renderer: crate::Renderer {
/// * the current value of the [`ProgressBar`]
/// * maybe a specific background of the [`ProgressBar`]
/// * maybe a specific active color of the [`ProgressBar`]
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
fn draw(
&self,
bounds: Rectangle,
@@ -157,8 +148,8 @@ pub trait Renderer: crate::Renderer {
impl<'a, Message, Renderer> From<ProgressBar<Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static,
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
{
fn from(
progress_bar: ProgressBar<Renderer>,
diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs
index 46983db3..3a1dd386 100644
--- a/native/src/widget/radio.rs
+++ b/native/src/widget/radio.rs
@@ -1,9 +1,13 @@
//! Create choices using radio buttons.
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::row;
+use crate::text;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, row, text, Align, Clipboard, Element, Event, Font, Hasher,
- HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text,
- VerticalAlignment, Widget,
+ Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,
+ Point, Rectangle, Row, Text, VerticalAlignment, Widget,
};
use std::hash::Hash;
@@ -35,14 +39,22 @@ use std::hash::Hash;
///
/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/hecrj/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Radio<Message, Renderer: self::Renderer> {
+pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> {
is_selected: bool,
on_click: Message,
label: String,
+ width: Length,
+ size: u16,
+ spacing: u16,
+ text_size: Option<u16>,
style: Renderer::Style,
}
-impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
+impl<Message, Renderer: self::Renderer + text::Renderer>
+ Radio<Message, Renderer>
+where
+ Message: Clone,
+{
/// Creates a new [`Radio`] button.
///
/// It expects:
@@ -51,9 +63,12 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
/// * 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`.
- ///
- /// [`Radio`]: struct.Radio.html
- pub fn new<F, V>(value: V, label: &str, selected: Option<V>, f: F) -> Self
+ pub fn new<F, V>(
+ value: V,
+ label: impl Into<String>,
+ selected: Option<V>,
+ f: F,
+ ) -> Self
where
V: Eq + Copy,
F: 'static + Fn(V) -> Message,
@@ -61,14 +76,40 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
- label: String::from(label),
+ label: label.into(),
+ width: Length::Shrink,
+ size: <Renderer as self::Renderer>::DEFAULT_SIZE,
+ spacing: Renderer::DEFAULT_SPACING, //15
+ text_size: None,
style: Renderer::Style::default(),
}
}
+ /// Sets the size of the [`Radio`] button.
+ pub fn size(mut self, size: u16) -> Self {
+ self.size = size;
+ self
+ }
+
+ /// Sets the width of the [`Radio`] button.
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the spacing between the [`Radio`] button and the text.
+ pub fn spacing(mut self, spacing: u16) -> Self {
+ self.spacing = spacing;
+ self
+ }
+
+ /// Sets the text size of the [`Radio`] button.
+ pub fn text_size(mut self, text_size: u16) -> Self {
+ self.text_size = Some(text_size);
+ self
+ }
+
/// Sets the style of the [`Radio`] button.
- ///
- /// [`Radio`]: struct.Radio.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -77,11 +118,11 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
where
- Renderer: self::Renderer + text::Renderer + row::Renderer,
Message: Clone,
+ Renderer: self::Renderer + text::Renderer + row::Renderer,
{
fn width(&self) -> Length {
- Length::Fill
+ self.width
}
fn height(&self) -> Length {
@@ -93,18 +134,20 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let size = self::Renderer::default_size(renderer);
-
Row::<(), Renderer>::new()
- .width(Length::Fill)
- .spacing(15)
+ .width(self.width)
+ .spacing(self.spacing)
.align_items(Align::Center)
.push(
Row::new()
- .width(Length::Units(size as u16))
- .height(Length::Units(size as u16)),
+ .width(Length::Units(self.size))
+ .height(Length::Units(self.size)),
+ )
+ .push(
+ Text::new(&self.label)
+ .width(self.width)
+ .size(self.text_size.unwrap_or(renderer.default_size())),
)
- .push(Text::new(&self.label))
.layout(renderer, limits)
}
@@ -116,22 +159,23 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
}) => {
if layout.bounds().contains(cursor_position) {
messages.push(self.on_click.clone());
+
+ return event::Status::Captured;
}
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -140,6 +184,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
let bounds = layout.bounds();
let mut children = layout.children();
@@ -153,8 +198,8 @@ where
defaults,
label_layout.bounds(),
&self.label,
- <Renderer as text::Renderer>::DEFAULT_SIZE,
- Font::Default,
+ self.text_size.unwrap_or(renderer.default_size()),
+ Default::default(),
None,
HorizontalAlignment::Left,
VerticalAlignment::Center,
@@ -173,6 +218,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.label.hash(state);
}
}
@@ -182,16 +230,16 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Radio`] button in your user interface.
///
-/// [`Radio`]: struct.Radio.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
- /// Returns the default size of a [`Radio`] button.
- ///
- /// [`Radio`]: struct.Radio.html
- fn default_size(&self) -> u32;
+ /// The default size of a [`Radio`] button.
+ const DEFAULT_SIZE: u16;
+
+ /// The default spacing of a [`Radio`] button.
+ const DEFAULT_SPACING: u16;
/// Draws a [`Radio`] button.
///
@@ -200,8 +248,6 @@ pub trait Renderer: crate::Renderer {
/// * whether the [`Radio`] is selected or not
/// * whether the mouse is over the [`Radio`] or not
/// * the drawn label of the [`Radio`]
- ///
- /// [`Radio`]: struct.Radio.html
fn draw(
&mut self,
bounds: Rectangle,
@@ -215,8 +261,8 @@ pub trait Renderer: crate::Renderer {
impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + row::Renderer + text::Renderer,
- Message: 'static + Clone,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer + row::Renderer + text::Renderer,
{
fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(radio)
diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs
index c8812ea2..b71663bd 100644
--- a/native/src/widget/row.rs
+++ b/native/src/widget/row.rs
@@ -1,18 +1,15 @@
//! Distribute content horizontally.
-use std::hash::Hash;
-
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
+use std::hash::Hash;
use std::u32;
/// A container that distributes its contents horizontally.
-///
-/// A [`Row`] will try to fill the horizontal space of its container.
-///
-/// [`Row`]: struct.Row.html
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message, Renderer> {
spacing: u16,
@@ -27,15 +24,11 @@ pub struct Row<'a, Message, Renderer> {
impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
/// Creates an empty [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Row`] with the given elements.
- ///
- /// [`Row`]: struct.Row.html
pub fn with_children(
children: Vec<Element<'a, Message, Renderer>>,
) -> Self {
@@ -62,57 +55,42 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
}
/// Sets the padding of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the vertical alignment of the contents of the [`Row`] .
- ///
- /// [`Row`]: struct.Row.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Adds an [`Element`] to the [`Row`].
- ///
- /// [`Element`]: ../struct.Element.html
- /// [`Row`]: struct.Row.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -165,9 +143,11 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
- self.children.iter_mut().zip(layout.children()).for_each(
- |(child, layout)| {
+ ) -> event::Status {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .map(|(child, layout)| {
child.widget.on_event(
event.clone(),
layout,
@@ -176,8 +156,8 @@ where
renderer,
clipboard,
)
- },
- );
+ })
+ .fold(event::Status::Ignored, event::Status::merge)
}
fn draw(
@@ -186,24 +166,44 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
- renderer.draw(defaults, &self.children, layout, cursor_position)
+ renderer.draw(
+ defaults,
+ &self.children,
+ layout,
+ cursor_position,
+ viewport,
+ )
}
fn hash_layout(&self, state: &mut Hasher) {
- 1.hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.align_items.hash(state);
self.spacing.hash(state);
- self.spacing.hash(state);
+ self.padding.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|(child, layout)| child.widget.overlay(layout))
+ .next()
+ }
}
/// The renderer of a [`Row`].
@@ -211,8 +211,7 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Row`] in your user interface.
///
-/// [`Row`]: struct.Row.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// Draws a [`Row`].
///
@@ -220,15 +219,13 @@ pub trait Renderer: crate::Renderer + Sized {
/// - the children of the [`Row`]
/// - the [`Layout`] of the [`Row`] and its children
/// - the cursor position
- ///
- /// [`Row`]: struct.Row.html
- /// [`Layout`]: ../layout/struct.Layout.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
children: &[Element<'_, Message, Self>],
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
}
@@ -236,7 +233,7 @@ impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(row)
diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs
new file mode 100644
index 00000000..18c88658
--- /dev/null
+++ b/native/src/widget/rule.rs
@@ -0,0 +1,116 @@
+//! Display a horizontal or vertical rule for dividing content.
+
+use std::hash::Hash;
+
+use crate::{
+ layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
+};
+
+/// Display a horizontal or vertical rule for dividing content.
+#[derive(Debug, Copy, Clone)]
+pub struct Rule<Renderer: self::Renderer> {
+ width: Length,
+ height: Length,
+ style: Renderer::Style,
+ is_horizontal: bool,
+}
+
+impl<Renderer: self::Renderer> Rule<Renderer> {
+ /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing.
+ pub fn horizontal(spacing: u16) -> Self {
+ Rule {
+ width: Length::Fill,
+ height: Length::from(Length::Units(spacing)),
+ style: Renderer::Style::default(),
+ is_horizontal: true,
+ }
+ }
+
+ /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing.
+ pub fn vertical(spacing: u16) -> Self {
+ Rule {
+ width: Length::from(Length::Units(spacing)),
+ height: Length::Fill,
+ style: Renderer::Style::default(),
+ is_horizontal: false,
+ }
+ }
+
+ /// Sets the style of the [`Rule`].
+ pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer>
+where
+ Renderer: self::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,
+ renderer: &mut Renderer,
+ _defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ renderer.draw(layout.bounds(), &self.style, self.is_horizontal)
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.width.hash(state);
+ self.height.hash(state);
+ }
+}
+
+/// The renderer of a [`Rule`].
+pub trait Renderer: crate::Renderer {
+ /// The style supported by this renderer.
+ type Style: Default;
+
+ /// Draws a [`Rule`].
+ ///
+ /// It receives:
+ /// * the bounds of the [`Rule`]
+ /// * the style of the [`Rule`]
+ /// * whether the [`Rule`] is horizontal (true) or vertical (false)
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ style: &Self::Style,
+ is_horizontal: bool,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<Rule<Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
+{
+ fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> {
+ Element::new(rule)
+ }
+}
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index eb1722ed..8c321ee5 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -1,9 +1,13 @@
//! Navigate an endless amount of content with a scrollbar.
+use crate::column;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::touch::{self, Touch};
use crate::{
- column,
- input::{mouse, touch, ButtonState, Touch},
- layout, Align, Clipboard, Column, Element, Event, Hasher, Layout, Length,
- Point, Rectangle, Size, Widget,
+ Align, Clipboard, Column, Element, Hasher, Layout, Length, Point,
+ Rectangle, Size, Vector, Widget,
};
use std::{f32, hash::Hash, u32};
@@ -15,20 +19,23 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
state: &'a mut State,
height: Length,
max_height: u32,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
content: Column<'a, Message, Renderer>,
style: Renderer::Style,
}
impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
/// Creates a new [`Scrollable`] with the given [`State`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn new(state: &'a mut State) -> Self {
Scrollable {
state,
height: Length::Shrink,
max_height: u32::MAX,
+ scrollbar_width: 10,
+ scrollbar_margin: 0,
+ scroller_width: 10,
content: Column::new(),
style: Renderer::Style::default(),
}
@@ -45,64 +52,68 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
}
/// Sets the padding of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn padding(mut self, units: u16) -> Self {
self.content = self.content.padding(units);
self
}
/// Sets the width of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn width(mut self, width: Length) -> Self {
self.content = self.content.width(width);
self
}
/// Sets the height of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.content = self.content.max_width(max_width);
self
}
/// Sets the maximum height of the [`Scrollable`] in pixels.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the horizontal alignment of the contents of the [`Scrollable`] .
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn align_items(mut self, align_items: Align) -> Self {
self.content = self.content.align_items(align_items);
self
}
+ /// Sets the scrollbar width of the [`Scrollable`] .
+ /// Silently enforces a minimum value of 1.
+ pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self {
+ self.scrollbar_width = scrollbar_width.max(1);
+ self
+ }
+
+ /// Sets the scrollbar margin of the [`Scrollable`] .
+ pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self {
+ self.scrollbar_margin = scrollbar_margin;
+ self
+ }
+
+ /// Sets the scroller width of the [`Scrollable`] .
+ /// Silently enforces a minimum value of 1.
+ pub fn scroller_width(mut self, scroller_width: u16) -> Self {
+ self.scroller_width = scroller_width.max(1);
+ self
+ }
+
/// Sets the style of the [`Scrollable`] .
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
}
/// Adds an element to the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -115,7 +126,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + column::Renderer,
+ Renderer: self::Renderer,
{
fn width(&self) -> Length {
Widget::<Message, Renderer>::width(&self.content)
@@ -154,14 +165,56 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
- // TODO: Event capture. Nested scrollables should capture scroll events.
+ let offset = self.state.offset(bounds, content_bounds);
+ let scrollbar = renderer.scrollbar(
+ bounds,
+ content_bounds,
+ offset,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ );
+ let is_mouse_over_scrollbar = scrollbar
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false);
+
+ let event_status = {
+ let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
+ Point::new(
+ cursor_position.x,
+ cursor_position.y
+ + self.state.offset(bounds, content_bounds) as f32,
+ )
+ } 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(cursor_position.x, -1.0)
+ };
+
+ self.content.on_event(
+ event.clone(),
+ content,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ };
+
+ if let event::Status::Captured = event_status {
+ return event::Status::Captured;
+ }
+
if is_mouse_over {
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
@@ -174,6 +227,8 @@ where
self.state.scroll(y, bounds, content_bounds);
}
}
+
+ return event::Status::Captured;
}
Event::Touch(Touch { phase, .. }) => match phase {
touch::Phase::Started => {
@@ -199,19 +254,11 @@ where
}
}
- let offset = self.state.offset(bounds, content_bounds);
- let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
-
if self.state.is_scroller_grabbed() {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Released,
- })
+ Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Left,
+ ))
| Event::Touch(Touch {
phase: touch::Phase::Ended,
..
@@ -221,6 +268,8 @@ where
..
}) => {
self.state.scroller_grabbed_at = None;
+
+ return event::Status::Captured;
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(Touch {
@@ -238,16 +287,17 @@ where
bounds,
content_bounds,
);
+
+ return event::Status::Captured;
}
}
_ => {}
}
} else if is_mouse_over_scrollbar {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -267,6 +317,8 @@ where
self.state.scroller_grabbed_at =
Some(scroller_grabbed_at);
+
+ return event::Status::Captured;
}
}
}
@@ -274,28 +326,7 @@ where
}
}
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(
- cursor_position.x,
- cursor_position.y
- + self.state.offset(bounds, content_bounds) as f32,
- )
- } 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(cursor_position.x, -1.0)
- };
-
- self.content.on_event(
- event,
- content,
- cursor_position,
- messages,
- renderer,
- clipboard,
- )
+ event::Status::Ignored
}
fn draw(
@@ -304,12 +335,20 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
let offset = self.state.offset(bounds, content_bounds);
- let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
+ let scrollbar = renderer.scrollbar(
+ bounds,
+ content_bounds,
+ offset,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ );
let is_mouse_over = bounds.contains(cursor_position);
let is_mouse_over_scrollbar = scrollbar
@@ -329,6 +368,10 @@ where
defaults,
content_layout,
cursor_position,
+ &Rectangle {
+ y: bounds.y + offset as f32,
+ ..bounds
+ },
)
};
@@ -347,18 +390,35 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
- std::any::TypeId::of::<Scrollable<'static, (), Renderer>>().hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
self.height.hash(state);
self.max_height.hash(state);
self.content.hash_layout(state)
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ let Self { content, state, .. } = self;
+
+ content
+ .overlay(layout.children().next().unwrap())
+ .map(|overlay| {
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+ let offset = state.offset(bounds, content_bounds);
+
+ overlay.translate(Vector::new(0.0, -(offset as f32)))
+ })
+ }
}
/// The local state of a [`Scrollable`].
-///
-/// [`Scrollable`]: struct.Scrollable.html
#[derive(Debug, Clone, Copy, Default)]
pub struct State {
scroller_grabbed_at: Option<f32>,
@@ -368,17 +428,12 @@ pub struct State {
impl State {
/// Creates a new [`State`] with the scrollbar located at the top.
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
State::default()
}
/// Apply a scrolling offset to the current [`State`], given the bounds of
/// the [`Scrollable`] and its contents.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn scroll(
&mut self,
delta_y: f32,
@@ -399,9 +454,6 @@ impl State {
///
/// `0` represents scrollbar at the top, while `1` represents scrollbar at
/// the bottom.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn scroll_to(
&mut self,
percentage: f32,
@@ -414,9 +466,6 @@ impl State {
/// Returns the current scrolling offset of the [`State`], given the bounds
/// of the [`Scrollable`] and its contents.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
let hidden_content =
(content_bounds.height - bounds.height).max(0.0).round() as u32;
@@ -436,28 +485,29 @@ impl State {
}
/// The scrollbar of a [`Scrollable`].
-///
-/// [`Scrollable`]: struct.Scrollable.html
#[derive(Debug)]
pub struct Scrollbar {
+ /// The outer bounds of the scrollable, including the [`Scrollbar`] and
+ /// [`Scroller`].
+ pub outer_bounds: Rectangle,
+
/// The bounds of the [`Scrollbar`].
- ///
- /// [`Scrollbar`]: struct.Scrollbar.html
pub bounds: Rectangle,
+ /// The margin within the [`Scrollbar`].
+ pub margin: u16,
+
/// The bounds of the [`Scroller`].
- ///
- /// [`Scroller`]: struct.Scroller.html
pub scroller: Scroller,
}
impl Scrollbar {
fn is_mouse_over(&self, cursor_position: Point) -> bool {
- self.bounds.contains(cursor_position)
+ self.outer_bounds.contains(cursor_position)
}
fn grab_scroller(&self, cursor_position: Point) -> Option<f32> {
- if self.bounds.contains(cursor_position) {
+ if self.outer_bounds.contains(cursor_position) {
Some(if self.scroller.bounds.contains(cursor_position) {
(cursor_position.y - self.scroller.bounds.y)
/ self.scroller.bounds.height
@@ -482,13 +532,9 @@ impl Scrollbar {
}
/// The handle of a [`Scrollbar`].
-///
-/// [`Scrollbar`]: struct.Scrollbar.html
#[derive(Debug, Clone, Copy)]
pub struct Scroller {
/// The bounds of the [`Scroller`].
- ///
- /// [`Scroller`]: struct.Scrollbar.html
pub bounds: Rectangle,
}
@@ -497,22 +543,21 @@ pub struct Scroller {
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Scrollable`] in your user interface.
///
-/// [`Scrollable`]: struct.Scrollable.html
-/// [renderer]: ../../renderer/index.html
-pub trait Renderer: crate::Renderer + Sized {
+/// [renderer]: crate::renderer
+pub trait Renderer: column::Renderer + Sized {
/// The style supported by this renderer.
type Style: Default;
/// Returns the [`Scrollbar`] given the bounds and content bounds of a
/// [`Scrollable`].
- ///
- /// [`Scrollbar`]: struct.Scrollbar.html
- /// [`Scrollable`]: struct.Scrollable.html
fn scrollbar(
&self,
bounds: Rectangle,
content_bounds: Rectangle,
offset: u32,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
) -> Option<Scrollbar>;
/// Draws the [`Scrollable`].
@@ -526,10 +571,6 @@ pub trait Renderer: crate::Renderer + Sized {
/// - a optional [`Scrollbar`] to be rendered
/// - the scrolling offset
/// - the drawn content
- ///
- /// [`Scrollbar`]: struct.Scrollbar.html
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
fn draw(
&mut self,
scrollable: &State,
@@ -547,8 +588,8 @@ pub trait Renderer: crate::Renderer + Sized {
impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + column::Renderer,
- Message: 'static,
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
{
fn from(
scrollable: Scrollable<'a, Message, Renderer>,
diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs
index c98cebb6..755e6b2b 100644
--- a/native/src/widget/slider.rs
+++ b/native/src/widget/slider.rs
@@ -1,13 +1,12 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
-//!
-//! [`Slider`]: struct.Slider.html
-//! [`State`]: struct.State.html
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Rectangle, Size, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
};
use std::{hash::Hash, ops::RangeInclusive};
@@ -17,13 +16,15 @@ use std::{hash::Hash, ops::RangeInclusive};
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
-/// [`Slider`]: struct.Slider.html
+/// The [`Slider`] range of numeric values is generic and its step size defaults
+/// to 1 unit.
///
/// # Example
/// ```
/// # use iced_native::{slider, renderer::Null};
/// #
-/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>;
+/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>;
+/// #[derive(Clone)]
/// pub enum Message {
/// SliderChanged(f32),
/// }
@@ -36,16 +37,24 @@ use std::{hash::Hash, ops::RangeInclusive};
///
/// ![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, Message, Renderer: self::Renderer> {
+pub struct Slider<'a, T, Message, Renderer: self::Renderer> {
state: &'a mut State,
- range: RangeInclusive<f32>,
- value: f32,
- on_change: Box<dyn Fn(f32) -> Message>,
+ range: RangeInclusive<T>,
+ step: T,
+ value: T,
+ on_change: Box<dyn Fn(T) -> Message>,
+ on_release: Option<Message>,
width: Length,
+ height: u16,
style: Renderer::Style,
}
-impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> {
+impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer>
+where
+ T: Copy + From<u8> + std::cmp::PartialOrd,
+ Message: Clone,
+ Renderer: self::Renderer,
+{
/// Creates a new [`Slider`].
///
/// It expects:
@@ -55,48 +64,77 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> {
/// * a function that will be called when the [`Slider`] is dragged.
/// It receives the new value of the [`Slider`] and must produce a
/// `Message`.
- ///
- /// [`Slider`]: struct.Slider.html
- /// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
- range: RangeInclusive<f32>,
- value: f32,
+ range: RangeInclusive<T>,
+ value: T,
on_change: F,
) -> Self
where
- F: 'static + Fn(f32) -> Message,
+ F: 'static + Fn(T) -> Message,
{
+ let value = if value >= *range.start() {
+ value
+ } else {
+ *range.start()
+ };
+
+ let value = if value <= *range.end() {
+ value
+ } else {
+ *range.end()
+ };
+
Slider {
state,
- value: value.max(*range.start()).min(*range.end()),
+ value,
range,
+ step: T::from(1),
on_change: Box::new(on_change),
+ on_release: None,
width: Length::Fill,
+ height: Renderer::DEFAULT_HEIGHT,
style: Renderer::Style::default(),
}
}
- /// Sets the width of the [`Slider`].
+ /// Sets the release message of the [`Slider`].
+ /// This is called when the mouse is released from the slider.
///
- /// [`Slider`]: struct.Slider.html
+ /// 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: Length) -> Self {
self.width = width;
self
}
+ /// Sets the height of the [`Slider`].
+ pub fn height(mut self, height: u16) -> Self {
+ self.height = height;
+ self
+ }
+
/// Sets the style of the [`Slider`].
- ///
- /// [`Slider`]: struct.Slider.html
pub fn style(mut self, style: impl Into<Renderer::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
+ }
}
/// The local state of a [`Slider`].
-///
-/// [`Slider`]: struct.Slider.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_dragging: bool,
@@ -104,16 +142,16 @@ pub struct State {
impl State {
/// Creates a new [`State`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
}
-impl<'a, Message, Renderer> Widget<Message, Renderer>
- for Slider<'a, Message, Renderer>
+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: self::Renderer,
{
fn width(&self) -> Length {
@@ -126,12 +164,11 @@ where
fn layout(
&self,
- renderer: &Renderer,
+ _renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .width(self.width)
- .height(Length::Units(renderer.height() as u16));
+ let limits =
+ limits.width(self.width).height(Length::Units(self.height));
let size = limits.resolve(Size::ZERO);
@@ -146,28 +183,32 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
let mut change = || {
let bounds = layout.bounds();
-
if cursor_position.x <= bounds.x {
messages.push((self.on_change)(*self.range.start()));
} else if cursor_position.x >= bounds.x + bounds.width {
messages.push((self.on_change)(*self.range.end()));
} else {
- let percent = (cursor_position.x - bounds.x) / bounds.width;
- let value = (self.range.end() - self.range.start()) * percent
- + self.range.start();
+ let step = self.step.into();
+ let start = (*self.range.start()).into();
+ let end = (*self.range.end()).into();
+
+ let percent = f64::from(cursor_position.x - bounds.x)
+ / f64::from(bounds.width);
- messages.push((self.on_change)(value));
+ let steps = (percent * (end - start) / step).round();
+ let value = steps * step + start;
+
+ if let Some(value) = T::from_f64(value) {
+ messages.push((self.on_change)(value));
+ }
}
};
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -175,17 +216,23 @@ where
if layout.bounds().contains(cursor_position) {
change();
self.state.is_dragging = true;
+
+ return event::Status::Captured;
}
}
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Released,
- })
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Ended,
..
}) => {
- self.state.is_dragging = false;
+ if self.state.is_dragging {
+ if let Some(on_release) = self.on_release.clone() {
+ messages.push(on_release);
+ }
+ self.state.is_dragging = false;
+
+ return event::Status::Captured;
+ }
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(Touch {
@@ -194,10 +241,14 @@ where
}) => {
if self.state.is_dragging {
change();
+
+ return event::Status::Captured;
}
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -206,18 +257,25 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
+ let start = *self.range.start();
+ let end = *self.range.end();
+
renderer.draw(
layout.bounds(),
cursor_position,
- self.range.clone(),
- self.value,
+ start.into() as f32..=end.into() as f32,
+ self.value.into() as f32,
self.state.is_dragging,
&self.style,
)
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
}
}
@@ -227,16 +285,13 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Slider`] in your user interface.
///
-/// [`Slider`]: struct.Slider.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
- /// Returns the height of the [`Slider`].
- ///
- /// [`Slider`]: struct.Slider.html
- fn height(&self) -> u32;
+ /// The default height of a [`Slider`].
+ const DEFAULT_HEIGHT: u16;
/// Draws a [`Slider`].
///
@@ -246,10 +301,6 @@ pub trait Renderer: crate::Renderer {
/// * the local state of the [`Slider`]
/// * the range of values of the [`Slider`]
/// * the current value of the [`Slider`]
- ///
- /// [`Slider`]: struct.Slider.html
- /// [`State`]: struct.State.html
- /// [`Class`]: enum.Class.html
fn draw(
&mut self,
bounds: Rectangle,
@@ -261,14 +312,15 @@ pub trait Renderer: crate::Renderer {
) -> Self::Output;
}
-impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>>
+impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static,
+ T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer,
{
fn from(
- slider: Slider<'a, Message, Renderer>,
+ slider: Slider<'a, T, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(slider)
}
diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs
index 24c94bf6..6b34ece8 100644
--- a/native/src/widget/space.rs
+++ b/native/src/widget/space.rs
@@ -16,15 +16,11 @@ pub struct Space {
impl Space {
/// Creates an amount of empty [`Space`] with the given width and height.
- ///
- /// [`Space`]: struct.Space.html
pub fn new(width: Length, height: Length) -> Self {
Space { width, height }
}
/// Creates an amount of horizontal [`Space`].
- ///
- /// [`Space`]: struct.Space.html
pub fn with_width(width: Length) -> Self {
Space {
width,
@@ -33,8 +29,6 @@ impl Space {
}
/// Creates an amount of vertical [`Space`].
- ///
- /// [`Space`]: struct.Space.html
pub fn with_height(height: Length) -> Self {
Space {
width: Length::Shrink,
@@ -43,7 +37,7 @@ impl Space {
}
}
-impl<'a, Message, Renderer> Widget<Message, Renderer> for Space
+impl<Message, Renderer> Widget<Message, Renderer> for Space
where
Renderer: self::Renderer,
{
@@ -71,33 +65,31 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(layout.bounds())
}
fn hash_layout(&self, state: &mut Hasher) {
std::any::TypeId::of::<Space>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
}
}
/// The renderer of an amount of [`Space`].
-///
-/// [`Space`]: struct.Space.html
pub trait Renderer: crate::Renderer {
/// Draws an amount of empty [`Space`].
///
/// You should most likely return an empty primitive here.
- ///
- /// [`Space`]: struct.Space.html
fn draw(&mut self, bounds: Rectangle) -> Self::Output;
}
impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(space: Space) -> Element<'a, Message, Renderer> {
Element::new(space)
diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs
index 063730bb..9cd61918 100644
--- a/native/src/widget/svg.rs
+++ b/native/src/widget/svg.rs
@@ -1,9 +1,11 @@
//! Display vector graphics in your application.
-use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
+use crate::layout;
+use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};
use std::{
- hash::Hash,
- path::{Path, PathBuf},
+ hash::{Hash, Hasher as _},
+ path::PathBuf,
+ sync::Arc,
};
/// A vector graphics image.
@@ -12,8 +14,6 @@ use std::{
///
/// [`Svg`] images can have a considerable rendering cost when resized,
/// specially when they are complex.
-///
-/// [`Svg`]: struct.Svg.html
#[derive(Debug, Clone)]
pub struct Svg {
handle: Handle,
@@ -23,9 +23,6 @@ pub struct Svg {
impl Svg {
/// Creates a new [`Svg`] from the given [`Handle`].
- ///
- /// [`Svg`]: struct.Svg.html
- /// [`Handle`]: struct.Handle.html
pub fn new(handle: impl Into<Handle>) -> Self {
Svg {
handle: handle.into(),
@@ -34,17 +31,19 @@ impl Svg {
}
}
+ /// Creates a new [`Svg`] that will display the contents of the file at the
+ /// provided path.
+ pub fn from_path(path: impl Into<PathBuf>) -> Self {
+ Self::new(Handle::from_path(path))
+ }
+
/// Sets the width of the [`Svg`].
- ///
- /// [`Svg`]: struct.Svg.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Svg`].
- ///
- /// [`Svg`]: struct.Svg.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
@@ -94,68 +93,88 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(self.handle.clone(), layout)
}
fn hash_layout(&self, state: &mut Hasher) {
+ std::any::TypeId::of::<Svg>().hash(state);
+
+ self.handle.hash(state);
self.width.hash(state);
self.height.hash(state);
}
}
/// An [`Svg`] handle.
-///
-/// [`Svg`]: struct.Svg.html
#[derive(Debug, Clone)]
pub struct Handle {
id: u64,
- path: PathBuf,
+ data: Arc<Data>,
}
impl Handle {
/// Creates an SVG [`Handle`] pointing to the vector image of the given
/// path.
- ///
- /// [`Handle`]: struct.Handle.html
- pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
- use std::hash::Hasher as _;
+ pub fn from_path(path: impl Into<PathBuf>) -> Handle {
+ Self::from_data(Data::Path(path.into()))
+ }
- let path = path.into();
+ /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string
+ /// or gzip compressed data.
+ ///
+ /// This is useful if you already have your SVG data in-memory, maybe
+ /// because you downloaded or generated it procedurally.
+ pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle {
+ Self::from_data(Data::Bytes(bytes.into()))
+ }
+ fn from_data(data: Data) -> Handle {
let mut hasher = Hasher::default();
- path.hash(&mut hasher);
+ data.hash(&mut hasher);
Handle {
id: hasher.finish(),
- path,
+ data: Arc::new(data),
}
}
/// Returns the unique identifier of the [`Handle`].
- ///
- /// [`Handle`]: struct.Handle.html
pub fn id(&self) -> u64 {
self.id
}
- /// Returns a reference to the path of the [`Handle`].
- ///
- /// [`Handle`]: enum.Handle.html
- pub fn path(&self) -> &Path {
- &self.path
+ /// Returns a reference to the SVG [`Data`].
+ pub fn data(&self) -> &Data {
+ &self.data
}
}
-impl From<String> for Handle {
- fn from(path: String) -> Handle {
- Handle::from_path(path)
+impl Hash for Handle {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
}
}
-impl From<&str> for Handle {
- fn from(path: &str) -> Handle {
- Handle::from_path(path)
+/// The data of an [`Svg`].
+#[derive(Clone, Hash)]
+pub enum Data {
+ /// File data
+ Path(PathBuf),
+
+ /// In-memory data
+ ///
+ /// Can contain an SVG string or a gzip compressed data.
+ Bytes(Vec<u8>),
+}
+
+impl std::fmt::Debug for Data {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Data::Path(path) => write!(f, "Path({:?})", path),
+ Data::Bytes(_) => write!(f, "Bytes(...)"),
+ }
}
}
@@ -164,17 +183,12 @@ impl From<&str> for Handle {
/// Your [renderer] will need to implement this trait before being able to use
/// an [`Svg`] in your user interface.
///
-/// [`Svg`]: struct.Svg.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
- /// Returns the default dimensions of an [`Svg`] located on the given path.
- ///
- /// [`Svg`]: struct.Svg.html
+ /// Returns the default dimensions of an [`Svg`] for the given [`Handle`].
fn dimensions(&self, handle: &Handle) -> (u32, u32);
/// Draws an [`Svg`].
- ///
- /// [`Svg`]: struct.Svg.html
fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output;
}
diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs
index 7d8cad6e..6cc18e6c 100644
--- a/native/src/widget/text.rs
+++ b/native/src/widget/text.rs
@@ -1,7 +1,7 @@
//! Write some text for your users to read.
use crate::{
- layout, Color, Element, Font, Hasher, HorizontalAlignment, Layout, Length,
- Point, Rectangle, Size, VerticalAlignment, Widget,
+ layout, Color, Element, Hasher, HorizontalAlignment, Layout, Length, Point,
+ Rectangle, Size, VerticalAlignment, Widget,
};
use std::hash::Hash;
@@ -11,7 +11,7 @@ use std::hash::Hash;
/// # Example
///
/// ```
-/// # use iced_native::Text;
+/// # type Text = iced_native::Text<iced_native::renderer::Null>;
/// #
/// Text::new("I <3 iced!")
/// .color([0.0, 0.0, 1.0])
@@ -19,28 +19,26 @@ use std::hash::Hash;
/// ```
///
/// ![Text drawn by `iced_wgpu`](https://github.com/hecrj/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text.png?raw=true)
-#[derive(Debug, Clone)]
-pub struct Text {
+#[derive(Debug)]
+pub struct Text<Renderer: self::Renderer> {
content: String,
size: Option<u16>,
color: Option<Color>,
- font: Font,
+ font: Renderer::Font,
width: Length,
height: Length,
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
}
-impl Text {
+impl<Renderer: self::Renderer> Text<Renderer> {
/// Create a new fragment of [`Text`] with the given contents.
- ///
- /// [`Text`]: struct.Text.html
pub fn new<T: Into<String>>(label: T) -> Self {
Text {
content: label.into(),
size: None,
color: None,
- font: Font::Default,
+ font: Default::default(),
width: Length::Shrink,
height: Length::Shrink,
horizontal_alignment: HorizontalAlignment::Left,
@@ -49,17 +47,12 @@ impl Text {
}
/// Sets the size of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
}
/// Sets the [`Color`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`Color`]: ../../struct.Color.html
pub fn color<C: Into<Color>>(mut self, color: C) -> Self {
self.color = Some(color.into());
self
@@ -67,33 +60,25 @@ impl Text {
/// Sets the [`Font`] of the [`Text`].
///
- /// [`Text`]: struct.Text.html
- /// [`Font`]: ../../struct.Font.html
- pub fn font(mut self, font: Font) -> Self {
- self.font = font;
+ /// [`Font`]: Renderer::Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = font.into();
self
}
/// Sets the width of the [`Text`] boundaries.
- ///
- /// [`Text`]: struct.Text.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Text`] boundaries.
- ///
- /// [`Text`]: struct.Text.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the [`HorizontalAlignment`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
pub fn horizontal_alignment(
mut self,
alignment: HorizontalAlignment,
@@ -103,16 +88,13 @@ impl Text {
}
/// Sets the [`VerticalAlignment`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`VerticalAlignment`]: enum.VerticalAlignment.html
pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.vertical_alignment = alignment;
self
}
}
-impl<Message, Renderer> Widget<Message, Renderer> for Text
+impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer>
where
Renderer: self::Renderer,
{
@@ -131,7 +113,7 @@ where
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
- let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE);
+ let size = self.size.unwrap_or(renderer.default_size());
let bounds = limits.max();
@@ -149,12 +131,13 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
defaults,
layout.bounds(),
&self.content,
- self.size.unwrap_or(Renderer::DEFAULT_SIZE),
+ self.size.unwrap_or(renderer.default_size()),
self.font,
self.color,
self.horizontal_alignment,
@@ -163,6 +146,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.content.hash(state);
self.size.hash(state);
self.width.hash(state);
@@ -173,26 +159,23 @@ where
/// The renderer of a [`Text`] fragment.
///
/// Your [renderer] will need to implement this trait before being
-/// able to use [`Text`] in your [`UserInterface`].
+/// able to use [`Text`] in your user interface.
///
-/// [`Text`]: struct.Text.html
-/// [renderer]: ../../renderer/index.html
-/// [`UserInterface`]: ../../struct.UserInterface.html
+/// [renderer]: crate::Renderer
pub trait Renderer: crate::Renderer {
- /// The default size of [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- const DEFAULT_SIZE: u16;
+ /// The font type used for [`Text`].
+ type Font: Default + Copy;
+
+ /// Returns the default size of [`Text`].
+ fn default_size(&self) -> u16;
/// Measures the [`Text`] in the given bounds and returns the minimum
/// boundaries that can fit the contents.
- ///
- /// [`Text`]: struct.Text.html
fn measure(
&self,
content: &str,
size: u16,
- font: Font,
+ font: Self::Font,
bounds: Size,
) -> (f32, f32);
@@ -205,28 +188,40 @@ pub trait Renderer: crate::Renderer {
/// * the color of the [`Text`]
/// * the [`HorizontalAlignment`] of the [`Text`]
/// * the [`VerticalAlignment`] of the [`Text`]
- ///
- /// [`Text`]: struct.Text.html
- /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
- /// [`VerticalAlignment`]: enum.VerticalAlignment.html
fn draw(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
content: &str,
size: u16,
- font: Font,
+ font: Self::Font,
color: Option<Color>,
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
) -> Self::Output;
}
-impl<'a, Message, Renderer> From<Text> for Element<'a, Message, Renderer>
+impl<'a, Message, Renderer> From<Text<Renderer>>
+ for Element<'a, Message, Renderer>
where
- Renderer: self::Renderer,
+ Renderer: self::Renderer + 'a,
{
- fn from(text: Text) -> Element<'a, Message, Renderer> {
+ fn from(text: Text<Renderer>) -> Element<'a, Message, Renderer> {
Element::new(text)
}
}
+
+impl<Renderer: self::Renderer> Clone for Text<Renderer> {
+ fn clone(&self) -> Self {
+ Self {
+ content: self.content.clone(),
+ size: self.size,
+ color: self.color,
+ font: self.font,
+ width: self.width,
+ height: self.height,
+ horizontal_alignment: self.horizontal_alignment,
+ vertical_alignment: self.vertical_alignment,
+ }
+ }
+}
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs
index c06a8cce..ca71c20c 100644
--- a/native/src/widget/text_input.rs
+++ b/native/src/widget/text_input.rs
@@ -1,17 +1,27 @@
//! Display fields that can be filled with text.
//!
//! A [`TextInput`] has some local [`State`].
-//!
-//! [`TextInput`]: struct.TextInput.html
-//! [`State`]: struct.State.html
+mod editor;
+mod value;
+
+pub mod cursor;
+
+pub use cursor::Cursor;
+pub use value::Value;
+
+use editor::Editor;
+
+use crate::event::{self, Event};
+use crate::keyboard;
+use crate::layout;
+use crate::mouse::{self, click};
+use crate::text;
+use crate::touch::{self, Touch};
use crate::{
- input::{keyboard, mouse, touch, ButtonState, Touch},
- layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point,
- Rectangle, Size, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
};
use std::u32;
-use unicode_segmentation::UnicodeSegmentation;
/// A field that can be filled with text.
///
@@ -43,7 +53,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {
placeholder: String,
value: Value,
is_secure: bool,
- font: Font,
+ font: Renderer::Font,
width: Length,
max_width: u32,
padding: u16,
@@ -53,7 +63,11 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {
style: Renderer::Style,
}
-impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
+impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
+where
+ Message: Clone,
+ Renderer: self::Renderer,
+{
/// Creates a new [`TextInput`].
///
/// It expects:
@@ -61,9 +75,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
/// - a placeholder
/// - the current value
/// - a function that produces a message when the [`TextInput`] changes
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
placeholder: &str,
@@ -78,7 +89,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
placeholder: String::from(placeholder),
value: Value::new(value),
is_secure: false,
- font: Font::Default,
+ font: Default::default(),
width: Length::Fill,
max_width: u32::MAX,
padding: 0,
@@ -90,8 +101,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
}
/// Converts the [`TextInput`] into a secure password input.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn password(mut self) -> Self {
self.is_secure = true;
self
@@ -99,39 +108,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
/// Sets the [`Font`] of the [`Text`].
///
- /// [`Text`]: struct.Text.html
- /// [`Font`]: ../../struct.Font.html
- pub fn font(mut self, font: Font) -> Self {
+ /// [`Font`]: crate::widget::text::Renderer::Font
+ /// [`Text`]: crate::widget::Text
+ pub fn font(mut self, font: Renderer::Font) -> Self {
self.font = font;
self
}
/// Sets the width of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the maximum width of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the padding of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the text size of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
@@ -139,27 +140,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
/// Sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn on_submit(mut self, message: Message) -> Self {
self.on_submit = Some(message);
self
}
/// Sets the style of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
}
+
+ /// Returns the current [`State`] of the [`TextInput`].
+ pub fn state(&self) -> &State {
+ self.state
+ }
+}
+
+impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ /// Draws the [`TextInput`] with the given [`Renderer`], overriding its
+ /// [`Value`] if provided.
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ value: Option<&Value>,
+ ) -> Renderer::Output {
+ let value = value.unwrap_or(&self.value);
+ let bounds = layout.bounds();
+ let text_bounds = layout.children().next().unwrap().bounds();
+
+ if self.is_secure {
+ self::Renderer::draw(
+ renderer,
+ bounds,
+ text_bounds,
+ cursor_position,
+ self.font,
+ self.size.unwrap_or(renderer.default_size()),
+ &self.placeholder,
+ &value.secure(),
+ &self.state,
+ &self.style,
+ )
+ } else {
+ self::Renderer::draw(
+ renderer,
+ bounds,
+ text_bounds,
+ cursor_position,
+ self.font,
+ self.size.unwrap_or(renderer.default_size()),
+ &self.placeholder,
+ value,
+ &self.state,
+ &self.style,
+ )
+ }
+ }
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for TextInput<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: Clone + std::fmt::Debug,
+ Message: Clone,
+ Renderer: self::Renderer,
{
fn width(&self) -> Length {
self.width
@@ -197,22 +246,91 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
}) => {
let is_clicked = layout.bounds().contains(cursor_position);
+ self.state.is_focused = is_clicked;
+
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,
+ self.state.last_click,
+ );
+
+ match click.kind() {
+ click::Kind::Single => {
+ if target > 0.0 {
+ let value = if self.is_secure {
+ self.value.secure()
+ } else {
+ self.value.clone()
+ };
+
+ let position = renderer.find_cursor_position(
+ text_layout.bounds(),
+ self.font,
+ self.size,
+ &value,
+ &self.state,
+ target,
+ );
+
+ self.state.cursor.move_to(position);
+ } else {
+ self.state.cursor.move_to(0);
+ }
+
+ self.state.is_dragging = true;
+ }
+ click::Kind::Double => {
+ if self.is_secure {
+ self.state.cursor.select_all(&self.value);
+ } else {
+ let position = renderer.find_cursor_position(
+ text_layout.bounds(),
+ self.font,
+ self.size,
+ &self.value,
+ &self.state,
+ target,
+ );
+
+ self.state.cursor.select_range(
+ self.value.previous_start_of_word(position),
+ self.value.next_end_of_word(position),
+ );
+ }
+
+ self.state.is_dragging = false;
+ }
+ click::Kind::Triple => {
+ self.state.cursor.select_all(&self.value);
+ self.state.is_dragging = false;
+ }
+ }
+
+ self.state.last_click = Some(click);
+
+ return event::Status::Captured;
+ }
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
+ self.state.is_dragging = false;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { position }) => {
+ if self.state.is_dragging {
+ let text_layout = layout.children().next().unwrap();
+ let target = position.x - text_layout.bounds().x;
+
if target > 0.0 {
let value = if self.is_secure {
self.value.secure()
@@ -220,154 +338,243 @@ where
self.value.clone()
};
- let size = self.size.unwrap_or(renderer.default_size());
-
- let offset = renderer.offset(
+ let position = renderer.find_cursor_position(
text_layout.bounds(),
- size,
+ self.font,
+ self.size,
&value,
&self.state,
- self.font,
+ target,
);
- self.state.cursor_position = find_cursor_position(
- renderer,
- target + offset,
- &value,
- size,
- 0,
- self.value.len(),
- self.font,
+ self.state.cursor.select_range(
+ self.state.cursor.start(&value),
+ position,
);
- } else {
- self.state.cursor_position = 0;
}
- }
- self.state.is_focused = is_clicked;
+ return event::Status::Captured;
+ }
}
Event::Keyboard(keyboard::Event::CharacterReceived(c))
if self.state.is_focused
&& self.state.is_pasting.is_none()
+ && !self.state.keyboard_modifiers.is_command_pressed()
&& !c.is_control() =>
{
- let cursor_position = self.state.cursor_position(&self.value);
+ let mut editor =
+ Editor::new(&mut self.value, &mut self.state.cursor);
- self.value.insert(cursor_position, c);
- self.state.move_cursor_right(&self.value);
+ editor.insert(c);
- let message = (self.on_change)(self.value.to_string());
+ let message = (self.on_change)(editor.contents());
messages.push(message);
+
+ return event::Status::Captured;
}
- Event::Keyboard(keyboard::Event::Input {
- key_code,
- state: ButtonState::Pressed,
- modifiers,
- }) if self.state.is_focused => match key_code {
- keyboard::KeyCode::Enter => {
- if let Some(on_submit) = self.on_submit.clone() {
- messages.push(on_submit);
+ Event::Keyboard(keyboard::Event::KeyPressed {
+ key_code, ..
+ }) if self.state.is_focused => {
+ let modifiers = self.state.keyboard_modifiers;
+
+ match key_code {
+ keyboard::KeyCode::Enter => {
+ if let Some(on_submit) = self.on_submit.clone() {
+ messages.push(on_submit);
+ }
}
- }
- keyboard::KeyCode::Backspace => {
- let cursor_position =
- self.state.cursor_position(&self.value);
+ keyboard::KeyCode::Backspace => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && self
+ .state
+ .cursor
+ .selection(&self.value)
+ .is_none()
+ {
+ if self.is_secure {
+ let cursor_pos =
+ self.state.cursor.end(&self.value);
+ self.state.cursor.select_range(0, cursor_pos);
+ } else {
+ self.state
+ .cursor
+ .select_left_by_words(&self.value);
+ }
+ }
- if cursor_position > 0 {
- self.state.move_cursor_left(&self.value);
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
- let _ = self.value.remove(cursor_position - 1);
+ editor.backspace();
- let message = (self.on_change)(self.value.to_string());
+ let message = (self.on_change)(editor.contents());
messages.push(message);
}
- }
- keyboard::KeyCode::Delete => {
- let cursor_position =
- self.state.cursor_position(&self.value);
+ keyboard::KeyCode::Delete => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && self
+ .state
+ .cursor
+ .selection(&self.value)
+ .is_none()
+ {
+ if self.is_secure {
+ let cursor_pos =
+ self.state.cursor.end(&self.value);
+ self.state
+ .cursor
+ .select_range(cursor_pos, self.value.len());
+ } else {
+ self.state
+ .cursor
+ .select_right_by_words(&self.value);
+ }
+ }
+
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
- if cursor_position < self.value.len() {
- let _ = self.value.remove(cursor_position);
+ editor.delete();
- let message = (self.on_change)(self.value.to_string());
+ let message = (self.on_change)(editor.contents());
messages.push(message);
}
- }
- keyboard::KeyCode::Left => {
- if platform::is_jump_modifier_pressed(modifiers)
- && !self.is_secure
- {
- self.state.move_cursor_left_by_words(&self.value);
- } else {
- self.state.move_cursor_left(&self.value);
+ keyboard::KeyCode::Left => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && !self.is_secure
+ {
+ if modifiers.shift {
+ self.state
+ .cursor
+ .select_left_by_words(&self.value);
+ } else {
+ self.state
+ .cursor
+ .move_left_by_words(&self.value);
+ }
+ } else if modifiers.shift {
+ self.state.cursor.select_left(&self.value)
+ } else {
+ self.state.cursor.move_left(&self.value);
+ }
}
- }
- keyboard::KeyCode::Right => {
- if platform::is_jump_modifier_pressed(modifiers)
- && !self.is_secure
- {
- self.state.move_cursor_right_by_words(&self.value);
- } else {
- self.state.move_cursor_right(&self.value);
+ keyboard::KeyCode::Right => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && !self.is_secure
+ {
+ if modifiers.shift {
+ self.state
+ .cursor
+ .select_right_by_words(&self.value);
+ } else {
+ self.state
+ .cursor
+ .move_right_by_words(&self.value);
+ }
+ } else if modifiers.shift {
+ self.state.cursor.select_right(&self.value)
+ } else {
+ self.state.cursor.move_right(&self.value);
+ }
}
- }
- keyboard::KeyCode::Home => {
- self.state.cursor_position = 0;
- }
- keyboard::KeyCode::End => {
- self.state.move_cursor_to_end(&self.value);
- }
- keyboard::KeyCode::V => {
- if platform::is_copy_paste_modifier_pressed(modifiers) {
- if let Some(clipboard) = clipboard {
- let content = match self.state.is_pasting.take() {
- Some(content) => content,
- None => {
- let content: String = clipboard
- .content()
- .unwrap_or(String::new())
- .chars()
- .filter(|c| !c.is_control())
- .collect();
-
- Value::new(&content)
- }
- };
-
- let cursor_position =
- self.state.cursor_position(&self.value);
-
- self.value
- .insert_many(cursor_position, content.clone());
-
- self.state.move_cursor_right_by_amount(
- &self.value,
- content.len(),
+ keyboard::KeyCode::Home => {
+ if modifiers.shift {
+ self.state.cursor.select_range(
+ self.state.cursor.start(&self.value),
+ 0,
);
- self.state.is_pasting = Some(content);
-
- let message =
- (self.on_change)(self.value.to_string());
- messages.push(message);
+ } else {
+ self.state.cursor.move_to(0);
+ }
+ }
+ keyboard::KeyCode::End => {
+ if modifiers.shift {
+ self.state.cursor.select_range(
+ self.state.cursor.start(&self.value),
+ self.value.len(),
+ );
+ } else {
+ self.state.cursor.move_to(self.value.len());
+ }
+ }
+ keyboard::KeyCode::V => {
+ if self.state.keyboard_modifiers.is_command_pressed() {
+ if let Some(clipboard) = clipboard {
+ let content = match self.state.is_pasting.take()
+ {
+ Some(content) => content,
+ None => {
+ let content: String = clipboard
+ .content()
+ .unwrap_or(String::new())
+ .chars()
+ .filter(|c| !c.is_control())
+ .collect();
+
+ Value::new(&content)
+ }
+ };
+
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
+
+ editor.paste(content.clone());
+
+ let message =
+ (self.on_change)(editor.contents());
+ messages.push(message);
+
+ self.state.is_pasting = Some(content);
+ }
+ } else {
+ self.state.is_pasting = None;
+ }
+ }
+ keyboard::KeyCode::A => {
+ if self.state.keyboard_modifiers.is_command_pressed() {
+ self.state.cursor.select_all(&self.value);
}
- } else {
+ }
+ keyboard::KeyCode::Escape => {
+ self.state.is_focused = false;
+ self.state.is_dragging = false;
self.state.is_pasting = None;
+
+ self.state.keyboard_modifiers =
+ keyboard::Modifiers::default();
}
+ _ => {}
}
- _ => {}
- },
- Event::Keyboard(keyboard::Event::Input {
- key_code,
- state: ButtonState::Released,
- ..
- }) => match key_code {
- keyboard::KeyCode::V => {
- self.state.is_pasting = None;
+
+ return event::Status::Captured;
+ }
+ Event::Keyboard(keyboard::Event::KeyReleased {
+ key_code, ..
+ }) if self.state.is_focused => {
+ match key_code {
+ keyboard::KeyCode::V => {
+ self.state.is_pasting = None;
+ }
+ _ => {}
}
- _ => {}
- },
+
+ return event::Status::Captured;
+ }
+ Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers))
+ if self.state.is_focused =>
+ {
+ self.state.keyboard_modifiers = modifiers;
+ }
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -376,41 +583,15 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
- let bounds = layout.bounds();
- let text_bounds = layout.children().next().unwrap().bounds();
-
- if self.is_secure {
- renderer.draw(
- bounds,
- text_bounds,
- cursor_position,
- self.size.unwrap_or(renderer.default_size()),
- self.font,
- &self.placeholder,
- &self.value.secure(),
- &self.state,
- &self.style,
- )
- } else {
- renderer.draw(
- bounds,
- text_bounds,
- cursor_position,
- self.size.unwrap_or(renderer.default_size()),
- self.font,
- &self.placeholder,
- &self.value,
- &self.state,
- &self.style,
- )
- }
+ self.draw(renderer, layout, cursor_position, None)
}
fn hash_layout(&self, state: &mut Hasher) {
use std::{any::TypeId, hash::Hash};
-
- TypeId::of::<TextInput<'static, (), Renderer>>().hash(state);
+ struct Marker;
+ TypeId::of::<Marker>().hash(state);
self.width.hash(state);
self.max_width.hash(state);
@@ -424,71 +605,82 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`TextInput`] in your user interface.
///
-/// [`TextInput`]: struct.TextInput.html
-/// [renderer]: ../../renderer/index.html
-pub trait Renderer: crate::Renderer + Sized {
+/// [renderer]: crate::renderer
+pub trait Renderer: text::Renderer + Sized {
/// The style supported by this renderer.
type Style: Default;
- /// Returns the default size of the text of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- fn default_size(&self) -> u16;
-
/// Returns the width of the value of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- fn measure_value(&self, value: &str, size: u16, font: Font) -> f32;
+ fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32;
/// Returns the current horizontal offset of the value of the
/// [`TextInput`].
///
/// This is the amount of horizontal scrolling applied when the [`Value`]
/// does not fit the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`Value`]: struct.Value.html
fn offset(
&self,
text_bounds: Rectangle,
+ font: Self::Font,
size: u16,
value: &Value,
state: &State,
- font: Font,
) -> f32;
/// Draws a [`TextInput`].
///
/// It receives:
- /// - its bounds of the [`TextInput`]
+ /// - the bounds of the [`TextInput`]
/// - the bounds of the text (i.e. the current value)
/// - the cursor position
/// - the placeholder to show when the value is empty
/// - the current [`Value`]
/// - the current [`State`]
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`Value`]: struct.Value.html
- /// [`State`]: struct.State.html
fn draw(
&mut self,
bounds: Rectangle,
text_bounds: Rectangle,
cursor_position: Point,
+ font: Self::Font,
size: u16,
- font: Font,
placeholder: &str,
value: &Value,
state: &State,
style: &Self::Style,
) -> Self::Output;
+
+ /// Computes the position of the text cursor at the given X coordinate of
+ /// a [`TextInput`].
+ fn find_cursor_position(
+ &self,
+ text_bounds: Rectangle,
+ font: Self::Font,
+ size: Option<u16>,
+ value: &Value,
+ state: &State,
+ x: f32,
+ ) -> usize {
+ let size = size.unwrap_or(self.default_size());
+
+ let offset = self.offset(text_bounds, font, size, &value, &state);
+
+ find_cursor_position(
+ self,
+ &value,
+ font,
+ size,
+ x + offset,
+ 0,
+ value.len(),
+ )
+ }
}
impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static + Clone + std::fmt::Debug,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer,
{
fn from(
text_input: TextInput<'a, Message, Renderer>,
@@ -498,245 +690,80 @@ where
}
/// The state of a [`TextInput`].
-///
-/// [`TextInput`]: struct.TextInput.html
#[derive(Debug, Default, Clone)]
pub struct State {
is_focused: bool,
+ is_dragging: bool,
is_pasting: Option<Value>,
- cursor_position: usize,
+ last_click: Option<mouse::Click>,
+ cursor: Cursor,
+ keyboard_modifiers: keyboard::Modifiers,
// TODO: Add stateful horizontal scrolling offset
}
impl State {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
Self::default()
}
/// Creates a new [`State`], representing a focused [`TextInput`].
- ///
- /// [`State`]: struct.State.html
pub fn focused() -> Self {
- use std::usize;
-
Self {
is_focused: true,
+ is_dragging: false,
is_pasting: None,
- cursor_position: usize::MAX,
+ last_click: None,
+ cursor: Cursor::default(),
+ keyboard_modifiers: keyboard::Modifiers::default(),
}
}
/// Returns whether the [`TextInput`] is currently focused or not.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn is_focused(&self) -> bool {
self.is_focused
}
- /// Returns the cursor position of a [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub fn cursor_position(&self, value: &Value) -> usize {
- self.cursor_position.min(value.len())
- }
-
- /// Moves the cursor of a [`TextInput`] to the left.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_left(&mut self, value: &Value) {
- let current = self.cursor_position(value);
-
- if current > 0 {
- self.cursor_position = current - 1;
- }
- }
-
- /// Moves the cursor of a [`TextInput`] to the right.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_right(&mut self, value: &Value) {
- self.move_cursor_right_by_amount(value, 1)
- }
-
- pub(crate) fn move_cursor_right_by_amount(
- &mut self,
- value: &Value,
- amount: usize,
- ) {
- let current = self.cursor_position(value);
- let new_position = current.saturating_add(amount);
-
- if new_position < value.len() + 1 {
- self.cursor_position = new_position;
- }
- }
-
- /// Moves the cursor of a [`TextInput`] to the previous start of a word.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_left_by_words(&mut self, value: &Value) {
- let current = self.cursor_position(value);
-
- self.cursor_position = value.previous_start_of_word(current);
+ /// Returns the [`Cursor`] of the [`TextInput`].
+ pub fn cursor(&self) -> Cursor {
+ self.cursor
}
- /// Moves the cursor of a [`TextInput`] to the next end of a word.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_right_by_words(&mut self, value: &Value) {
- let current = self.cursor_position(value);
-
- self.cursor_position = value.next_end_of_word(current);
+ /// Focuses the [`TextInput`].
+ pub fn focus(&mut self) {
+ self.is_focused = true;
}
- /// Moves the cursor of a [`TextInput`] to the end.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_to_end(&mut self, value: &Value) {
- self.cursor_position = value.len();
+ /// Unfocuses the [`TextInput`].
+ pub fn unfocus(&mut self) {
+ self.is_focused = false;
}
-}
-/// The value of a [`TextInput`].
-///
-/// [`TextInput`]: struct.TextInput.html
-// TODO: Reduce allocations, cache results (?)
-#[derive(Debug, Clone)]
-pub struct Value {
- graphemes: Vec<String>,
-}
-
-impl Value {
- /// Creates a new [`Value`] from a string slice.
- ///
- /// [`Value`]: struct.Value.html
- pub fn new(string: &str) -> Self {
- let graphemes = UnicodeSegmentation::graphemes(string, true)
- .map(String::from)
- .collect();
-
- Self { graphemes }
- }
-
- /// Returns the total amount of graphemes in the [`Value`].
- ///
- /// [`Value`]: struct.Value.html
- pub fn len(&self) -> usize {
- self.graphemes.len()
- }
-
- /// Returns the position of the previous start of a word from the given
- /// grapheme `index`.
- ///
- /// [`Value`]: struct.Value.html
- 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)
+ /// 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);
}
- /// Returns the position of the next end of a word from the given grapheme
- /// `index`.
- ///
- /// [`Value`]: struct.Value.html
- 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)
- .filter(|(_, word)| !word.trim_start().is_empty())
- .next()
- .map(|(i, next_word)| {
- index
- + UnicodeSegmentation::graphemes(next_word, true).count()
- + UnicodeSegmentation::graphemes(
- &next_string[..i] as &str,
- true,
- )
- .count()
- })
- .unwrap_or(self.len())
+ /// 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);
}
- /// Returns a new [`Value`] containing the graphemes until the given
- /// `index`.
- ///
- /// [`Value`]: struct.Value.html
- 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`.
- ///
- /// [`Value`]: struct.Value.html
- 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`.
- ///
- /// [`Value`]: struct.Value.html
- pub fn remove(&mut self, index: usize) {
- let _ = self.graphemes.remove(index);
- }
-
- /// Returns a new [`Value`] with all its graphemes replaced with the
- /// dot ('•') character.
- ///
- /// [`Value`]: struct.Value.html
- pub fn secure(&self) -> Self {
- Self {
- graphemes: std::iter::repeat(String::from("•"))
- .take(self.graphemes.len())
- .collect(),
- }
+ /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location.
+ pub fn move_cursor_to(&mut self, position: usize) {
+ self.cursor.move_to(position);
}
}
// TODO: Reduce allocations
fn find_cursor_position<Renderer: self::Renderer>(
renderer: &Renderer,
- target: f32,
value: &Value,
+ font: Renderer::Font,
size: u16,
+ target: f32,
start: usize,
end: usize,
- font: Font,
) -> usize {
if start >= end {
if start == 0 {
@@ -764,46 +791,34 @@ fn find_cursor_position<Renderer: self::Renderer>(
if width > target {
find_cursor_position(
renderer,
- target,
value,
+ font,
size,
+ target,
start,
start + index,
- font,
)
} else {
find_cursor_position(
renderer,
- target,
value,
+ font,
size,
+ target,
start + index + 1,
end,
- font,
)
}
}
mod platform {
- use crate::input::keyboard;
+ use crate::keyboard;
- pub fn is_jump_modifier_pressed(
- modifiers: keyboard::ModifiersState,
- ) -> bool {
+ pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
modifiers.alt
} else {
modifiers.control
}
}
-
- pub fn is_copy_paste_modifier_pressed(
- modifiers: keyboard::ModifiersState,
- ) -> bool {
- if cfg!(target_os = "macos") {
- modifiers.logo
- } else {
- modifiers.control
- }
- }
}
diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs
new file mode 100644
index 00000000..e630e293
--- /dev/null
+++ b/native/src/widget/text_input/cursor.rs
@@ -0,0 +1,186 @@
+//! Track the cursor of a text input.
+use crate::widget::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 }
+ }
+ }
+ }
+ }
+
+ 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())
+ }
+
+ pub(crate) fn selection(&self, value: &Value) -> Option<(usize, usize)> {
+ match self.state(value) {
+ State::Selection { start, end } => {
+ Some((start.min(end), start.max(end)))
+ }
+ _ => None,
+ }
+ }
+
+ 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/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs
new file mode 100644
index 00000000..20e42567
--- /dev/null
+++ b/native/src/widget/text_input/editor.rs
@@ -0,0 +1,77 @@
+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) {
+ match self.cursor.selection(self.value) {
+ Some((left, right)) => {
+ 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();
+
+ match self.cursor.selection(self.value) {
+ Some((left, right)) => {
+ 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/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs
new file mode 100644
index 00000000..86be2790
--- /dev/null
+++ b/native/src/widget/text_input/value.rs
@@ -0,0 +1,125 @@
+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)
+ .filter(|(_, word)| !word.trim_start().is_empty())
+ .next()
+ .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 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(),
+ }
+ }
+}