Create iced_widget subcrate and re-organize the whole codebase

This commit is contained in:
Héctor Ramón Jiménez 2023-03-04 05:37:11 +01:00
parent c54409d171
commit 3a0d34c024
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
209 changed files with 1959 additions and 2183 deletions

37
widget/Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "iced_widget"
version = "0.1.0"
edition = "2021"
[features]
lazy = ["ouroboros"]
image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
qr_code = ["canvas", "qrcode"]
[dependencies]
unicode-segmentation = "1.6"
num-traits = "0.2"
thiserror = "1"
[dependencies.iced_native]
version = "0.9"
path = "../native"
[dependencies.iced_renderer]
version = "0.1"
path = "../renderer"
[dependencies.iced_style]
version = "0.7"
path = "../style"
[dependencies.ouroboros]
version = "0.13"
optional = true
[dependencies.qrcode]
version = "0.12"
optional = true
default-features = false

455
widget/src/button.rs Normal file
View file

@ -0,0 +1,455 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Point,
Rectangle, Shell, Vector, Widget,
};
pub use iced_style::button::{Appearance, StyleSheet};
/// A generic widget that produces a message when pressed.
///
/// ```
/// # type Button<'a, Message> =
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
///
/// let button = Button::new("Press me!").on_press(Message::ButtonPressed);
/// ```
///
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
/// be disabled:
///
/// ```
/// # type Button<'a, Message> =
/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
///
/// fn disabled_button<'a>() -> Button<'a, Message> {
/// Button::new("I'm disabled!")
/// }
///
/// fn enabled_button<'a>() -> Button<'a, Message> {
/// disabled_button().on_press(Message::ButtonPressed)
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Button<'a, Message, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
content: Element<'a, Message, Renderer>,
on_press: Option<Message>,
width: Length,
height: Length,
padding: Padding,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a new [`Button`] with the given content.
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
Button {
content: content.into(),
on_press: None,
width: Length::Shrink,
height: Length::Shrink,
padding: Padding::new(5.0),
style: <Renderer::Theme as StyleSheet>::Style::default(),
}
}
/// Sets the width of the [`Button`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Button`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the [`Padding`] of the [`Button`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
}
/// Sets the style variant of this [`Button`].
pub fn style(
mut self,
style: <Renderer::Theme as StyleSheet>::Style,
) -> Self {
self.style = style;
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Button<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.height,
self.padding,
|renderer, limits| {
self.content.as_widget().layout(renderer, limits)
},
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
if let event::Status::Captured = self.content.as_widget_mut().on_event(
&mut tree.children[0],
event.clone(),
layout.children().next().unwrap(),
cursor_position,
renderer,
clipboard,
shell,
) {
return event::Status::Captured;
}
update(
event,
layout,
cursor_position,
shell,
&self.on_press,
|| tree.state.downcast_mut::<State>(),
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let styling = draw(
renderer,
bounds,
cursor_position,
self.on_press.is_some(),
theme,
&self.style,
|| tree.state.downcast_ref::<State>(),
);
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
&renderer::Style {
text_color: styling.text_color,
},
content_layout,
cursor_position,
&bounds,
);
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(layout, cursor_position, self.on_press.is_some())
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
)
}
}
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: Clone + 'a,
Renderer: crate::core::Renderer + 'a,
Renderer::Theme: StyleSheet,
{
fn from(button: Button<'a, Message, Renderer>) -> Self {
Self::new(button)
}
}
/// The local state of a [`Button`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_pressed: bool,
}
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State::default()
}
}
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
/// accordingly.
pub fn update<'a, Message: Clone>(
event: Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
on_press: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if on_press.is_some() {
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
let state = state();
state.is_pressed = true;
return event::Status::Captured;
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = on_press.clone() {
let state = state();
if state.is_pressed {
state.is_pressed = false;
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
shell.publish(on_press);
}
return event::Status::Captured;
}
}
}
Event::Touch(touch::Event::FingerLost { .. }) => {
let state = state();
state.is_pressed = false;
}
_ => {}
}
event::Status::Ignored
}
/// Draws a [`Button`].
pub fn draw<'a, Renderer: crate::core::Renderer>(
renderer: &mut Renderer,
bounds: Rectangle,
cursor_position: Point,
is_enabled: bool,
style_sheet: &dyn StyleSheet<
Style = <Renderer::Theme as StyleSheet>::Style,
>,
style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State,
) -> Appearance
where
Renderer::Theme: StyleSheet,
{
let is_mouse_over = bounds.contains(cursor_position);
let styling = if !is_enabled {
style_sheet.disabled(style)
} else if is_mouse_over {
let state = state();
if state.is_pressed {
style_sheet.pressed(style)
} else {
style_sheet.hovered(style)
}
} else {
style_sheet.active(style)
};
if styling.background.is_some() || styling.border_width > 0.0 {
if styling.shadow_offset != Vector::default() {
// TODO: Implement proper shadow support
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + styling.shadow_offset.x,
y: bounds.y + styling.shadow_offset.y,
..bounds
},
border_radius: styling.border_radius.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Background::Color([0.0, 0.0, 0.0, 0.5].into()),
);
}
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: styling.border_radius.into(),
border_width: styling.border_width,
border_color: styling.border_color,
},
styling
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
styling
}
/// Computes the layout of a [`Button`].
pub fn layout<Renderer>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
height: Length,
padding: Padding,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
let mut content = layout_content(renderer, &limits.pad(padding));
let padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size()).pad(padding);
content.move_to(Point::new(padding.left, padding.top));
layout::Node::with_children(size, vec![content])
}
/// Returns the [`mouse::Interaction`] of a [`Button`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor_position: Point,
is_enabled: bool,
) -> mouse::Interaction {
let is_mouse_over = layout.bounds().contains(cursor_position);
if is_mouse_over && is_enabled {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}

238
widget/src/canvas.rs Normal file
View file

@ -0,0 +1,238 @@
//! Draw 2D graphics for your users.
pub mod event;
mod cursor;
mod program;
pub use cursor::Cursor;
pub use event::Event;
pub use program::Program;
pub use crate::graphics::geometry::*;
pub use crate::renderer::geometry::*;
use crate::core;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{Clipboard, Element, Shell, Widget};
use crate::core::{Length, Point, Rectangle, Size, Vector};
use crate::graphics::geometry;
use std::marker::PhantomData;
/// A widget capable of drawing 2D graphics.
///
/// ## Drawing a simple circle
/// If you want to get a quick overview, here's how we can draw a simple circle:
///
/// ```no_run
/// # use iced_widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program};
/// # use iced_widget::core::{Color, Rectangle};
/// # use iced_widget::style::Theme;
/// #
/// # pub type Renderer = iced_widget::renderer::Renderer<Theme>;
/// // First, we define the data we need for drawing
/// #[derive(Debug)]
/// struct Circle {
/// radius: f32,
/// }
///
/// // Then, we implement the `Program` trait
/// impl Program<()> for Circle {
/// type State = ();
///
/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{
/// // We prepare a new `Frame`
/// let mut frame = Frame::new(renderer, bounds.size());
///
/// // We create a `Path` representing a simple circle
/// let circle = Path::circle(frame.center(), self.radius);
///
/// // And fill it with some color
/// frame.fill(&circle, Color::BLACK);
///
/// // Finally, we produce the geometry
/// vec![frame.into_geometry()]
/// }
/// }
///
/// // Finally, we simply use our `Circle` to create the `Canvas`!
/// let canvas = Canvas::new(Circle { radius: 50.0 });
/// ```
#[derive(Debug)]
pub struct Canvas<P, Message, Renderer = crate::Renderer>
where
Renderer: geometry::Renderer,
P: Program<Message, Renderer>,
{
width: Length,
height: Length,
program: P,
message_: PhantomData<Message>,
theme_: PhantomData<Renderer>,
}
impl<P, Message, Renderer> Canvas<P, Message, Renderer>
where
Renderer: geometry::Renderer,
P: Program<Message, Renderer>,
{
const DEFAULT_SIZE: f32 = 100.0;
/// Creates a new [`Canvas`].
pub fn new(program: P) -> Self {
Canvas {
width: Length::Fixed(Self::DEFAULT_SIZE),
height: Length::Fixed(Self::DEFAULT_SIZE),
program,
message_: PhantomData,
theme_: PhantomData,
}
}
/// Sets the width of the [`Canvas`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Canvas`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
impl<P, Message, Renderer> Widget<Message, Renderer>
for Canvas<P, Message, Renderer>
where
Renderer: geometry::Renderer,
P: Program<Message, Renderer>,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
tree::Tag::of::<Tag<P::State>>()
}
fn state(&self) -> tree::State {
tree::State::new(P::State::default())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let size = limits.resolve(Size::ZERO);
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: core::Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let bounds = layout.bounds();
let canvas_event = match event {
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
core::Event::Keyboard(keyboard_event) => {
Some(Event::Keyboard(keyboard_event))
}
_ => None,
};
let cursor = Cursor::from_window_position(cursor_position);
if let Some(canvas_event) = canvas_event {
let state = tree.state.downcast_mut::<P::State>();
let (event_status, message) =
self.program.update(state, canvas_event, bounds, cursor);
if let Some(message) = message {
shell.publish(message);
}
return event_status;
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let cursor = Cursor::from_window_position(cursor_position);
let state = tree.state.downcast_ref::<P::State>();
self.program.mouse_interaction(state, bounds, cursor)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
if bounds.width < 1.0 || bounds.height < 1.0 {
return;
}
let cursor = Cursor::from_window_position(cursor_position);
let state = tree.state.downcast_ref::<P::State>();
renderer.with_translation(
Vector::new(bounds.x, bounds.y),
|renderer| {
renderer.draw(
self.program.draw(state, renderer, theme, bounds, cursor),
);
},
);
}
}
impl<'a, P, Message, Renderer> From<Canvas<P, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + geometry::Renderer,
P: Program<Message, Renderer> + 'a,
{
fn from(
canvas: Canvas<P, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(canvas)
}
}

View file

@ -0,0 +1,64 @@
use crate::core::{Point, Rectangle};
/// The mouse cursor state.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Cursor {
/// The cursor has a defined position.
Available(Point),
/// The cursor is currently unavailable (i.e. out of bounds or busy).
Unavailable,
}
impl Cursor {
// TODO: Remove this once this type is used in `iced_native` to encode
// proper cursor availability
pub(crate) fn from_window_position(position: Point) -> Self {
if position.x < 0.0 || position.y < 0.0 {
Cursor::Unavailable
} else {
Cursor::Available(position)
}
}
/// Returns the absolute position of the [`Cursor`], if available.
pub fn position(&self) -> Option<Point> {
match self {
Cursor::Available(position) => Some(*position),
Cursor::Unavailable => None,
}
}
/// Returns the relative position of the [`Cursor`] inside the given bounds,
/// if available.
///
/// If the [`Cursor`] is not over the provided bounds, this method will
/// return `None`.
pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> {
if self.is_over(bounds) {
self.position_from(bounds.position())
} else {
None
}
}
/// Returns the relative position of the [`Cursor`] from the given origin,
/// if available.
pub fn position_from(&self, origin: Point) -> Option<Point> {
match self {
Cursor::Available(position) => {
Some(Point::new(position.x - origin.x, position.y - origin.y))
}
Cursor::Unavailable => None,
}
}
/// Returns whether the [`Cursor`] is currently over the provided bounds
/// or not.
pub fn is_over(&self, bounds: &Rectangle) -> bool {
match self {
Cursor::Available(position) => bounds.contains(*position),
Cursor::Unavailable => false,
}
}
}

View file

@ -0,0 +1,21 @@
//! Handle events of a canvas.
use crate::core::keyboard;
use crate::core::mouse;
use crate::core::touch;
pub use crate::core::event::Status;
/// A [`Canvas`] event.
///
/// [`Canvas`]: crate::widget::Canvas
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Event {
/// A mouse event.
Mouse(mouse::Event),
/// A touch event.
Touch(touch::Event),
/// A keyboard event.
Keyboard(keyboard::Event),
}

View file

@ -0,0 +1,109 @@
use crate::canvas::event::{self, Event};
use crate::canvas::mouse;
use crate::canvas::Cursor;
use crate::core::Rectangle;
use crate::graphics::geometry;
/// The state and logic of a [`Canvas`].
///
/// A [`Program`] can mutate internal state and produce messages for an
/// application.
///
/// [`Canvas`]: crate::widget::Canvas
pub trait Program<Message, Renderer = crate::Renderer>
where
Renderer: geometry::Renderer,
{
/// The internal state mutated by the [`Program`].
type State: Default + 'static;
/// Updates the [`State`](Self::State) of the [`Program`].
///
/// When a [`Program`] is used in a [`Canvas`], the runtime will call this
/// method for each [`Event`].
///
/// This method can optionally return a `Message` to notify an application
/// of any meaningful interactions.
///
/// By default, this method does and returns nothing.
///
/// [`Canvas`]: crate::widget::Canvas
fn update(
&self,
_state: &mut Self::State,
_event: Event,
_bounds: Rectangle,
_cursor: Cursor,
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
}
/// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
///
/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
/// [`Cache`].
///
/// [`Frame`]: crate::widget::canvas::Frame
/// [`Cache`]: crate::widget::canvas::Cache
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
theme: &Renderer::Theme,
bounds: Rectangle,
cursor: Cursor,
) -> Vec<Renderer::Geometry>;
/// Returns the current mouse interaction of the [`Program`].
///
/// The interaction returned will be in effect even if the cursor position
/// is out of bounds of the program's [`Canvas`].
///
/// [`Canvas`]: crate::widget::Canvas
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
}
impl<Message, Renderer, T> Program<Message, Renderer> for &T
where
Renderer: geometry::Renderer,
T: Program<Message, Renderer>,
{
type State = T::State;
fn update(
&self,
state: &mut Self::State,
event: Event,
bounds: Rectangle,
cursor: Cursor,
) -> (event::Status, Option<Message>) {
T::update(self, state, event, bounds, cursor)
}
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
theme: &Renderer::Theme,
bounds: Rectangle,
cursor: Cursor,
) -> Vec<Renderer::Geometry> {
T::draw(self, state, renderer, theme, bounds, cursor)
}
fn mouse_interaction(
&self,
state: &Self::State,
bounds: Rectangle,
cursor: Cursor,
) -> mouse::Interaction {
T::mouse_interaction(self, state, bounds, cursor)
}
}

323
widget/src/checkbox.rs Normal file
View file

@ -0,0 +1,323 @@
//! Show toggle controls using checkboxes.
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
use crate::core::widget::Tree;
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
Shell, Widget,
};
use crate::{Row, Text};
pub use iced_style::checkbox::{Appearance, StyleSheet};
/// The icon in a [`Checkbox`].
#[derive(Debug, Clone, PartialEq)]
pub struct Icon<Font> {
/// Font that will be used to display the `code_point`,
pub font: Font,
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
pub size: Option<f32>,
}
/// A box that can be checked.
///
/// # Example
///
/// ```
/// # type Checkbox<'a, Message> =
/// # iced_widget::Checkbox<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// pub enum Message {
/// CheckboxToggled(bool),
/// }
///
/// let is_checked = true;
///
/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled);
/// ```
///
/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
#[allow(missing_debug_implementations)]
pub struct Checkbox<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
is_checked: bool,
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
label: String,
width: Length,
size: f32,
spacing: f32,
text_size: Option<f32>,
font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
/// The default size of a [`Checkbox`].
const DEFAULT_SIZE: f32 = 20.0;
/// The default spacing of a [`Checkbox`].
const DEFAULT_SPACING: f32 = 15.0;
/// Creates a new [`Checkbox`].
///
/// It expects:
/// * a boolean describing whether the [`Checkbox`] is checked or not
/// * the label of the [`Checkbox`]
/// * a function that will be called when the [`Checkbox`] is toggled. It
/// will receive the new state of the [`Checkbox`] and must produce a
/// `Message`.
pub fn new<F>(label: impl Into<String>, is_checked: bool, f: F) -> Self
where
F: 'a + Fn(bool) -> Message,
{
Checkbox {
is_checked,
on_toggle: Box::new(f),
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING,
text_size: None,
font: None,
icon: Icon {
font: Renderer::ICON_FONT,
code_point: Renderer::CHECKMARK_ICON,
size: None,
},
style: Default::default(),
}
}
/// Sets the size of the [`Checkbox`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = size.into().0;
self
}
/// Sets the width of the [`Checkbox`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the spacing between the [`Checkbox`] and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
self
}
/// Sets the text size of the [`Checkbox`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
self.text_size = Some(text_size.into().0);
self
}
/// Sets the [`Font`] of the text of the [`Checkbox`].
///
/// [`Font`]: crate::text::Renderer::Font
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the [`Icon`] of the [`Checkbox`].
pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
self.icon = icon;
self
}
/// Sets the style of the [`Checkbox`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Checkbox<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
Row::<(), Renderer>::new()
.width(self.width)
.spacing(self.spacing)
.align_items(Alignment::Center)
.push(Row::new().width(self.size).height(self.size))
.push(
Text::new(&self.label)
.font(self.font.unwrap_or_else(|| renderer.default_font()))
.width(self.width)
.size(
self.text_size
.unwrap_or_else(|| renderer.default_size()),
),
)
.layout(renderer, limits)
}
fn on_event(
&mut self,
_tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = layout.bounds().contains(cursor_position);
if mouse_over {
shell.publish((self.on_toggle)(!self.is_checked));
return event::Status::Captured;
}
}
_ => {}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if layout.bounds().contains(cursor_position) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let mut children = layout.children();
let custom_style = if is_mouse_over {
theme.hovered(&self.style, self.is_checked)
} else {
theme.active(&self.style, self.is_checked)
};
{
let layout = children.next().unwrap();
let bounds = layout.bounds();
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: custom_style.border_radius.into(),
border_width: custom_style.border_width,
border_color: custom_style.border_color,
},
custom_style.background,
);
let Icon {
font,
code_point,
size,
} = &self.icon;
let size = size.unwrap_or(bounds.height * 0.7);
if self.is_checked {
renderer.fill_text(text::Text {
content: &code_point.to_string(),
font: *font,
size,
bounds: Rectangle {
x: bounds.center_x(),
y: bounds.center_y(),
..bounds
},
color: custom_style.icon_color,
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
});
}
}
{
let label_layout = children.next().unwrap();
crate::text::draw(
renderer,
style,
label_layout,
&self.label,
self.text_size,
self.font,
crate::text::Appearance {
color: custom_style.text_color,
},
alignment::Horizontal::Left,
alignment::Vertical::Center,
);
}
}
}
impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn from(
checkbox: Checkbox<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(checkbox)
}
}

264
widget/src/column.rs Normal file
View file

@ -0,0 +1,264 @@
//! Distribute content vertically.
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point,
Rectangle, Shell, Widget,
};
/// A container that distributes its contents vertically.
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message, Renderer = crate::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
align_items: Alignment,
children: Vec<Element<'a, Message, Renderer>>,
}
impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
/// Creates an empty [`Column`].
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Column`] with the given elements.
pub fn with_children(
children: Vec<Element<'a, Message, Renderer>>,
) -> Self {
Column {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
children,
}
}
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the [`Padding`] of the [`Column`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the width of the [`Column`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Column`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the maximum width of the [`Column`].
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = max_width.into().0;
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
pub fn align_items(mut self, align: Alignment) -> Self {
self.align_items = align;
self
}
/// Adds an element to the [`Column`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
self.children.push(child.into());
self
}
}
impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> {
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Column<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&self.children);
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits
.max_width(self.max_width)
.width(self.width)
.height(self.height);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
self.padding,
self.spacing,
self.align_items,
&self.children,
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.operate(state, layout, renderer, operation);
})
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor_position,
renderer,
clipboard,
shell,
)
})
.fold(event::Status::Ignored, event::Status::merge)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
state,
layout,
cursor_position,
viewport,
renderer,
)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
{
child.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor_position,
viewport,
);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
overlay::from_children(&mut self.children, tree, layout, renderer)
}
}
impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(column: Column<'a, Message, Renderer>) -> Self {
Self::new(column)
}
}

368
widget/src/container.rs Normal file
View file

@ -0,0 +1,368 @@
//! Decorate content and apply alignment.
use crate::core::alignment::{self, Alignment};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{self, Operation, Tree};
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
Point, Rectangle, Shell, Widget,
};
pub use iced_style::container::{Appearance, StyleSheet};
/// An element decorating some content.
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct Container<'a, Message, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
id: Option<Id>,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
max_height: f32,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
style: <Renderer::Theme as StyleSheet>::Style,
content: Element<'a, Message, Renderer>,
}
impl<'a, Message, Renderer> Container<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates an empty [`Container`].
pub fn new<T>(content: T) -> Self
where
T: Into<Element<'a, Message, Renderer>>,
{
Container {
id: None,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
max_height: f32::INFINITY,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
style: Default::default(),
content: content.into(),
}
}
/// Sets the [`Id`] of the [`Container`].
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
/// Sets the [`Padding`] of the [`Container`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the width of the [`Container`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Container`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the maximum width of the [`Container`].
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = max_width.into().0;
self
}
/// Sets the maximum height of the [`Container`].
pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
self.max_height = max_height.into().0;
self
}
/// Sets the content alignment for the horizontal axis of the [`Container`].
pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
self.horizontal_alignment = alignment;
self
}
/// Sets the content alignment for the vertical axis of the [`Container`].
pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
self.vertical_alignment = alignment;
self
}
/// Centers the contents in the horizontal axis of the [`Container`].
pub fn center_x(mut self) -> Self {
self.horizontal_alignment = alignment::Horizontal::Center;
self
}
/// Centers the contents in the vertical axis of the [`Container`].
pub fn center_y(mut self) -> Self {
self.vertical_alignment = alignment::Vertical::Center;
self
}
/// Sets the style of the [`Container`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Container<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.height,
self.max_width,
self.max_height,
self.padding,
self.horizontal_alignment,
self.vertical_alignment,
|renderer, limits| {
self.content.as_widget().layout(renderer, limits)
},
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(
self.id.as_ref().map(|id| &id.0),
&mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
},
);
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout.children().next().unwrap(),
cursor_position,
renderer,
clipboard,
shell,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout.children().next().unwrap(),
cursor_position,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
let style = theme.appearance(&self.style);
draw_background(renderer, &style, layout.bounds());
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
&renderer::Style {
text_color: style
.text_color
.unwrap_or(renderer_style.text_color),
},
layout.children().next().unwrap(),
cursor_position,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
)
}
}
impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
column: Container<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(column)
}
}
/// Computes the layout of a [`Container`].
pub fn layout<Renderer>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
height: Length,
max_width: f32,
max_height: f32,
padding: Padding,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits
.loose()
.max_width(max_width)
.max_height(max_height)
.width(width)
.height(height);
let mut content = layout_content(renderer, &limits.pad(padding).loose());
let padding = padding.fit(content.size(), limits.max());
let size = limits.pad(padding).resolve(content.size());
content.move_to(Point::new(padding.left, padding.top));
content.align(
Alignment::from(horizontal_alignment),
Alignment::from(vertical_alignment),
size,
);
layout::Node::with_children(size.pad(padding), vec![content])
}
/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`.
pub fn draw_background<Renderer>(
renderer: &mut Renderer,
appearance: &Appearance,
bounds: Rectangle,
) where
Renderer: crate::core::Renderer,
{
if appearance.background.is_some() || appearance.border_width > 0.0 {
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: appearance.border_radius.into(),
border_width: appearance.border_width,
border_color: appearance.border_color,
},
appearance
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
}
/// The identifier of a [`Container`].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Id(widget::Id);
impl Id {
/// Creates a custom [`Id`].
pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
Self(widget::Id::new(id))
}
/// Creates a unique [`Id`].
///
/// This function produces a different [`Id`] every time it is called.
pub fn unique() -> Self {
Self(widget::Id::unique())
}
}
impl From<Id> for widget::Id {
fn from(id: Id) -> Self {
id.0
}
}

362
widget/src/helpers.rs Normal file
View file

@ -0,0 +1,362 @@
//! Helper functions to create pure widgets.
use crate::button::{self, Button};
use crate::checkbox::{self, Checkbox};
use crate::container::{self, Container};
use crate::core;
use crate::core::widget::operation;
use crate::core::{Element, Length, Pixels};
use crate::native::Command;
use crate::overlay;
use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar};
use crate::radio::{self, Radio};
use crate::rule::{self, Rule};
use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider};
use crate::text::{self, Text};
use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
use crate::{Column, Row, Space, VerticalSlider};
use std::borrow::Cow;
use std::ops::RangeInclusive;
/// Creates a [`Column`] with the given children.
///
/// [`Column`]: widget::Column
#[macro_export]
macro_rules! column {
() => (
$crate::Column::new()
);
($($x:expr),+ $(,)?) => (
$crate::Column::with_children(vec![$($crate::core::Element::from($x)),+])
);
}
/// Creates a [`Row`] with the given children.
///
/// [`Row`]: widget::Row
#[macro_export]
macro_rules! row {
() => (
$crate::Row::new()
);
($($x:expr),+ $(,)?) => (
$crate::Row::with_children(vec![$($crate::core::Element::from($x)),+])
);
}
/// Creates a new [`Container`] with the provided content.
///
/// [`Container`]: widget::Container
pub fn container<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Container<'a, Message, Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: container::StyleSheet,
{
Container::new(content)
}
/// Creates a new [`Column`] with the given children.
///
/// [`Column`]: widget::Column
pub fn column<Message, Renderer>(
children: Vec<Element<'_, Message, Renderer>>,
) -> Column<'_, Message, Renderer> {
Column::with_children(children)
}
/// Creates a new [`Row`] with the given children.
///
/// [`Row`]: widget::Row
pub fn row<Message, Renderer>(
children: Vec<Element<'_, Message, Renderer>>,
) -> Row<'_, Message, Renderer> {
Row::with_children(children)
}
/// Creates a new [`Scrollable`] with the provided content.
///
/// [`Scrollable`]: widget::Scrollable
pub fn scrollable<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Scrollable<'a, Message, Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: scrollable::StyleSheet,
{
Scrollable::new(content)
}
/// Creates a new [`Button`] with the provided content.
///
/// [`Button`]: widget::Button
pub fn button<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Button<'a, Message, Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: button::StyleSheet,
<Renderer::Theme as button::StyleSheet>::Style: Default,
{
Button::new(content)
}
/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`].
///
/// [`Tooltip`]: widget::Tooltip
/// [`tooltip::Position`]: widget::tooltip::Position
pub fn tooltip<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
tooltip: impl ToString,
position: tooltip::Position,
) -> crate::Tooltip<'a, Message, Renderer>
where
Renderer: core::text::Renderer,
Renderer::Theme: container::StyleSheet + text::StyleSheet,
{
Tooltip::new(content, tooltip.to_string(), position)
}
/// Creates a new [`Text`] widget with the provided content.
///
/// [`Text`]: widget::Text
pub fn text<'a, Renderer>(text: impl ToString) -> Text<'a, Renderer>
where
Renderer: core::text::Renderer,
Renderer::Theme: text::StyleSheet,
{
Text::new(text.to_string())
}
/// Creates a new [`Checkbox`].
///
/// [`Checkbox`]: widget::Checkbox
pub fn checkbox<'a, Message, Renderer>(
label: impl Into<String>,
is_checked: bool,
f: impl Fn(bool) -> Message + 'a,
) -> Checkbox<'a, Message, Renderer>
where
Renderer: core::text::Renderer,
Renderer::Theme: checkbox::StyleSheet + text::StyleSheet,
{
Checkbox::new(label, is_checked, f)
}
/// Creates a new [`Radio`].
///
/// [`Radio`]: widget::Radio
pub fn radio<Message, Renderer, V>(
label: impl Into<String>,
value: V,
selected: Option<V>,
on_click: impl FnOnce(V) -> Message,
) -> Radio<Message, Renderer>
where
Message: Clone,
Renderer: core::text::Renderer,
Renderer::Theme: radio::StyleSheet,
V: Copy + Eq,
{
Radio::new(value, label, selected, on_click)
}
/// Creates a new [`Toggler`].
///
/// [`Toggler`]: widget::Toggler
pub fn toggler<'a, Message, Renderer>(
label: impl Into<Option<String>>,
is_checked: bool,
f: impl Fn(bool) -> Message + 'a,
) -> Toggler<'a, Message, Renderer>
where
Renderer: core::text::Renderer,
Renderer::Theme: toggler::StyleSheet,
{
Toggler::new(label, is_checked, f)
}
/// Creates a new [`TextInput`].
///
/// [`TextInput`]: widget::TextInput
pub fn text_input<'a, Message, Renderer>(
placeholder: &str,
value: &str,
on_change: impl Fn(String) -> Message + 'a,
) -> TextInput<'a, Message, Renderer>
where
Message: Clone,
Renderer: core::text::Renderer,
Renderer::Theme: text_input::StyleSheet,
{
TextInput::new(placeholder, value, on_change)
}
/// Creates a new [`Slider`].
///
/// [`Slider`]: widget::Slider
pub fn slider<'a, T, Message, Renderer>(
range: std::ops::RangeInclusive<T>,
value: T,
on_change: impl Fn(T) -> Message + 'a,
) -> Slider<'a, T, Message, Renderer>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
Renderer: core::Renderer,
Renderer::Theme: slider::StyleSheet,
{
Slider::new(range, value, on_change)
}
/// Creates a new [`VerticalSlider`].
///
/// [`VerticalSlider`]: widget::VerticalSlider
pub fn vertical_slider<'a, T, Message, Renderer>(
range: std::ops::RangeInclusive<T>,
value: T,
on_change: impl Fn(T) -> Message + 'a,
) -> VerticalSlider<'a, T, Message, Renderer>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
Renderer: core::Renderer,
Renderer::Theme: slider::StyleSheet,
{
VerticalSlider::new(range, value, on_change)
}
/// Creates a new [`PickList`].
///
/// [`PickList`]: widget::PickList
pub fn pick_list<'a, Message, Renderer, T>(
options: impl Into<Cow<'a, [T]>>,
selected: Option<T>,
on_selected: impl Fn(T) -> Message + 'a,
) -> PickList<'a, T, Message, Renderer>
where
T: ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: core::text::Renderer,
Renderer::Theme: pick_list::StyleSheet
+ scrollable::StyleSheet
+ overlay::menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as overlay::menu::StyleSheet>::Style:
From<<Renderer::Theme as pick_list::StyleSheet>::Style>,
{
PickList::new(options, selected, on_selected)
}
/// Creates a new horizontal [`Space`] with the given [`Length`].
///
/// [`Space`]: widget::Space
pub fn horizontal_space(width: impl Into<Length>) -> Space {
Space::with_width(width)
}
/// Creates a new vertical [`Space`] with the given [`Length`].
///
/// [`Space`]: widget::Space
pub fn vertical_space(height: impl Into<Length>) -> Space {
Space::with_height(height)
}
/// Creates a horizontal [`Rule`] with the given height.
///
/// [`Rule`]: widget::Rule
pub fn horizontal_rule<Renderer>(height: impl Into<Pixels>) -> Rule<Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: rule::StyleSheet,
{
Rule::horizontal(height)
}
/// Creates a vertical [`Rule`] with the given width.
///
/// [`Rule`]: widget::Rule
pub fn vertical_rule<Renderer>(width: impl Into<Pixels>) -> Rule<Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: rule::StyleSheet,
{
Rule::vertical(width)
}
/// Creates a new [`ProgressBar`].
///
/// It expects:
/// * an inclusive range of possible values, and
/// * the current value of the [`ProgressBar`].
///
/// [`ProgressBar`]: widget::ProgressBar
pub fn progress_bar<Renderer>(
range: RangeInclusive<f32>,
value: f32,
) -> ProgressBar<Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: progress_bar::StyleSheet,
{
ProgressBar::new(range, value)
}
/// Creates a new [`Image`].
///
/// [`Image`]: widget::Image
#[cfg(feature = "image")]
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
crate::Image::new(handle.into())
}
/// Creates a new [`Svg`] widget from the given [`Handle`].
///
/// [`Svg`]: widget::Svg
/// [`Handle`]: widget::svg::Handle
#[cfg(feature = "svg")]
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
pub fn svg<Renderer>(
handle: impl Into<core::svg::Handle>,
) -> crate::Svg<Renderer>
where
Renderer: core::svg::Renderer,
Renderer::Theme: crate::svg::StyleSheet,
{
crate::Svg::new(handle)
}
/// Creates a new [`Canvas`].
#[cfg(feature = "canvas")]
#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
pub fn canvas<P, Message, Renderer>(
program: P,
) -> crate::Canvas<P, Message, Renderer>
where
Renderer: crate::graphics::geometry::Renderer,
P: crate::canvas::Program<Message, Renderer>,
{
crate::Canvas::new(program)
}
/// Focuses the previous focusable widget.
pub fn focus_previous<Message>() -> Command<Message>
where
Message: 'static,
{
Command::widget(operation::focusable::focus_previous())
}
/// Focuses the next focusable widget.
pub fn focus_next<Message>() -> Command<Message>
where
Message: 'static,
{
Command::widget(operation::focusable::focus_next())
}

205
widget/src/image.rs Normal file
View file

@ -0,0 +1,205 @@
//! Display images in your user interface.
pub mod viewer;
pub use viewer::Viewer;
use crate::core::image;
use crate::core::layout;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
};
use std::hash::Hash;
pub use image::Handle;
/// Creates a new [`Viewer`] with the given image `Handle`.
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
Viewer::new(handle)
}
/// A frame that displays an image while keeping aspect ratio.
///
/// # Example
///
/// ```
/// # use iced_widget::image::{self, Image};
/// #
/// let image = Image::<image::Handle>::new("resources/ferris.png");
/// ```
///
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
#[derive(Debug)]
pub struct Image<Handle> {
handle: Handle,
width: Length,
height: Length,
content_fit: ContentFit,
}
impl<Handle> Image<Handle> {
/// Creates a new [`Image`] with the given path.
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Image {
handle: handle.into(),
width: Length::Shrink,
height: Length::Shrink,
content_fit: ContentFit::Contain,
}
}
/// Sets the width of the [`Image`] boundaries.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Image`] boundaries.
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the [`ContentFit`] of the [`Image`].
///
/// Defaults to [`ContentFit::Contain`]
pub fn content_fit(self, content_fit: ContentFit) -> Self {
Self {
content_fit,
..self
}
}
}
/// Computes the layout of an [`Image`].
pub fn layout<Renderer, Handle>(
renderer: &Renderer,
limits: &layout::Limits,
handle: &Handle,
width: Length,
height: Length,
content_fit: ContentFit,
) -> layout::Node
where
Renderer: image::Renderer<Handle = Handle>,
{
// The raw w/h of the underlying image
let image_size = {
let Size { width, height } = renderer.dimensions(handle);
Size::new(width as f32, height as f32)
};
// The size to be available to the widget prior to `Shrink`ing
let raw_size = limits.width(width).height(height).resolve(image_size);
// The uncropped size of the image when fit to the bounds above
let full_size = content_fit.fit(image_size, raw_size);
// Shrink the widget to fit the resized image, if requested
let final_size = Size {
width: match width {
Length::Shrink => f32::min(raw_size.width, full_size.width),
_ => raw_size.width,
},
height: match height {
Length::Shrink => f32::min(raw_size.height, full_size.height),
_ => raw_size.height,
},
};
layout::Node::new(final_size)
}
/// Draws an [`Image`]
pub fn draw<Renderer, Handle>(
renderer: &mut Renderer,
layout: Layout<'_>,
handle: &Handle,
content_fit: ContentFit,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
{
let Size { width, height } = renderer.dimensions(handle);
let image_size = Size::new(width as f32, height as f32);
let bounds = layout.bounds();
let adjusted_fit = content_fit.fit(image_size, bounds.size());
let render = |renderer: &mut Renderer| {
let offset = Vector::new(
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
);
let drawing_bounds = Rectangle {
width: adjusted_fit.width,
height: adjusted_fit.height,
..bounds
};
renderer.draw(handle.clone(), drawing_bounds + offset)
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
{
renderer.with_layer(bounds, render);
} else {
render(renderer)
}
}
impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
&self.handle,
self.width,
self.height,
self.content_fit,
)
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
draw(renderer, layout, &self.handle, self.content_fit)
}
}
impl<'a, Message, Renderer, Handle> From<Image<Handle>>
for Element<'a, Message, Renderer>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash + 'a,
{
fn from(image: Image<Handle>) -> Element<'a, Message, Renderer> {
Element::new(image)
}
}

428
widget/src/image/viewer.rs Normal file
View file

@ -0,0 +1,428 @@
//! Zoom and pan on an image.
use crate::core::event::{self, Event};
use crate::core::image;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
Vector, Widget,
};
use std::hash::Hash;
/// A frame that displays an image with the ability to zoom in/out and pan.
#[allow(missing_debug_implementations)]
pub struct Viewer<Handle> {
padding: f32,
width: Length,
height: Length,
min_scale: f32,
max_scale: f32,
scale_step: f32,
handle: Handle,
}
impl<Handle> Viewer<Handle> {
/// Creates a new [`Viewer`] with the given [`State`].
pub fn new(handle: Handle) -> Self {
Viewer {
padding: 0.0,
width: Length::Shrink,
height: Length::Shrink,
min_scale: 0.25,
max_scale: 10.0,
scale_step: 0.10,
handle,
}
}
/// Sets the padding of the [`Viewer`].
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
self.padding = padding.into().0;
self
}
/// Sets the width of the [`Viewer`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Viewer`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the max scale applied to the image of the [`Viewer`].
///
/// Default is `10.0`
pub fn max_scale(mut self, max_scale: f32) -> Self {
self.max_scale = max_scale;
self
}
/// Sets the min scale applied to the image of the [`Viewer`].
///
/// Default is `0.25`
pub fn min_scale(mut self, min_scale: f32) -> Self {
self.min_scale = min_scale;
self
}
/// Sets the percentage the image of the [`Viewer`] will be scaled by
/// when zoomed in / out.
///
/// Default is `0.10`
pub fn scale_step(mut self, scale_step: f32) -> Self {
self.scale_step = scale_step;
self
}
}
impl<Message, Renderer, Handle> Widget<Message, Renderer> for Viewer<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let Size { width, height } = renderer.dimensions(&self.handle);
let mut size = limits
.width(self.width)
.height(self.height)
.resolve(Size::new(width as f32, height as f32));
let expansion_size = if height > width {
self.width
} else {
self.height
};
// Only calculate viewport sizes if the images are constrained to a limited space.
// If they are Fill|Portion let them expand within their alotted space.
match expansion_size {
Length::Shrink | Length::Fixed(_) => {
let aspect_ratio = width as f32 / height as f32;
let viewport_aspect_ratio = size.width / size.height;
if viewport_aspect_ratio > aspect_ratio {
size.width = width as f32 * size.height / height as f32;
} else {
size.height = height as f32 * size.width / width as f32;
}
}
Length::Fill | Length::FillPortion(_) => {}
}
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
) -> event::Status {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta })
if is_mouse_over =>
{
match delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
let state = tree.state.downcast_mut::<State>();
let previous_scale = state.scale;
if y < 0.0 && previous_scale > self.min_scale
|| y > 0.0 && previous_scale < self.max_scale
{
state.scale = (if y > 0.0 {
state.scale * (1.0 + self.scale_step)
} else {
state.scale / (1.0 + self.scale_step)
})
.clamp(self.min_scale, self.max_scale);
let image_size = image_size(
renderer,
&self.handle,
state,
bounds.size(),
);
let factor = state.scale / previous_scale - 1.0;
let cursor_to_center =
cursor_position - bounds.center();
let adjustment = cursor_to_center * factor
+ state.current_offset * factor;
state.current_offset = Vector::new(
if image_size.width > bounds.width {
state.current_offset.x + adjustment.x
} else {
0.0
},
if image_size.height > bounds.height {
state.current_offset.y + adjustment.y
} else {
0.0
},
);
}
}
}
event::Status::Captured
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
if is_mouse_over =>
{
let state = tree.state.downcast_mut::<State>();
state.cursor_grabbed_at = Some(cursor_position);
state.starting_offset = state.current_offset;
event::Status::Captured
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree.state.downcast_mut::<State>();
if state.cursor_grabbed_at.is_some() {
state.cursor_grabbed_at = None;
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Mouse(mouse::Event::CursorMoved { position }) => {
let state = tree.state.downcast_mut::<State>();
if let Some(origin) = state.cursor_grabbed_at {
let image_size = image_size(
renderer,
&self.handle,
state,
bounds.size(),
);
let hidden_width = (image_size.width - bounds.width / 2.0)
.max(0.0)
.round();
let hidden_height = (image_size.height
- bounds.height / 2.0)
.max(0.0)
.round();
let delta = position - origin;
let x = if bounds.width < image_size.width {
(state.starting_offset.x - delta.x)
.clamp(-hidden_width, hidden_width)
} else {
0.0
};
let y = if bounds.height < image_size.height {
(state.starting_offset.y - delta.y)
.clamp(-hidden_height, hidden_height)
} else {
0.0
};
state.current_offset = Vector::new(x, y);
event::Status::Captured
} else {
event::Status::Ignored
}
}
_ => event::Status::Ignored,
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
if state.is_cursor_grabbed() {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::Idle
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let image_size =
image_size(renderer, &self.handle, state, bounds.size());
let translation = {
let image_top_left = Vector::new(
bounds.width / 2.0 - image_size.width / 2.0,
bounds.height / 2.0 - image_size.height / 2.0,
);
image_top_left - state.offset(bounds, image_size)
};
renderer.with_layer(bounds, |renderer| {
renderer.with_translation(translation, |renderer| {
image::Renderer::draw(
renderer,
self.handle.clone(),
Rectangle {
x: bounds.x,
y: bounds.y,
..Rectangle::with_size(image_size)
},
)
});
});
}
}
/// The local state of a [`Viewer`].
#[derive(Debug, Clone, Copy)]
pub struct State {
scale: f32,
starting_offset: Vector,
current_offset: Vector,
cursor_grabbed_at: Option<Point>,
}
impl Default for State {
fn default() -> Self {
Self {
scale: 1.0,
starting_offset: Vector::default(),
current_offset: Vector::default(),
cursor_grabbed_at: None,
}
}
}
impl State {
/// Creates a new [`State`].
pub fn new() -> Self {
State::default()
}
/// Returns the current offset of the [`State`], given the bounds
/// of the [`Viewer`] and its image.
fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
let hidden_width =
(image_size.width - bounds.width / 2.0).max(0.0).round();
let hidden_height =
(image_size.height - bounds.height / 2.0).max(0.0).round();
Vector::new(
self.current_offset.x.clamp(-hidden_width, hidden_width),
self.current_offset.y.clamp(-hidden_height, hidden_height),
)
}
/// Returns if the cursor is currently grabbed by the [`Viewer`].
pub fn is_cursor_grabbed(&self) -> bool {
self.cursor_grabbed_at.is_some()
}
}
impl<'a, Message, Renderer, Handle> From<Viewer<Handle>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + image::Renderer<Handle = Handle>,
Message: 'a,
Handle: Clone + Hash + 'a,
{
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Renderer> {
Element::new(viewer)
}
}
/// Returns the bounds of the underlying image, given the bounds of
/// the [`Viewer`]. Scaling will be applied and original aspect ratio
/// will be respected.
pub fn image_size<Renderer>(
renderer: &Renderer,
handle: &<Renderer as image::Renderer>::Handle,
state: &State,
bounds: Size,
) -> Size
where
Renderer: image::Renderer,
{
let Size { width, height } = renderer.dimensions(handle);
let (width, height) = {
let dimensions = (width as f32, height as f32);
let width_ratio = bounds.width / dimensions.0;
let height_ratio = bounds.height / dimensions.1;
let ratio = width_ratio.min(height_ratio);
let scale = state.scale;
if ratio < 1.0 {
(dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
} else {
(dimensions.0 * scale, dimensions.1 * scale)
}
};
Size::new(width, height)
}

409
widget/src/lazy.rs Normal file
View file

@ -0,0 +1,409 @@
#![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)]
pub(crate) mod helpers;
pub mod component;
pub mod responsive;
pub use component::Component;
pub use responsive::Responsive;
mod cache;
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::Element;
use crate::core::{
self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size,
};
use ouroboros::self_referencing;
use std::cell::RefCell;
use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Renderer, Dependency, View> {
dependency: Dependency,
view: Box<dyn Fn(&Dependency) -> View + 'a>,
element: RefCell<
Option<Rc<RefCell<Option<Element<'static, Message, Renderer>>>>>,
>,
}
impl<'a, Message, Renderer, Dependency, View>
Lazy<'a, Message, Renderer, Dependency, View>
where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Renderer>>,
{
pub fn new(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
) -> Self {
Self {
dependency,
view: Box::new(view),
element: RefCell::new(None),
}
}
fn with_element<T>(
&self,
f: impl FnOnce(&Element<'_, Message, Renderer>) -> T,
) -> T {
f(self
.element
.borrow()
.as_ref()
.unwrap()
.borrow()
.as_ref()
.unwrap())
}
fn with_element_mut<T>(
&self,
f: impl FnOnce(&mut Element<'_, Message, Renderer>) -> T,
) -> T {
f(self
.element
.borrow()
.as_ref()
.unwrap()
.borrow_mut()
.as_mut()
.unwrap())
}
}
struct Internal<Message, Renderer> {
element: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>,
hash: u64,
}
impl<'a, Message, Renderer, Dependency, View> Widget<Message, Renderer>
for Lazy<'a, Message, Renderer, Dependency, View>
where
View: Into<Element<'static, Message, Renderer>> + 'static,
Dependency: Hash + 'a,
Message: 'static,
Renderer: core::Renderer + 'static,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
tree::Tag::of::<Tag<View>>()
}
fn state(&self) -> tree::State {
let mut hasher = Hasher::default();
self.dependency.hash(&mut hasher);
let hash = hasher.finish();
let element =
Rc::new(RefCell::new(Some((self.view)(&self.dependency).into())));
(*self.element.borrow_mut()) = Some(element.clone());
tree::State::new(Internal { element, hash })
}
fn children(&self) -> Vec<Tree> {
self.with_element(|element| vec![Tree::new(element.as_widget())])
}
fn diff(&self, tree: &mut Tree) {
let current = tree.state.downcast_mut::<Internal<Message, Renderer>>();
let mut hasher = Hasher::default();
self.dependency.hash(&mut hasher);
let new_hash = hasher.finish();
if current.hash != new_hash {
current.hash = new_hash;
let element = (self.view)(&self.dependency).into();
current.element = Rc::new(RefCell::new(Some(element)));
(*self.element.borrow_mut()) = Some(current.element.clone());
self.with_element(|element| {
tree.diff_children(std::slice::from_ref(&element.as_widget()))
});
} else {
(*self.element.borrow_mut()) = Some(current.element.clone());
}
}
fn width(&self) -> Length {
self.with_element(|element| element.as_widget().width())
}
fn height(&self) -> Length {
self.with_element(|element| element.as_widget().height())
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
element.as_widget().layout(renderer, limits)
})
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
self.with_element(|element| {
element.as_widget().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_element_mut(|element| {
element.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
})
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_element(|element| {
element.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor_position,
viewport,
renderer,
)
})
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
self.with_element(|element| {
element.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor_position,
viewport,
)
})
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'_, Message, Renderer>> {
let overlay = Overlay(Some(
InnerBuilder {
cell: self.element.borrow().as_ref().unwrap().clone(),
element: self
.element
.borrow()
.as_ref()
.unwrap()
.borrow_mut()
.take()
.unwrap(),
tree: &mut tree.children[0],
overlay_builder: |element, tree| {
element.as_widget_mut().overlay(tree, layout, renderer)
},
}
.build(),
));
let has_overlay = overlay
.with_overlay_maybe(|overlay| overlay::Element::position(overlay));
has_overlay
.map(|position| overlay::Element::new(position, Box::new(overlay)))
}
}
#[self_referencing]
struct Inner<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a,
{
cell: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>,
element: Element<'static, Message, Renderer>,
tree: &'a mut Tree,
#[borrows(mut element, mut tree)]
#[covariant]
overlay: Option<overlay::Element<'this, Message, Renderer>>,
}
struct Overlay<'a, Message, Renderer>(Option<Inner<'a, Message, Renderer>>);
impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> {
fn drop(&mut self) {
let heads = self.0.take().unwrap().into_heads();
(*heads.cell.borrow_mut()) = Some(heads.element);
}
}
impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> {
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
) -> Option<T> {
self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f)
}
fn with_overlay_mut_maybe<T>(
&mut self,
f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
) -> Option<T> {
self.0
.as_mut()
.unwrap()
.with_overlay_mut(|overlay| overlay.as_mut().map(f))
}
}
impl<'a, Message, Renderer> overlay::Overlay<Message, Renderer>
for Overlay<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
fn layout(
&self,
renderer: &Renderer,
bounds: Size,
position: Point,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
let translation = position - overlay.position();
overlay.layout(renderer, bounds, translation)
})
.unwrap_or_default()
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
) {
let _ = self.with_overlay_maybe(|overlay| {
overlay.draw(renderer, theme, style, layout, cursor_position);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
overlay.mouse_interaction(
layout,
cursor_position,
viewport,
renderer,
)
})
.unwrap_or_default()
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_overlay_mut_maybe(|overlay| {
overlay.on_event(
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
})
.unwrap_or(event::Status::Ignored)
}
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
self.with_overlay_maybe(|overlay| {
overlay.is_over(layout, cursor_position)
})
.unwrap_or_default()
}
}
impl<'a, Message, Renderer, Dependency, View>
From<Lazy<'a, Message, Renderer, Dependency, View>>
for Element<'a, Message, Renderer>
where
View: Into<Element<'static, Message, Renderer>> + 'static,
Renderer: core::Renderer + 'static,
Message: 'static,
Dependency: Hash + 'a,
{
fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self {
Self::new(lazy)
}
}

13
widget/src/lazy/cache.rs Normal file
View file

@ -0,0 +1,13 @@
use crate::core::overlay;
use crate::core::Element;
use ouroboros::self_referencing;
#[self_referencing(pub_extras)]
pub struct Cache<'a, Message: 'a, Renderer: 'a> {
pub element: Element<'a, Message, Renderer>,
#[borrows(mut element)]
#[covariant]
overlay: Option<overlay::Element<'this, Message, Renderer>>,
}

View file

@ -0,0 +1,575 @@
//! Build and reuse custom widgets using The Elm Architecture.
use crate::core::event;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
};
use ouroboros::self_referencing;
use std::cell::RefCell;
use std::marker::PhantomData;
/// A reusable, custom widget that uses The Elm Architecture.
///
/// A [`Component`] allows you to implement custom widgets as if they were
/// `iced` applications with encapsulated state.
///
/// In other words, a [`Component`] allows you to turn `iced` applications into
/// custom widgets and embed them without cumbersome wiring.
///
/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event)
/// and update the internal state of the [`Component`].
///
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
/// the parent application of any relevant interactions.
pub trait Component<Message, Renderer> {
/// The internal state of this [`Component`].
type State: Default;
/// The type of event this [`Component`] handles internally.
type Event;
/// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly.
///
/// It can produce a `Message` for the parent application.
fn update(
&mut self,
state: &mut Self::State,
event: Self::Event,
) -> Option<Message>;
/// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event)
/// on user interaction.
fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>;
/// Update the [`Component`] state based on the provided [`Operation`](widget::Operation)
///
/// By default, it does nothing.
fn operate(
&self,
_state: &mut Self::State,
_operation: &mut dyn widget::Operation<Message>,
) {
}
}
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
pub fn view<'a, C, Message, Renderer>(
component: C,
) -> Element<'a, Message, Renderer>
where
C: Component<Message, Renderer> + 'a,
C::State: 'static,
Message: 'a,
Renderer: core::Renderer + 'a,
{
Element::new(Instance {
state: RefCell::new(Some(
StateBuilder {
component: Box::new(component),
message: PhantomData,
state: PhantomData,
element_builder: |_| None,
}
.build(),
)),
})
}
struct Instance<'a, Message, Renderer, Event, S> {
state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>,
}
#[self_referencing]
struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> {
component:
Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>,
message: PhantomData<Message>,
state: PhantomData<S>,
#[borrows(component)]
#[covariant]
element: Option<Element<'this, Event, Renderer>>,
}
impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S>
where
S: Default,
{
fn rebuild_element(&self, state: &S) {
let heads = self.state.borrow_mut().take().unwrap().into_heads();
*self.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |component| Some(component.view(state)),
}
.build(),
);
}
fn rebuild_element_with_operation(
&self,
state: &mut S,
operation: &mut dyn widget::Operation<Message>,
) {
let heads = self.state.borrow_mut().take().unwrap().into_heads();
heads.component.operate(state, operation);
*self.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |component| Some(component.view(state)),
}
.build(),
);
}
fn with_element<T>(
&self,
f: impl FnOnce(&Element<'_, Event, Renderer>) -> T,
) -> T {
self.with_element_mut(|element| f(element))
}
fn with_element_mut<T>(
&self,
f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T,
) -> T {
self.state
.borrow_mut()
.as_mut()
.unwrap()
.with_element_mut(|element| f(element.as_mut().unwrap()))
}
}
impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer>
for Instance<'a, Message, Renderer, Event, S>
where
S: 'static + Default,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
tree::Tag::of::<Tag<S>>()
}
fn state(&self) -> tree::State {
tree::State::new(S::default())
}
fn children(&self) -> Vec<Tree> {
self.rebuild_element(&S::default());
self.with_element(|element| vec![Tree::new(element)])
}
fn diff(&self, tree: &mut Tree) {
self.rebuild_element(tree.state.downcast_ref());
self.with_element(|element| {
tree.diff_children(std::slice::from_ref(&element))
})
}
fn width(&self) -> Length {
self.with_element(|element| element.as_widget().width())
}
fn height(&self) -> Length {
self.with_element(|element| element.as_widget().height())
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
element.as_widget().layout(renderer, limits)
})
}
fn on_event(
&mut self,
tree: &mut Tree,
event: core::Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
let event_status = self.with_element_mut(|element| {
element.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout,
cursor_position,
renderer,
clipboard,
&mut local_shell,
)
});
local_shell.revalidate_layout(|| shell.invalidate_layout());
if let Some(redraw_request) = local_shell.redraw_request() {
shell.request_redraw(redraw_request);
}
if !local_messages.is_empty() {
let mut heads = self.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| {
heads
.component
.update(tree.state.downcast_mut::<S>(), message)
}) {
shell.publish(message);
}
self.state = RefCell::new(Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |state| {
Some(state.view(tree.state.downcast_ref::<S>()))
},
}
.build(),
));
self.with_element(|element| {
tree.diff_children(std::slice::from_ref(&element))
});
shell.invalidate_layout();
}
event_status
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
self.rebuild_element_with_operation(
tree.state.downcast_mut(),
operation,
);
struct MapOperation<'a, B> {
operation: &'a mut dyn widget::Operation<B>,
}
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
fn container(
&mut self,
id: Option<&widget::Id>,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>,
),
) {
self.operation.container(id, &mut |operation| {
operate_on_children(&mut MapOperation { operation });
});
}
fn focusable(
&mut self,
state: &mut dyn widget::operation::Focusable,
id: Option<&widget::Id>,
) {
self.operation.focusable(state, id);
}
fn text_input(
&mut self,
state: &mut dyn widget::operation::TextInput,
id: Option<&widget::Id>,
) {
self.operation.text_input(state, id);
}
}
self.with_element(|element| {
tree.diff_children(std::slice::from_ref(&element));
element.as_widget().operate(
&mut tree.children[0],
layout,
renderer,
&mut MapOperation { operation },
);
});
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
self.with_element(|element| {
element.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor_position,
viewport,
);
});
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_element(|element| {
element.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor_position,
viewport,
renderer,
)
})
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let overlay = OverlayBuilder {
instance: self,
tree,
types: PhantomData,
overlay_builder: |instance, tree| {
instance.state.get_mut().as_mut().unwrap().with_element_mut(
move |element| {
element.as_mut().unwrap().as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
)
},
)
},
}
.build();
let has_overlay = overlay.with_overlay(|overlay| {
overlay.as_ref().map(overlay::Element::position)
});
has_overlay.map(|position| {
overlay::Element::new(
position,
Box::new(OverlayInstance {
overlay: Some(overlay),
}),
)
})
}
}
#[self_referencing]
struct Overlay<'a, 'b, Message, Renderer, Event, S> {
instance: &'a mut Instance<'b, Message, Renderer, Event, S>,
tree: &'a mut Tree,
types: PhantomData<(Message, Event, S)>,
#[borrows(mut instance, mut tree)]
#[covariant]
overlay: Option<overlay::Element<'this, Event, Renderer>>,
}
struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> {
overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>,
}
impl<'a, 'b, Message, Renderer, Event, S>
OverlayInstance<'a, 'b, Message, Renderer, Event, S>
{
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T,
) -> Option<T> {
self.overlay
.as_ref()
.unwrap()
.borrow_overlay()
.as_ref()
.map(f)
}
fn with_overlay_mut_maybe<T>(
&mut self,
f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T,
) -> Option<T> {
self.overlay
.as_mut()
.unwrap()
.with_overlay_mut(|overlay| overlay.as_mut().map(f))
}
}
impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer>
for OverlayInstance<'a, 'b, Message, Renderer, Event, S>
where
Renderer: core::Renderer,
S: 'static + Default,
{
fn layout(
&self,
renderer: &Renderer,
bounds: Size,
position: Point,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
let translation = position - overlay.position();
overlay.layout(renderer, bounds, translation)
})
.unwrap_or_default()
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
) {
let _ = self.with_overlay_maybe(|overlay| {
overlay.draw(renderer, theme, style, layout, cursor_position);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
overlay.mouse_interaction(
layout,
cursor_position,
viewport,
renderer,
)
})
.unwrap_or_default()
}
fn on_event(
&mut self,
event: core::Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
let event_status = self
.with_overlay_mut_maybe(|overlay| {
overlay.on_event(
event,
layout,
cursor_position,
renderer,
clipboard,
&mut local_shell,
)
})
.unwrap_or(event::Status::Ignored);
local_shell.revalidate_layout(|| shell.invalidate_layout());
if !local_messages.is_empty() {
let overlay = self.overlay.take().unwrap().into_heads();
let mut heads = overlay.instance.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| {
heads
.component
.update(overlay.tree.state.downcast_mut::<S>(), message)
}) {
shell.publish(message);
}
*overlay.instance.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |state| {
Some(state.view(overlay.tree.state.downcast_ref::<S>()))
},
}
.build(),
);
overlay.instance.with_element(|element| {
overlay.tree.diff_children(std::slice::from_ref(&element))
});
self.overlay = Some(
OverlayBuilder {
instance: overlay.instance,
tree: overlay.tree,
types: PhantomData,
overlay_builder: |_, _| None,
}
.build(),
);
shell.invalidate_layout();
}
event_status
}
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
self.with_overlay_maybe(|overlay| {
overlay.is_over(layout, cursor_position)
})
.unwrap_or_default()
}
}

View file

@ -0,0 +1,39 @@
use crate::core::{self, Element, Size};
use crate::lazy::component::{self, Component};
use crate::lazy::{Lazy, Responsive};
use std::hash::Hash;
pub fn lazy<'a, Message, Renderer, Dependency, View>(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
) -> Lazy<'a, Message, Renderer, Dependency, View>
where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Renderer>>,
{
Lazy::new(dependency, view)
}
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
pub fn component<'a, C, Message, Renderer>(
component: C,
) -> Element<'a, Message, Renderer>
where
C: Component<Message, Renderer> + 'a,
C::State: 'static,
Message: 'a,
Renderer: core::Renderer + 'a,
{
component::view(component)
}
pub fn responsive<'a, Message, Renderer>(
f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
) -> Responsive<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
Responsive::new(f)
}

View file

@ -0,0 +1,427 @@
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
};
use crate::horizontal_space;
use ouroboros::self_referencing;
use std::cell::{RefCell, RefMut};
use std::marker::PhantomData;
use std::ops::Deref;
/// A widget that is aware of its dimensions.
///
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
#[allow(missing_debug_implementations)]
pub struct Responsive<'a, Message, Renderer = crate::Renderer> {
view: Box<dyn Fn(Size) -> Element<'a, Message, Renderer> + 'a>,
content: RefCell<Content<'a, Message, Renderer>>,
}
impl<'a, Message, Renderer> Responsive<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
/// Creates a new [`Responsive`] widget with a closure that produces its
/// contents.
///
/// The `view` closure will be provided with the current [`Size`] of
/// the [`Responsive`] widget and, therefore, can be used to build the
/// contents of the widget in a responsive way.
pub fn new(
view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
) -> Self {
Self {
view: Box::new(view),
content: RefCell::new(Content {
size: Size::ZERO,
layout: layout::Node::new(Size::ZERO),
element: Element::new(horizontal_space(0)),
}),
}
}
}
struct Content<'a, Message, Renderer> {
size: Size,
layout: layout::Node,
element: Element<'a, Message, Renderer>,
}
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
fn update(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
new_size: Size,
view: &dyn Fn(Size) -> Element<'a, Message, Renderer>,
) {
if self.size == new_size {
return;
}
self.element = view(new_size);
self.size = new_size;
tree.diff(&self.element);
self.layout = self
.element
.as_widget()
.layout(renderer, &layout::Limits::new(Size::ZERO, self.size));
}
fn resolve<R, T>(
&mut self,
tree: &mut Tree,
renderer: R,
layout: Layout<'_>,
view: &dyn Fn(Size) -> Element<'a, Message, Renderer>,
f: impl FnOnce(
&mut Tree,
R,
Layout<'_>,
&mut Element<'a, Message, Renderer>,
) -> T,
) -> T
where
R: Deref<Target = Renderer>,
{
self.update(tree, renderer.deref(), layout.bounds().size(), view);
let content_layout = Layout::with_offset(
layout.position() - Point::ORIGIN,
&self.layout,
);
f(tree, renderer, content_layout, &mut self.element)
}
}
struct State {
tree: RefCell<Tree>,
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Responsive<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
tree: RefCell::new(Tree::empty()),
})
}
fn width(&self) -> Length {
Length::Fill
}
fn height(&self) -> Length {
Length::Fill
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::Node::new(limits.max())
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
content.resolve(
&mut state.tree.borrow_mut(),
renderer,
layout,
&self.view,
|tree, renderer, layout, element| {
element
.as_widget()
.operate(tree, layout, renderer, operation);
},
);
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
content.resolve(
&mut state.tree.borrow_mut(),
renderer,
layout,
&self.view,
|tree, renderer, layout, element| {
element.as_widget_mut().on_event(
tree,
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
},
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let mut content = self.content.borrow_mut();
content.resolve(
&mut state.tree.borrow_mut(),
renderer,
layout,
&self.view,
|tree, renderer, layout, element| {
element.as_widget().draw(
tree,
renderer,
theme,
style,
layout,
cursor_position,
viewport,
)
},
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
let mut content = self.content.borrow_mut();
content.resolve(
&mut state.tree.borrow_mut(),
renderer,
layout,
&self.view,
|tree, renderer, layout, element| {
element.as_widget().mouse_interaction(
tree,
layout,
cursor_position,
viewport,
renderer,
)
},
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
use std::ops::DerefMut;
let state = tree.state.downcast_ref::<State>();
let overlay = OverlayBuilder {
content: self.content.borrow_mut(),
tree: state.tree.borrow_mut(),
types: PhantomData,
overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>,
tree| {
content.update(
tree,
renderer,
layout.bounds().size(),
&self.view,
);
let Content {
element,
layout: content_layout,
..
} = content.deref_mut();
let content_layout = Layout::with_offset(
layout.bounds().position() - Point::ORIGIN,
content_layout,
);
element
.as_widget_mut()
.overlay(tree, content_layout, renderer)
},
}
.build();
let has_overlay = overlay.with_overlay(|overlay| {
overlay.as_ref().map(overlay::Element::position)
});
has_overlay
.map(|position| overlay::Element::new(position, Box::new(overlay)))
}
}
impl<'a, Message, Renderer> From<Responsive<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: core::Renderer + 'a,
Message: 'a,
{
fn from(responsive: Responsive<'a, Message, Renderer>) -> Self {
Self::new(responsive)
}
}
#[self_referencing]
struct Overlay<'a, 'b, Message, Renderer> {
content: RefMut<'a, Content<'b, Message, Renderer>>,
tree: RefMut<'a, Tree>,
types: PhantomData<Message>,
#[borrows(mut content, mut tree)]
#[covariant]
overlay: Option<overlay::Element<'this, Message, Renderer>>,
}
impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> {
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
) -> Option<T> {
self.borrow_overlay().as_ref().map(f)
}
fn with_overlay_mut_maybe<T>(
&mut self,
f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
) -> Option<T> {
self.with_overlay_mut(|overlay| overlay.as_mut().map(f))
}
}
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
for Overlay<'a, 'b, Message, Renderer>
where
Renderer: core::Renderer,
{
fn layout(
&self,
renderer: &Renderer,
bounds: Size,
position: Point,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
let translation = position - overlay.position();
overlay.layout(renderer, bounds, translation)
})
.unwrap_or_default()
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
) {
let _ = self.with_overlay_maybe(|overlay| {
overlay.draw(renderer, theme, style, layout, cursor_position);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
overlay.mouse_interaction(
layout,
cursor_position,
viewport,
renderer,
)
})
.unwrap_or_default()
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_overlay_mut_maybe(|overlay| {
overlay.on_event(
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
})
.unwrap_or(event::Status::Ignored)
}
fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
self.with_overlay_maybe(|overlay| {
overlay.is_over(layout, cursor_position)
})
.unwrap_or_default()
}
}

122
widget/src/lib.rs Normal file
View file

@ -0,0 +1,122 @@
//! Use the built-in widgets or create your own.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![deny(
missing_debug_implementations,
//missing_docs,
unused_results,
clippy::extra_unused_lifetimes,
clippy::from_over_into,
clippy::needless_borrow,
clippy::new_without_default,
clippy::useless_conversion
)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
pub use iced_native as native;
pub use iced_native::core;
pub use iced_renderer as renderer;
pub use iced_renderer::graphics;
pub use iced_style as style;
mod column;
mod row;
pub mod button;
pub mod checkbox;
pub mod container;
pub mod overlay;
pub mod pane_grid;
pub mod pick_list;
pub mod progress_bar;
pub mod radio;
pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod space;
pub mod text;
pub mod text_input;
pub mod toggler;
pub mod tooltip;
pub mod vertical_slider;
mod helpers;
pub use helpers::*;
#[cfg(feature = "lazy")]
mod lazy;
#[cfg(feature = "lazy")]
pub use crate::lazy::{Component, Lazy, Responsive};
#[cfg(feature = "lazy")]
pub use crate::lazy::helpers::*;
#[doc(no_inline)]
pub use button::Button;
#[doc(no_inline)]
pub use checkbox::Checkbox;
#[doc(no_inline)]
pub use column::Column;
#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
pub use pane_grid::PaneGrid;
#[doc(no_inline)]
pub use pick_list::PickList;
#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
#[doc(no_inline)]
pub use row::Row;
#[doc(no_inline)]
pub use rule::Rule;
#[doc(no_inline)]
pub use scrollable::Scrollable;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
pub use space::Space;
#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
pub use text_input::TextInput;
#[doc(no_inline)]
pub use toggler::Toggler;
#[doc(no_inline)]
pub use tooltip::Tooltip;
#[doc(no_inline)]
pub use vertical_slider::VerticalSlider;
#[cfg(feature = "svg")]
pub mod svg;
#[cfg(feature = "svg")]
#[doc(no_inline)]
pub use svg::Svg;
#[cfg(feature = "image")]
pub mod image;
#[cfg(feature = "image")]
#[doc(no_inline)]
pub use image::Image;
#[cfg(feature = "canvas")]
pub mod canvas;
#[cfg(feature = "canvas")]
#[doc(no_inline)]
pub use canvas::Canvas;
#[cfg(feature = "qr_code")]
pub mod qr_code;
#[cfg(feature = "qr_code")]
#[doc(no_inline)]
pub use qr_code::QRCode;
type Renderer<Theme = style::Theme> = renderer::Renderer<Theme>;

1
widget/src/overlay.rs Normal file
View file

@ -0,0 +1 @@
pub mod menu;

519
widget/src/overlay/menu.rs Normal file
View file

@ -0,0 +1,519 @@
//! Build and show dropdown menus.
use crate::container::{self, Container};
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::Tree;
use crate::core::{
Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, Size, Vector,
};
use crate::core::{Element, Shell, Widget};
use crate::scrollable::{self, Scrollable};
pub use iced_style::menu::{Appearance, StyleSheet};
/// A list of selectable options.
#[allow(missing_debug_implementations)]
pub struct Menu<'a, T, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
last_selection: &'a mut Option<T>,
width: f32,
padding: Padding,
text_size: Option<f32>,
font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T, Renderer> Menu<'a, T, Renderer>
where
T: ToString + Clone,
Renderer: text::Renderer + 'a,
Renderer::Theme:
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
{
/// Creates a new [`Menu`] with the given [`State`], a list of options, and
/// the message to produced when an option is selected.
pub fn new(
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
last_selection: &'a mut Option<T>,
) -> Self {
Menu {
state,
options,
hovered_option,
last_selection,
width: 0.0,
padding: Padding::ZERO,
text_size: None,
font: None,
style: Default::default(),
}
}
/// Sets the width of the [`Menu`].
pub fn width(mut self, width: f32) -> Self {
self.width = width;
self
}
/// Sets the [`Padding`] of the [`Menu`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the text size of the [`Menu`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
self.text_size = Some(text_size.into().0);
self
}
/// Sets the font of the [`Menu`].
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the style of the [`Menu`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
/// Turns the [`Menu`] into an overlay [`Element`] at the given target
/// position.
///
/// The `target_height` will be used to display the menu either on top
/// of the target or under it, depending on the screen position and the
/// dimensions of the [`Menu`].
pub fn overlay<Message: 'a>(
self,
position: Point,
target_height: f32,
) -> overlay::Element<'a, Message, Renderer> {
overlay::Element::new(
position,
Box::new(Overlay::new(self, target_height)),
)
}
}
/// The local state of a [`Menu`].
#[derive(Debug)]
pub struct State {
tree: Tree,
}
impl State {
/// Creates a new [`State`] for a [`Menu`].
pub fn new() -> Self {
Self {
tree: Tree::empty(),
}
}
}
impl Default for State {
fn default() -> Self {
Self::new()
}
}
struct Overlay<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
state: &'a mut Tree,
container: Container<'a, Message, Renderer>,
width: f32,
target_height: f32,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> Overlay<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a,
Renderer: text::Renderer,
Renderer::Theme:
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
{
pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self
where
T: Clone + ToString,
{
let Menu {
state,
options,
hovered_option,
last_selection,
width,
padding,
font,
text_size,
style,
} = menu;
let container = Container::new(Scrollable::new(List {
options,
hovered_option,
last_selection,
font,
text_size,
padding,
style: style.clone(),
}));
state.tree.diff(&container as &dyn Widget<_, _>);
Self {
state: &mut state.tree,
container,
width,
target_height,
style,
}
}
}
impl<'a, Message, Renderer> crate::core::Overlay<Message, Renderer>
for Overlay<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
fn layout(
&self,
renderer: &Renderer,
bounds: Size,
position: Point,
) -> layout::Node {
let space_below = bounds.height - (position.y + self.target_height);
let space_above = position.y;
let limits = layout::Limits::new(
Size::ZERO,
Size::new(
bounds.width - position.x,
if space_below > space_above {
space_below
} else {
space_above
},
),
)
.width(self.width);
let mut node = self.container.layout(renderer, &limits);
node.move_to(if space_below > space_above {
position + Vector::new(0.0, self.target_height)
} else {
position - Vector::new(0.0, node.size().height)
});
node
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.container.on_event(
self.state,
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.container.mouse_interaction(
self.state,
layout,
cursor_position,
viewport,
renderer,
)
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
) {
let appearance = theme.appearance(&self.style);
let bounds = layout.bounds();
renderer.fill_quad(
renderer::Quad {
bounds,
border_color: appearance.border_color,
border_width: appearance.border_width,
border_radius: appearance.border_radius.into(),
},
appearance.background,
);
self.container.draw(
self.state,
renderer,
theme,
style,
layout,
cursor_position,
&bounds,
);
}
}
struct List<'a, T, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
options: &'a [T],
hovered_option: &'a mut Option<usize>,
last_selection: &'a mut Option<T>,
padding: Padding,
text_size: Option<f32>,
font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
for List<'a, T, Renderer>
where
T: Clone + ToString,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
fn width(&self) -> Length {
Length::Fill
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
use std::f32;
let limits = limits.width(Length::Fill).height(Length::Shrink);
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let size = {
let intrinsic = Size::new(
0.0,
(text_size * 1.2 + self.padding.vertical())
* self.options.len() as f32,
);
limits.resolve(intrinsic)
};
layout::Node::new(size)
}
fn on_event(
&mut self,
_state: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
if let Some(index) = *self.hovered_option {
if let Some(option) = self.options.get(index) {
*self.last_selection = Some(option.clone());
}
}
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
let text_size = self
.text_size
.unwrap_or_else(|| renderer.default_size());
*self.hovered_option = Some(
((cursor_position.y - bounds.y)
/ (text_size * 1.2 + self.padding.vertical()))
as usize,
);
}
}
Event::Touch(touch::Event::FingerPressed { .. }) => {
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
let text_size = self
.text_size
.unwrap_or_else(|| renderer.default_size());
*self.hovered_option = Some(
((cursor_position.y - bounds.y)
/ (text_size * 1.2 + self.padding.vertical()))
as usize,
);
if let Some(index) = *self.hovered_option {
if let Some(option) = self.options.get(index) {
*self.last_selection = Some(option.clone());
}
}
}
}
_ => {}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
_state: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let is_mouse_over = layout.bounds().contains(cursor_position);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
viewport: &Rectangle,
) {
let appearance = theme.appearance(&self.style);
let bounds = layout.bounds();
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let option_height =
(text_size * 1.2 + self.padding.vertical()) as usize;
let offset = viewport.y - bounds.y;
let start = (offset / option_height as f32) as usize;
let end =
((offset + viewport.height) / option_height as f32).ceil() as usize;
let visible_options = &self.options[start..end.min(self.options.len())];
for (i, option) in visible_options.iter().enumerate() {
let i = start + i;
let is_selected = *self.hovered_option == Some(i);
let bounds = Rectangle {
x: bounds.x,
y: bounds.y + (option_height * i) as f32,
width: bounds.width,
height: text_size * 1.2 + self.padding.vertical(),
};
if is_selected {
renderer.fill_quad(
renderer::Quad {
bounds,
border_color: Color::TRANSPARENT,
border_width: 0.0,
border_radius: appearance.border_radius.into(),
},
appearance.selected_background,
);
}
renderer.fill_text(Text {
content: &option.to_string(),
bounds: Rectangle {
x: bounds.x + self.padding.left,
y: bounds.center_y(),
width: f32::INFINITY,
..bounds
},
size: text_size,
font: self.font.unwrap_or_else(|| renderer.default_font()),
color: if is_selected {
appearance.selected_text_color
} else {
appearance.text_color
},
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
});
}
}
}
impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>>
for Element<'a, Message, Renderer>
where
T: ToString + Clone,
Message: 'a,
Renderer: 'a + text::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(list: List<'a, T, Renderer>) -> Self {
Element::new(list)
}
}

991
widget/src/pane_grid.rs Normal file
View file

@ -0,0 +1,991 @@
//! Let your users split regions of your application and organize layout dynamically.
//!
//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
//!
//! # Example
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid
mod axis;
mod configuration;
mod content;
mod direction;
mod draggable;
mod node;
mod pane;
mod split;
mod title_bar;
pub mod state;
pub use axis::Axis;
pub use configuration::Configuration;
pub use content::Content;
pub use direction::Direction;
pub use draggable::Draggable;
pub use node::Node;
pub use pane::Pane;
pub use split::Split;
pub use state::State;
pub use title_bar::TitleBar;
pub use crate::style::pane_grid::{Line, StyleSheet};
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay::{self, Group};
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell,
Size, Vector, Widget,
};
/// A collection of panes distributed using either vertical or horizontal splits
/// to completely fill the space available.
///
/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](https://gfycat.com/frailfreshairedaleterrier)
///
/// This distribution of space is common in tiling window managers (like
/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
/// [`tmux`](https://github.com/tmux/tmux)).
///
/// A [`PaneGrid`] supports:
///
/// * Vertical and horizontal splits
/// * Tracking of the last active pane
/// * Mouse-based resizing
/// * Drag and drop to reorganize panes
/// * Hotkey support
/// * Configurable modifier keys
/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
///
/// ## Example
///
/// ```
/// # use iced_widget::{pane_grid, text};
/// #
/// # type PaneGrid<'a, Message> =
/// # iced_widget::PaneGrid<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// enum PaneState {
/// SomePane,
/// AnotherKindOfPane,
/// }
///
/// enum Message {
/// PaneDragged(pane_grid::DragEvent),
/// PaneResized(pane_grid::ResizeEvent),
/// }
///
/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane);
///
/// let pane_grid =
/// PaneGrid::new(&state, |pane, state, is_maximized| {
/// pane_grid::Content::new(match state {
/// PaneState::SomePane => text("This is some pane"),
/// PaneState::AnotherKindOfPane => text("This is another kind of pane"),
/// })
/// })
/// .on_drag(Message::PaneDragged)
/// .on_resize(10, Message::PaneResized);
/// ```
#[allow(missing_debug_implementations)]
pub struct PaneGrid<'a, Message, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
contents: Contents<'a, Content<'a, Message, Renderer>>,
width: Length,
height: Length,
spacing: f32,
on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
/// Creates a [`PaneGrid`] with the given [`State`] and view function.
///
/// The view function will be called to display each [`Pane`] present in the
/// [`State`]. [`bool`] is set if the pane is maximized.
pub fn new<T>(
state: &'a State<T>,
view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>,
) -> Self {
let contents = if let Some((pane, pane_state)) =
state.maximized.and_then(|pane| {
state.panes.get(&pane).map(|pane_state| (pane, pane_state))
}) {
Contents::Maximized(
pane,
view(pane, pane_state, true),
Node::Pane(pane),
)
} else {
Contents::All(
state
.panes
.iter()
.map(|(pane, pane_state)| {
(*pane, view(*pane, pane_state, false))
})
.collect(),
&state.internal,
)
};
Self {
contents,
width: Length::Fill,
height: Length::Fill,
spacing: 0.0,
on_click: None,
on_drag: None,
on_resize: None,
style: Default::default(),
}
}
/// Sets the width of the [`PaneGrid`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`PaneGrid`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the spacing _between_ the panes of the [`PaneGrid`].
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the message that will be produced when a [`Pane`] of the
/// [`PaneGrid`] is clicked.
pub fn on_click<F>(mut self, f: F) -> Self
where
F: 'a + Fn(Pane) -> Message,
{
self.on_click = Some(Box::new(f));
self
}
/// Enables the drag and drop interactions of the [`PaneGrid`], which will
/// use the provided function to produce messages.
pub fn on_drag<F>(mut self, f: F) -> Self
where
F: 'a + Fn(DragEvent) -> Message,
{
self.on_drag = Some(Box::new(f));
self
}
/// Enables the resize interactions of the [`PaneGrid`], which will
/// use the provided function to produce messages.
///
/// The `leeway` describes the amount of space around a split that can be
/// used to grab it.
///
/// The grabbable area of a split will have a length of `spacing + leeway`,
/// properly centered. In other words, a length of
/// `(spacing + leeway) / 2.0` on either side of the split line.
pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self
where
F: 'a + Fn(ResizeEvent) -> Message,
{
self.on_resize = Some((leeway.into().0, Box::new(f)));
self
}
/// Sets the style of the [`PaneGrid`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
fn drag_enabled(&self) -> bool {
(!self.contents.is_maximized())
.then(|| self.on_drag.is_some())
.unwrap_or_default()
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for PaneGrid<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<state::Action>()
}
fn state(&self) -> tree::State {
tree::State::new(state::Action::Idle)
}
fn children(&self) -> Vec<Tree> {
self.contents
.iter()
.map(|(_, content)| content.state())
.collect()
}
fn diff(&self, tree: &mut Tree) {
match &self.contents {
Contents::All(contents, _) => tree.diff_children_custom(
contents,
|state, (_, content)| content.diff(state),
|(_, content)| content.state(),
),
Contents::Maximized(_, content, _) => tree.diff_children_custom(
&[content],
|state, content| content.diff(state),
|content| content.state(),
),
}
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
self.contents.layout(),
self.width,
self.height,
self.spacing,
self.contents.iter(),
|content, renderer, limits| content.layout(renderer, limits),
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.contents
.iter()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|(((_pane, content), state), layout)| {
content.operate(state, layout, renderer, operation);
})
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let action = tree.state.downcast_mut::<state::Action>();
let on_drag = if self.drag_enabled() {
&self.on_drag
} else {
&None
};
let event_status = update(
action,
self.contents.layout(),
&event,
layout,
cursor_position,
shell,
self.spacing,
self.contents.iter(),
&self.on_click,
on_drag,
&self.on_resize,
);
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
self.contents
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|(((pane, content), tree), layout)| {
let is_picked = picked_pane == Some(pane);
content.on_event(
tree,
event.clone(),
layout,
cursor_position,
renderer,
clipboard,
shell,
is_picked,
)
})
.fold(event_status, event::Status::merge)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(
tree.state.downcast_ref(),
self.contents.layout(),
layout,
cursor_position,
self.spacing,
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
)
.unwrap_or_else(|| {
self.contents
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|(((_pane, content), tree), layout)| {
content.mouse_interaction(
tree,
layout,
cursor_position,
viewport,
renderer,
self.drag_enabled(),
)
})
.max()
.unwrap_or_default()
})
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
draw(
tree.state.downcast_ref(),
self.contents.layout(),
layout,
cursor_position,
renderer,
theme,
style,
viewport,
self.spacing,
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
&self.style,
self.contents
.iter()
.zip(&tree.children)
.map(|((pane, content), tree)| (pane, (content, tree))),
|(content, tree),
renderer,
style,
layout,
cursor_position,
rectangle| {
content.draw(
tree,
renderer,
theme,
style,
layout,
cursor_position,
rectangle,
);
},
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'_, Message, Renderer>> {
let children = self
.contents
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.filter_map(|(((_, content), state), layout)| {
content.overlay(state, layout, renderer)
})
.collect::<Vec<_>>();
(!children.is_empty()).then(|| Group::with_children(children).overlay())
}
}
impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
fn from(
pane_grid: PaneGrid<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(pane_grid)
}
}
/// Calculates the [`Layout`] of a [`PaneGrid`].
pub fn layout<Renderer, T>(
renderer: &Renderer,
limits: &layout::Limits,
node: &Node,
width: Length,
height: Length,
spacing: f32,
contents: impl Iterator<Item = (Pane, T)>,
layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
let size = limits.resolve(Size::ZERO);
let regions = node.pane_regions(spacing, size);
let children = contents
.filter_map(|(pane, content)| {
let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height);
let mut node = layout_content(
content,
renderer,
&layout::Limits::new(size, size),
);
node.move_to(Point::new(region.x, region.y));
Some(node)
})
.collect();
layout::Node::with_children(size, children)
}
/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`]
/// accordingly.
pub fn update<'a, Message, T: Draggable>(
action: &mut state::Action,
node: &Node,
event: &Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
spacing: f32,
contents: impl Iterator<Item = (Pane, T)>,
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
) -> event::Status {
let mut event_status = event::Status::Ignored;
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
event_status = event::Status::Captured;
match on_resize {
Some((leeway, _)) => {
let relative_cursor = Point::new(
cursor_position.x - bounds.x,
cursor_position.y - bounds.y,
);
let splits = node.split_regions(
spacing,
Size::new(bounds.width, bounds.height),
);
let clicked_split = hovered_split(
splits.iter(),
spacing + leeway,
relative_cursor,
);
if let Some((split, axis, _)) = clicked_split {
if action.picked_pane().is_none() {
*action =
state::Action::Resizing { split, axis };
}
} else {
click_pane(
action,
layout,
cursor_position,
shell,
contents,
on_click,
on_drag,
);
}
}
None => {
click_pane(
action,
layout,
cursor_position,
shell,
contents,
on_click,
on_drag,
);
}
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if let Some((pane, _)) = action.picked_pane() {
if let Some(on_drag) = on_drag {
let mut dropped_region = contents
.zip(layout.children())
.filter(|(_, layout)| {
layout.bounds().contains(cursor_position)
});
let event = match dropped_region.next() {
Some(((target, _), _)) if pane != target => {
DragEvent::Dropped { pane, target }
}
_ => DragEvent::Canceled { pane },
};
shell.publish(on_drag(event));
}
*action = state::Action::Idle;
event_status = event::Status::Captured;
} else if action.picked_split().is_some() {
*action = state::Action::Idle;
event_status = event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if let Some((_, on_resize)) = on_resize {
if let Some((split, _)) = action.picked_split() {
let bounds = layout.bounds();
let splits = node.split_regions(
spacing,
Size::new(bounds.width, bounds.height),
);
if let Some((axis, rectangle, _)) = splits.get(&split) {
let ratio = match axis {
Axis::Horizontal => {
let position =
cursor_position.y - bounds.y - rectangle.y;
(position / rectangle.height).clamp(0.1, 0.9)
}
Axis::Vertical => {
let position =
cursor_position.x - bounds.x - rectangle.x;
(position / rectangle.width).clamp(0.1, 0.9)
}
};
shell.publish(on_resize(ResizeEvent { split, ratio }));
event_status = event::Status::Captured;
}
}
}
}
_ => {}
}
event_status
}
fn click_pane<'a, Message, T>(
action: &mut state::Action,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
contents: impl Iterator<Item = (Pane, T)>,
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
) where
T: Draggable,
{
let mut clicked_region = contents
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().contains(cursor_position));
if let Some(((pane, content), layout)) = clicked_region.next() {
if let Some(on_click) = &on_click {
shell.publish(on_click(pane));
}
if let Some(on_drag) = &on_drag {
if content.can_be_dragged_at(layout, cursor_position) {
let pane_position = layout.position();
let origin = cursor_position
- Vector::new(pane_position.x, pane_position.y);
*action = state::Action::Dragging { pane, origin };
shell.publish(on_drag(DragEvent::Picked { pane }));
}
}
}
}
/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`].
pub fn mouse_interaction(
action: &state::Action,
node: &Node,
layout: Layout<'_>,
cursor_position: Point,
spacing: f32,
resize_leeway: Option<f32>,
) -> Option<mouse::Interaction> {
if action.picked_pane().is_some() {
return Some(mouse::Interaction::Grabbing);
}
let resize_axis =
action.picked_split().map(|(_, axis)| axis).or_else(|| {
resize_leeway.and_then(|leeway| {
let bounds = layout.bounds();
let splits = node.split_regions(spacing, bounds.size());
let relative_cursor = Point::new(
cursor_position.x - bounds.x,
cursor_position.y - bounds.y,
);
hovered_split(splits.iter(), spacing + leeway, relative_cursor)
.map(|(_, axis, _)| axis)
})
});
if let Some(resize_axis) = resize_axis {
return Some(match resize_axis {
Axis::Horizontal => mouse::Interaction::ResizingVertically,
Axis::Vertical => mouse::Interaction::ResizingHorizontally,
});
}
None
}
/// Draws a [`PaneGrid`].
pub fn draw<Renderer, T>(
action: &state::Action,
node: &Node,
layout: Layout<'_>,
cursor_position: Point,
renderer: &mut Renderer,
theme: &Renderer::Theme,
default_style: &renderer::Style,
viewport: &Rectangle,
spacing: f32,
resize_leeway: Option<f32>,
style: &<Renderer::Theme as StyleSheet>::Style,
contents: impl Iterator<Item = (Pane, T)>,
draw_pane: impl Fn(
T,
&mut Renderer,
&renderer::Style,
Layout<'_>,
Point,
&Rectangle,
),
) where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
let picked_pane = action.picked_pane();
let picked_split = action
.picked_split()
.and_then(|(split, axis)| {
let bounds = layout.bounds();
let splits = node.split_regions(spacing, bounds.size());
let (_axis, region, ratio) = splits.get(&split)?;
let region = axis.split_line_bounds(*region, *ratio, spacing);
Some((axis, region + Vector::new(bounds.x, bounds.y), true))
})
.or_else(|| match resize_leeway {
Some(leeway) => {
let bounds = layout.bounds();
let relative_cursor = Point::new(
cursor_position.x - bounds.x,
cursor_position.y - bounds.y,
);
let splits = node.split_regions(spacing, bounds.size());
let (_split, axis, region) = hovered_split(
splits.iter(),
spacing + leeway,
relative_cursor,
)?;
Some((axis, region + Vector::new(bounds.x, bounds.y), false))
}
None => None,
});
let pane_cursor_position = if picked_pane.is_some() {
// TODO: Remove once cursor availability is encoded in the type
// system
Point::new(-1.0, -1.0)
} else {
cursor_position
};
let mut render_picked_pane = None;
for ((id, pane), layout) in contents.zip(layout.children()) {
match picked_pane {
Some((dragging, origin)) if id == dragging => {
render_picked_pane = Some((pane, origin, layout));
}
_ => {
draw_pane(
pane,
renderer,
default_style,
layout,
pane_cursor_position,
viewport,
);
}
}
}
// Render picked pane last
if let Some((pane, origin, layout)) = render_picked_pane {
let bounds = layout.bounds();
renderer.with_translation(
cursor_position
- Point::new(bounds.x + origin.x, bounds.y + origin.y),
|renderer| {
renderer.with_layer(bounds, |renderer| {
draw_pane(
pane,
renderer,
default_style,
layout,
pane_cursor_position,
viewport,
);
});
},
);
};
if let Some((axis, split_region, is_picked)) = picked_split {
let highlight = if is_picked {
theme.picked_split(style)
} else {
theme.hovered_split(style)
};
if let Some(highlight) = highlight {
renderer.fill_quad(
renderer::Quad {
bounds: match axis {
Axis::Horizontal => Rectangle {
x: split_region.x,
y: (split_region.y
+ (split_region.height - highlight.width)
/ 2.0)
.round(),
width: split_region.width,
height: highlight.width,
},
Axis::Vertical => Rectangle {
x: (split_region.x
+ (split_region.width - highlight.width) / 2.0)
.round(),
y: split_region.y,
width: highlight.width,
height: split_region.height,
},
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
highlight.color,
);
}
}
}
/// An event produced during a drag and drop interaction of a [`PaneGrid`].
#[derive(Debug, Clone, Copy)]
pub enum DragEvent {
/// A [`Pane`] was picked for dragging.
Picked {
/// The picked [`Pane`].
pane: Pane,
},
/// A [`Pane`] was dropped on top of another [`Pane`].
Dropped {
/// The picked [`Pane`].
pane: Pane,
/// The [`Pane`] where the picked one was dropped on.
target: Pane,
},
/// A [`Pane`] was picked and then dropped outside of other [`Pane`]
/// boundaries.
Canceled {
/// The picked [`Pane`].
pane: Pane,
},
}
/// An event produced during a resize interaction of a [`PaneGrid`].
#[derive(Debug, Clone, Copy)]
pub struct ResizeEvent {
/// The [`Split`] that is being dragged for resizing.
pub split: Split,
/// The new ratio of the [`Split`].
///
/// The ratio is a value in [0, 1], representing the exact position of a
/// [`Split`] between two panes.
pub ratio: f32,
}
/*
* Helpers
*/
fn hovered_split<'a>(
splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
spacing: f32,
cursor_position: Point,
) -> Option<(Split, Axis, Rectangle)> {
splits
.filter_map(|(split, (axis, region, ratio))| {
let bounds = axis.split_line_bounds(*region, *ratio, spacing);
if bounds.contains(cursor_position) {
Some((*split, *axis, bounds))
} else {
None
}
})
.next()
}
/// The visible contents of the [`PaneGrid`]
#[derive(Debug)]
pub enum Contents<'a, T> {
/// All panes are visible
All(Vec<(Pane, T)>, &'a state::Internal),
/// A maximized pane is visible
Maximized(Pane, T, Node),
}
impl<'a, T> Contents<'a, T> {
/// Returns the layout [`Node`] of the [`Contents`]
pub fn layout(&self) -> &Node {
match self {
Contents::All(_, state) => state.layout(),
Contents::Maximized(_, _, layout) => layout,
}
}
/// Returns an iterator over the values of the [`Contents`]
pub fn iter(&self) -> Box<dyn Iterator<Item = (Pane, &T)> + '_> {
match self {
Contents::All(contents, _) => Box::new(
contents.iter().map(|(pane, content)| (*pane, content)),
),
Contents::Maximized(pane, content, _) => {
Box::new(std::iter::once((*pane, content)))
}
}
}
fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (Pane, &mut T)> + '_> {
match self {
Contents::All(contents, _) => Box::new(
contents.iter_mut().map(|(pane, content)| (*pane, content)),
),
Contents::Maximized(pane, content, _) => {
Box::new(std::iter::once((*pane, content)))
}
}
}
fn is_maximized(&self) -> bool {
matches!(self, Self::Maximized(..))
}
}

View file

@ -0,0 +1,241 @@
use crate::core::Rectangle;
/// A fixed reference line for the measurement of coordinates.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Axis {
/// The horizontal axis: —
Horizontal,
/// The vertical axis: |
Vertical,
}
impl Axis {
/// Splits the provided [`Rectangle`] on the current [`Axis`] with the
/// given `ratio` and `spacing`.
pub fn split(
&self,
rectangle: &Rectangle,
ratio: f32,
spacing: f32,
) -> (Rectangle, Rectangle) {
match self {
Axis::Horizontal => {
let height_top =
(rectangle.height * ratio - spacing / 2.0).round();
let height_bottom = rectangle.height - height_top - spacing;
(
Rectangle {
height: height_top,
..*rectangle
},
Rectangle {
y: rectangle.y + height_top + spacing,
height: height_bottom,
..*rectangle
},
)
}
Axis::Vertical => {
let width_left =
(rectangle.width * ratio - spacing / 2.0).round();
let width_right = rectangle.width - width_left - spacing;
(
Rectangle {
width: width_left,
..*rectangle
},
Rectangle {
x: rectangle.x + width_left + spacing,
width: width_right,
..*rectangle
},
)
}
}
}
/// Calculates the bounds of the split line in a [`Rectangle`] region.
pub fn split_line_bounds(
&self,
rectangle: Rectangle,
ratio: f32,
spacing: f32,
) -> Rectangle {
match self {
Axis::Horizontal => Rectangle {
x: rectangle.x,
y: (rectangle.y + rectangle.height * ratio - spacing / 2.0)
.round(),
width: rectangle.width,
height: spacing,
},
Axis::Vertical => Rectangle {
x: (rectangle.x + rectangle.width * ratio - spacing / 2.0)
.round(),
y: rectangle.y,
width: spacing,
height: rectangle.height,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
enum Case {
Horizontal {
overall_height: f32,
spacing: f32,
top_height: f32,
bottom_y: f32,
bottom_height: f32,
},
Vertical {
overall_width: f32,
spacing: f32,
left_width: f32,
right_x: f32,
right_width: f32,
},
}
#[test]
fn split() {
let cases = vec![
// Even height, even spacing
Case::Horizontal {
overall_height: 10.0,
spacing: 2.0,
top_height: 4.0,
bottom_y: 6.0,
bottom_height: 4.0,
},
// Odd height, even spacing
Case::Horizontal {
overall_height: 9.0,
spacing: 2.0,
top_height: 4.0,
bottom_y: 6.0,
bottom_height: 3.0,
},
// Even height, odd spacing
Case::Horizontal {
overall_height: 10.0,
spacing: 1.0,
top_height: 5.0,
bottom_y: 6.0,
bottom_height: 4.0,
},
// Odd height, odd spacing
Case::Horizontal {
overall_height: 9.0,
spacing: 1.0,
top_height: 4.0,
bottom_y: 5.0,
bottom_height: 4.0,
},
// Even width, even spacing
Case::Vertical {
overall_width: 10.0,
spacing: 2.0,
left_width: 4.0,
right_x: 6.0,
right_width: 4.0,
},
// Odd width, even spacing
Case::Vertical {
overall_width: 9.0,
spacing: 2.0,
left_width: 4.0,
right_x: 6.0,
right_width: 3.0,
},
// Even width, odd spacing
Case::Vertical {
overall_width: 10.0,
spacing: 1.0,
left_width: 5.0,
right_x: 6.0,
right_width: 4.0,
},
// Odd width, odd spacing
Case::Vertical {
overall_width: 9.0,
spacing: 1.0,
left_width: 4.0,
right_x: 5.0,
right_width: 4.0,
},
];
for case in cases {
match case {
Case::Horizontal {
overall_height,
spacing,
top_height,
bottom_y,
bottom_height,
} => {
let a = Axis::Horizontal;
let r = Rectangle {
x: 0.0,
y: 0.0,
width: 10.0,
height: overall_height,
};
let (top, bottom) = a.split(&r, 0.5, spacing);
assert_eq!(
top,
Rectangle {
height: top_height,
..r
}
);
assert_eq!(
bottom,
Rectangle {
y: bottom_y,
height: bottom_height,
..r
}
);
}
Case::Vertical {
overall_width,
spacing,
left_width,
right_x,
right_width,
} => {
let a = Axis::Vertical;
let r = Rectangle {
x: 0.0,
y: 0.0,
width: overall_width,
height: 10.0,
};
let (left, right) = a.split(&r, 0.5, spacing);
assert_eq!(
left,
Rectangle {
width: left_width,
..r
}
);
assert_eq!(
right,
Rectangle {
x: right_x,
width: right_width,
..r
}
);
}
}
}
}
}

View file

@ -0,0 +1,26 @@
use crate::pane_grid::Axis;
/// The arrangement of a [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
#[derive(Debug, Clone)]
pub enum Configuration<T> {
/// A split of the available space.
Split {
/// The direction of the split.
axis: Axis,
/// The ratio of the split in [0.0, 1.0].
ratio: f32,
/// The left/top [`Configuration`] of the split.
a: Box<Configuration<T>>,
/// The right/bottom [`Configuration`] of the split.
b: Box<Configuration<T>>,
},
/// A [`Pane`].
///
/// [`Pane`]: crate::widget::pane_grid::Pane
Pane(T),
}

View file

@ -0,0 +1,373 @@
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{self, Tree};
use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
use crate::pane_grid::{Draggable, TitleBar};
/// The content of a [`Pane`].
///
/// [`Pane`]: crate::widget::pane_grid::Pane
#[allow(missing_debug_implementations)]
pub struct Content<'a, Message, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
title_bar: Option<TitleBar<'a, Message, Renderer>>,
body: Element<'a, Message, Renderer>,
style: <Renderer::Theme as container::StyleSheet>::Style,
}
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
/// Creates a new [`Content`] with the provided body.
pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self {
Self {
title_bar: None,
body: body.into(),
style: Default::default(),
}
}
/// Sets the [`TitleBar`] of this [`Content`].
pub fn title_bar(
mut self,
title_bar: TitleBar<'a, Message, Renderer>,
) -> Self {
self.title_bar = Some(title_bar);
self
}
/// Sets the style of the [`Content`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
pub(super) fn state(&self) -> Tree {
let children = if let Some(title_bar) = self.title_bar.as_ref() {
vec![Tree::new(&self.body), title_bar.state()]
} else {
vec![Tree::new(&self.body), Tree::empty()]
};
Tree {
children,
..Tree::empty()
}
}
pub(super) fn diff(&self, tree: &mut Tree) {
if tree.children.len() == 2 {
if let Some(title_bar) = self.title_bar.as_ref() {
title_bar.diff(&mut tree.children[1]);
}
tree.children[0].diff(&self.body);
} else {
*tree = self.state();
}
}
/// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
///
/// [`Renderer`]: crate::Renderer
pub fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
use container::StyleSheet;
let bounds = layout.bounds();
{
let style = theme.appearance(&self.style);
container::draw_background(renderer, &style, bounds);
}
if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
let title_bar_layout = children.next().unwrap();
let body_layout = children.next().unwrap();
let show_controls = bounds.contains(cursor_position);
self.body.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
body_layout,
cursor_position,
viewport,
);
title_bar.draw(
&tree.children[1],
renderer,
theme,
style,
title_bar_layout,
cursor_position,
viewport,
show_controls,
);
} else {
self.body.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor_position,
viewport,
);
}
}
pub(crate) fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
if let Some(title_bar) = &self.title_bar {
let max_size = limits.max();
let title_bar_layout = title_bar
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
let title_bar_size = title_bar_layout.size();
let mut body_layout = self.body.as_widget().layout(
renderer,
&layout::Limits::new(
Size::ZERO,
Size::new(
max_size.width,
max_size.height - title_bar_size.height,
),
),
);
body_layout.move_to(Point::new(0.0, title_bar_size.height));
layout::Node::with_children(
max_size,
vec![title_bar_layout, body_layout],
)
} else {
self.body.as_widget().layout(renderer, limits)
}
}
pub(crate) fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
let body_layout = if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
title_bar.operate(
&mut tree.children[1],
children.next().unwrap(),
renderer,
operation,
);
children.next().unwrap()
} else {
layout
};
self.body.as_widget().operate(
&mut tree.children[0],
body_layout,
renderer,
operation,
);
}
pub(crate) fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
is_picked: bool,
) -> event::Status {
let mut event_status = event::Status::Ignored;
let body_layout = if let Some(title_bar) = &mut self.title_bar {
let mut children = layout.children();
event_status = title_bar.on_event(
&mut tree.children[1],
event.clone(),
children.next().unwrap(),
cursor_position,
renderer,
clipboard,
shell,
);
children.next().unwrap()
} else {
layout
};
let body_status = if is_picked {
event::Status::Ignored
} else {
self.body.as_widget_mut().on_event(
&mut tree.children[0],
event,
body_layout,
cursor_position,
renderer,
clipboard,
shell,
)
};
event_status.merge(body_status)
}
pub(crate) fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
drag_enabled: bool,
) -> mouse::Interaction {
let (body_layout, title_bar_interaction) =
if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
let title_bar_layout = children.next().unwrap();
let is_over_pick_area = title_bar
.is_over_pick_area(title_bar_layout, cursor_position);
if is_over_pick_area && drag_enabled {
return mouse::Interaction::Grab;
}
let mouse_interaction = title_bar.mouse_interaction(
&tree.children[1],
title_bar_layout,
cursor_position,
viewport,
renderer,
);
(children.next().unwrap(), mouse_interaction)
} else {
(layout, mouse::Interaction::default())
};
self.body
.as_widget()
.mouse_interaction(
&tree.children[0],
body_layout,
cursor_position,
viewport,
renderer,
)
.max(title_bar_interaction)
}
pub(crate) fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
if let Some(title_bar) = self.title_bar.as_mut() {
let mut children = layout.children();
let title_bar_layout = children.next()?;
let mut states = tree.children.iter_mut();
let body_state = states.next().unwrap();
let title_bar_state = states.next().unwrap();
match title_bar.overlay(title_bar_state, title_bar_layout, renderer)
{
Some(overlay) => Some(overlay),
None => self.body.as_widget_mut().overlay(
body_state,
children.next()?,
renderer,
),
}
} else {
self.body.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
)
}
}
}
impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
fn can_be_dragged_at(
&self,
layout: Layout<'_>,
cursor_position: Point,
) -> bool {
if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
let title_bar_layout = children.next().unwrap();
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
} else {
false
}
}
}
impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>
where
T: Into<Element<'a, Message, Renderer>>,
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
fn from(element: T) -> Self {
Self::new(element)
}
}

View file

@ -0,0 +1,12 @@
/// A four cardinal direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// ↑
Up,
/// ↓
Down,
/// ←
Left,
/// →
Right,
}

View file

@ -0,0 +1,12 @@
use crate::core::{Layout, Point};
/// A pane that can be dragged.
pub trait Draggable {
/// Returns whether the [`Draggable`] with the given [`Layout`] can be picked
/// at the provided cursor position.
fn can_be_dragged_at(
&self,
layout: Layout<'_>,
cursor_position: Point,
) -> bool;
}

View file

@ -0,0 +1,250 @@
use crate::core::{Rectangle, Size};
use crate::pane_grid::{Axis, Pane, Split};
use std::collections::BTreeMap;
/// A layout node of a [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
#[derive(Debug, Clone)]
pub enum Node {
/// The region of this [`Node`] is split into two.
Split {
/// The [`Split`] of this [`Node`].
id: Split,
/// The direction of the split.
axis: Axis,
/// The ratio of the split in [0.0, 1.0].
ratio: f32,
/// The left/top [`Node`] of the split.
a: Box<Node>,
/// The right/bottom [`Node`] of the split.
b: Box<Node>,
},
/// The region of this [`Node`] is taken by a [`Pane`].
Pane(Pane),
}
impl Node {
/// Returns an iterator over each [`Split`] in this [`Node`].
pub fn splits(&self) -> impl Iterator<Item = &Split> {
let mut unvisited_nodes = vec![self];
std::iter::from_fn(move || {
while let Some(node) = unvisited_nodes.pop() {
if let Node::Split { id, a, b, .. } = node {
unvisited_nodes.push(a);
unvisited_nodes.push(b);
return Some(id);
}
}
None
})
}
/// Returns the rectangular region for each [`Pane`] in the [`Node`] given
/// the spacing between panes and the total available space.
pub fn pane_regions(
&self,
spacing: f32,
size: Size,
) -> BTreeMap<Pane, Rectangle> {
let mut regions = BTreeMap::new();
self.compute_regions(
spacing,
&Rectangle {
x: 0.0,
y: 0.0,
width: size.width,
height: size.height,
},
&mut regions,
);
regions
}
/// Returns the axis, rectangular region, and ratio for each [`Split`] in
/// the [`Node`] given the spacing between panes and the total available
/// space.
pub fn split_regions(
&self,
spacing: f32,
size: Size,
) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
let mut splits = BTreeMap::new();
self.compute_splits(
spacing,
&Rectangle {
x: 0.0,
y: 0.0,
width: size.width,
height: size.height,
},
&mut splits,
);
splits
}
pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> {
match self {
Node::Split { a, b, .. } => {
a.find(pane).or_else(move || b.find(pane))
}
Node::Pane(p) => {
if p == pane {
Some(self)
} else {
None
}
}
}
}
pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
*self = Node::Split {
id,
axis,
ratio: 0.5,
a: Box::new(self.clone()),
b: Box::new(Node::Pane(new_pane)),
};
}
pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
if let Node::Split { a, b, .. } = self {
a.update(f);
b.update(f);
}
f(self);
}
pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool {
match self {
Node::Split {
id, ratio, a, b, ..
} => {
if id == split {
*ratio = percentage;
true
} else if a.resize(split, percentage) {
true
} else {
b.resize(split, percentage)
}
}
Node::Pane(_) => false,
}
}
pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> {
match self {
Node::Split { a, b, .. } => {
if a.pane() == Some(*pane) {
*self = *b.clone();
Some(self.first_pane())
} else if b.pane() == Some(*pane) {
*self = *a.clone();
Some(self.first_pane())
} else {
a.remove(pane).or_else(|| b.remove(pane))
}
}
Node::Pane(_) => None,
}
}
fn pane(&self) -> Option<Pane> {
match self {
Node::Split { .. } => None,
Node::Pane(pane) => Some(*pane),
}
}
fn first_pane(&self) -> Pane {
match self {
Node::Split { a, .. } => a.first_pane(),
Node::Pane(pane) => *pane,
}
}
fn compute_regions(
&self,
spacing: f32,
current: &Rectangle,
regions: &mut BTreeMap<Pane, Rectangle>,
) {
match self {
Node::Split {
axis, ratio, a, b, ..
} => {
let (region_a, region_b) = axis.split(current, *ratio, spacing);
a.compute_regions(spacing, &region_a, regions);
b.compute_regions(spacing, &region_b, regions);
}
Node::Pane(pane) => {
let _ = regions.insert(*pane, *current);
}
}
}
fn compute_splits(
&self,
spacing: f32,
current: &Rectangle,
splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,
) {
match self {
Node::Split {
axis,
ratio,
a,
b,
id,
} => {
let (region_a, region_b) = axis.split(current, *ratio, spacing);
let _ = splits.insert(*id, (*axis, *current, *ratio));
a.compute_splits(spacing, &region_a, splits);
b.compute_splits(spacing, &region_b, splits);
}
Node::Pane(_) => {}
}
}
}
impl std::hash::Hash for Node {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Node::Split {
id,
axis,
ratio,
a,
b,
} => {
id.hash(state);
axis.hash(state);
((ratio * 100_000.0) as u32).hash(state);
a.hash(state);
b.hash(state);
}
Node::Pane(pane) => {
pane.hash(state);
}
}
}
}

View file

@ -0,0 +1,5 @@
/// A rectangular region in a [`PaneGrid`] used to display widgets.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Pane(pub(super) usize);

View file

@ -0,0 +1,5 @@
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Split(pub(super) usize);

View file

@ -0,0 +1,348 @@
//! The state of a [`PaneGrid`].
//!
//! [`PaneGrid`]: crate::widget::PaneGrid
use crate::core::{Point, Size};
use crate::pane_grid::{Axis, Configuration, Direction, Node, Pane, Split};
use std::collections::HashMap;
/// The state of a [`PaneGrid`].
///
/// It keeps track of the state of each [`Pane`] and the position of each
/// [`Split`].
///
/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is
/// why this struct is generic over the type `T`. Values of this type are
/// provided to the view function of [`PaneGrid::new`] for displaying each
/// [`Pane`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
/// [`PaneGrid::new`]: crate::widget::PaneGrid::new
#[derive(Debug, Clone)]
pub struct State<T> {
/// The panes of the [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub panes: HashMap<Pane, T>,
/// The internal state of the [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub internal: Internal,
/// The maximized [`Pane`] of the [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub(super) maximized: Option<Pane>,
}
impl<T> State<T> {
/// Creates a new [`State`], initializing the first pane with the provided
/// state.
///
/// Alongside the [`State`], it returns the first [`Pane`] identifier.
pub fn new(first_pane_state: T) -> (Self, Pane) {
(
Self::with_configuration(Configuration::Pane(first_pane_state)),
Pane(0),
)
}
/// Creates a new [`State`] with the given [`Configuration`].
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
let mut panes = HashMap::new();
let internal =
Internal::from_configuration(&mut panes, config.into(), 0);
State {
panes,
internal,
maximized: None,
}
}
/// Returns the total amount of panes in the [`State`].
pub fn len(&self) -> usize {
self.panes.len()
}
/// Returns `true` if the amount of panes in the [`State`] is 0.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the internal state of the given [`Pane`], if it exists.
pub fn get(&self, pane: &Pane) -> Option<&T> {
self.panes.get(pane)
}
/// Returns the internal state of the given [`Pane`] with mutability, if it
/// exists.
pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> {
self.panes.get_mut(pane)
}
/// Returns an iterator over all the panes of the [`State`], alongside its
/// internal state.
pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
self.panes.iter()
}
/// Returns a mutable iterator over all the panes of the [`State`],
/// alongside its internal state.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
self.panes.iter_mut()
}
/// Returns the layout of the [`State`].
pub fn layout(&self) -> &Node {
&self.internal.layout
}
/// Returns the adjacent [`Pane`] of another [`Pane`] in the given
/// direction, if there is one.
pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
let regions = self
.internal
.layout
.pane_regions(0.0, Size::new(4096.0, 4096.0));
let current_region = regions.get(pane)?;
let target = match direction {
Direction::Left => {
Point::new(current_region.x - 1.0, current_region.y + 1.0)
}
Direction::Right => Point::new(
current_region.x + current_region.width + 1.0,
current_region.y + 1.0,
),
Direction::Up => {
Point::new(current_region.x + 1.0, current_region.y - 1.0)
}
Direction::Down => Point::new(
current_region.x + 1.0,
current_region.y + current_region.height + 1.0,
),
};
let mut colliding_regions =
regions.iter().filter(|(_, region)| region.contains(target));
let (pane, _) = colliding_regions.next()?;
Some(*pane)
}
/// Splits the given [`Pane`] into two in the given [`Axis`] and
/// initializing the new [`Pane`] with the provided internal state.
pub fn split(
&mut self,
axis: Axis,
pane: &Pane,
state: T,
) -> Option<(Pane, Split)> {
let node = self.internal.layout.find(pane)?;
let new_pane = {
self.internal.last_id = self.internal.last_id.checked_add(1)?;
Pane(self.internal.last_id)
};
let new_split = {
self.internal.last_id = self.internal.last_id.checked_add(1)?;
Split(self.internal.last_id)
};
node.split(new_split, axis, new_pane);
let _ = self.panes.insert(new_pane, state);
let _ = self.maximized.take();
Some((new_pane, new_split))
}
/// Swaps the position of the provided panes in the [`State`].
///
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
/// will need to call this method when handling a [`DragEvent`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
/// [`DragEvent`]: crate::widget::pane_grid::DragEvent
pub fn swap(&mut self, a: &Pane, b: &Pane) {
self.internal.layout.update(&|node| match node {
Node::Split { .. } => {}
Node::Pane(pane) => {
if pane == a {
*node = Node::Pane(*b);
} else if pane == b {
*node = Node::Pane(*a);
}
}
});
}
/// Resizes two panes by setting the position of the provided [`Split`].
///
/// The ratio is a value in [0, 1], representing the exact position of a
/// [`Split`] between two panes.
///
/// If you want to enable resize interactions in your [`PaneGrid`], you will
/// need to call this method when handling a [`ResizeEvent`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
/// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent
pub fn resize(&mut self, split: &Split, ratio: f32) {
let _ = self.internal.layout.resize(split, ratio);
}
/// Closes the given [`Pane`] and returns its internal state and its closest
/// sibling, if it exists.
pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> {
if self.maximized == Some(*pane) {
let _ = self.maximized.take();
}
if let Some(sibling) = self.internal.layout.remove(pane) {
self.panes.remove(pane).map(|state| (state, sibling))
} else {
None
}
}
/// Maximize the given [`Pane`]. Only this pane will be rendered by the
/// [`PaneGrid`] until [`Self::restore()`] is called.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub fn maximize(&mut self, pane: &Pane) {
self.maximized = Some(*pane);
}
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
/// will be rendered by the [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub fn restore(&mut self) {
let _ = self.maximized.take();
}
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub fn maximized(&self) -> Option<Pane> {
self.maximized
}
}
/// The internal state of a [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
#[derive(Debug, Clone)]
pub struct Internal {
layout: Node,
last_id: usize,
}
impl Internal {
/// Initializes the [`Internal`] state of a [`PaneGrid`] from a
/// [`Configuration`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
pub fn from_configuration<T>(
panes: &mut HashMap<Pane, T>,
content: Configuration<T>,
next_id: usize,
) -> Self {
let (layout, last_id) = match content {
Configuration::Split { axis, ratio, a, b } => {
let Internal {
layout: a,
last_id: next_id,
..
} = Self::from_configuration(panes, *a, next_id);
let Internal {
layout: b,
last_id: next_id,
..
} = Self::from_configuration(panes, *b, next_id);
(
Node::Split {
id: Split(next_id),
axis,
ratio,
a: Box::new(a),
b: Box::new(b),
},
next_id + 1,
)
}
Configuration::Pane(state) => {
let id = Pane(next_id);
let _ = panes.insert(id, state);
(Node::Pane(id), next_id + 1)
}
};
Self { layout, last_id }
}
}
/// The current action of a [`PaneGrid`].
///
/// [`PaneGrid`]: crate::widget::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
/// The [`PaneGrid`] is idle.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
Idle,
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
Dragging {
/// The [`Pane`] being dragged.
pane: Pane,
/// The starting [`Point`] of the drag interaction.
origin: Point,
},
/// A [`Split`] in the [`PaneGrid`] is being dragged.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
Resizing {
/// The [`Split`] being dragged.
split: Split,
/// The [`Axis`] of the [`Split`].
axis: Axis,
},
}
impl Action {
/// Returns the current [`Pane`] that is being dragged, if any.
pub fn picked_pane(&self) -> Option<(Pane, Point)> {
match *self {
Action::Dragging { pane, origin, .. } => Some((pane, origin)),
_ => None,
}
}
/// Returns the current [`Split`] that is being dragged, if any.
pub fn picked_split(&self) -> Option<(Split, Axis)> {
match *self {
Action::Resizing { split, axis, .. } => Some((split, axis)),
_ => None,
}
}
}
impl Internal {
/// The layout [`Node`] of the [`Internal`] state
pub fn layout(&self) -> &Node {
&self.layout
}
}

View file

@ -0,0 +1,432 @@
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{self, Tree};
use crate::core::{
Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
};
/// The title bar of a [`Pane`].
///
/// [`Pane`]: crate::widget::pane_grid::Pane
#[allow(missing_debug_implementations)]
pub struct TitleBar<'a, Message, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
content: Element<'a, Message, Renderer>,
controls: Option<Element<'a, Message, Renderer>>,
padding: Padding,
always_show_controls: bool,
style: <Renderer::Theme as container::StyleSheet>::Style,
}
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
/// Creates a new [`TitleBar`] with the given content.
pub fn new<E>(content: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
{
Self {
content: content.into(),
controls: None,
padding: Padding::ZERO,
always_show_controls: false,
style: Default::default(),
}
}
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
controls: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
self.controls = Some(controls.into());
self
}
/// Sets the [`Padding`] of the [`TitleBar`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the style of the [`TitleBar`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
/// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
/// always visible.
///
/// By default, the controls are only visible when the [`Pane`] of this
/// [`TitleBar`] is hovered.
///
/// [`controls`]: Self::controls
/// [`Pane`]: crate::widget::pane_grid::Pane
pub fn always_show_controls(mut self) -> Self {
self.always_show_controls = true;
self
}
}
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
pub(super) fn state(&self) -> Tree {
let children = if let Some(controls) = self.controls.as_ref() {
vec![Tree::new(&self.content), Tree::new(controls)]
} else {
vec![Tree::new(&self.content), Tree::empty()]
};
Tree {
children,
..Tree::empty()
}
}
pub(super) fn diff(&self, tree: &mut Tree) {
if tree.children.len() == 2 {
if let Some(controls) = self.controls.as_ref() {
tree.children[1].diff(controls);
}
tree.children[0].diff(&self.content);
} else {
*tree = self.state();
}
}
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
///
/// [`Renderer`]: crate::Renderer
pub fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
show_controls: bool,
) {
use container::StyleSheet;
let bounds = layout.bounds();
let style = theme.appearance(&self.style);
let inherited_style = renderer::Style {
text_color: style.text_color.unwrap_or(inherited_style.text_color),
};
container::draw_background(renderer, &style, bounds);
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
let mut show_title = true;
if let Some(controls) = &self.controls {
if show_controls || self.always_show_controls {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
show_title = false;
}
controls.as_widget().draw(
&tree.children[1],
renderer,
theme,
&inherited_style,
controls_layout,
cursor_position,
viewport,
);
}
}
if show_title {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
&inherited_style,
title_layout,
cursor_position,
viewport,
);
}
}
/// Returns whether the mouse cursor is over the pick area of the
/// [`TitleBar`] or not.
///
/// The whole [`TitleBar`] is a pick area, except its controls.
pub fn is_over_pick_area(
&self,
layout: Layout<'_>,
cursor_position: Point,
) -> bool {
if layout.bounds().contains(cursor_position) {
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
if self.controls.is_some() {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
!controls_layout.bounds().contains(cursor_position)
} else {
!controls_layout.bounds().contains(cursor_position)
&& !title_layout.bounds().contains(cursor_position)
}
} else {
!title_layout.bounds().contains(cursor_position)
}
} else {
false
}
}
pub(crate) fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.pad(self.padding);
let max_size = limits.max();
let title_layout = self
.content
.as_widget()
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
let title_size = title_layout.size();
let mut node = if let Some(controls) = &self.controls {
let mut controls_layout = controls
.as_widget()
.layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
let controls_size = controls_layout.size();
let space_before_controls = max_size.width - controls_size.width;
let height = title_size.height.max(controls_size.height);
controls_layout.move_to(Point::new(space_before_controls, 0.0));
layout::Node::with_children(
Size::new(max_size.width, height),
vec![title_layout, controls_layout],
)
} else {
layout::Node::with_children(
Size::new(max_size.width, title_size.height),
vec![title_layout],
)
};
node.move_to(Point::new(self.padding.left, self.padding.top));
layout::Node::with_children(node.size().pad(self.padding), vec![node])
}
pub(crate) fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
let mut show_title = true;
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
show_title = false;
}
controls.as_widget().operate(
&mut tree.children[1],
controls_layout,
renderer,
operation,
)
};
if show_title {
self.content.as_widget().operate(
&mut tree.children[0],
title_layout,
renderer,
operation,
)
}
}
pub(crate) fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
let mut show_title = true;
let control_status = if let Some(controls) = &mut self.controls {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
show_title = false;
}
controls.as_widget_mut().on_event(
&mut tree.children[1],
event.clone(),
controls_layout,
cursor_position,
renderer,
clipboard,
shell,
)
} else {
event::Status::Ignored
};
let title_status = if show_title {
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
title_layout,
cursor_position,
renderer,
clipboard,
shell,
)
} else {
event::Status::Ignored
};
control_status.merge(title_status)
}
pub(crate) fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
let title_interaction = self.content.as_widget().mouse_interaction(
&tree.children[0],
title_layout,
cursor_position,
viewport,
renderer,
);
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
let controls_interaction = controls.as_widget().mouse_interaction(
&tree.children[1],
controls_layout,
cursor_position,
viewport,
renderer,
);
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
controls_interaction
} else {
controls_interaction.max(title_interaction)
}
} else {
title_interaction
}
}
pub(crate) fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let mut children = layout.children();
let padded = children.next()?;
let mut children = padded.children();
let title_layout = children.next()?;
let Self {
content, controls, ..
} = self;
let mut states = tree.children.iter_mut();
let title_state = states.next().unwrap();
let controls_state = states.next().unwrap();
content
.as_widget_mut()
.overlay(title_state, title_layout, renderer)
.or_else(move || {
controls.as_mut().and_then(|controls| {
let controls_layout = children.next()?;
controls.as_widget_mut().overlay(
controls_state,
controls_layout,
renderer,
)
})
})
}
}

658
widget/src/pick_list.rs Normal file
View file

@ -0,0 +1,658 @@
//! Display a dropdown list of selectable values.
use crate::container;
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
Shell, Size, Widget,
};
use crate::overlay::menu::{self, Menu};
use crate::scrollable;
use std::borrow::Cow;
pub use crate::style::pick_list::{Appearance, StyleSheet};
/// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)]
pub struct PickList<'a, T, Message, Renderer = crate::Renderer>
where
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
on_selected: Box<dyn Fn(T) -> Message + 'a>,
options: Cow<'a, [T]>,
placeholder: Option<String>,
selected: Option<T>,
width: Length,
padding: Padding,
text_size: Option<f32>,
font: Option<Renderer::Font>,
handle: Handle<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer>
where
T: ToString + Eq,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
/// The default padding of a [`PickList`].
pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
/// Creates a new [`PickList`] with the given list of options, the current
/// selected value, and the message to produce when an option is selected.
pub fn new(
options: impl Into<Cow<'a, [T]>>,
selected: Option<T>,
on_selected: impl Fn(T) -> Message + 'a,
) -> Self {
Self {
on_selected: Box::new(on_selected),
options: options.into(),
placeholder: None,
selected,
width: Length::Shrink,
padding: Self::DEFAULT_PADDING,
text_size: None,
font: None,
handle: Default::default(),
style: Default::default(),
}
}
/// Sets the placeholder of the [`PickList`].
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
/// Sets the width of the [`PickList`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the [`Padding`] of the [`PickList`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the text size of the [`PickList`].
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
self.text_size = Some(size.into().0);
self
}
/// Sets the font of the [`PickList`].
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the [`Handle`] of the [`PickList`].
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
self.handle = handle;
self
}
/// Sets the style of the [`PickList`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
for PickList<'a, T, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<T>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<T>::new())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.padding,
self.text_size,
self.font,
self.placeholder.as_deref(),
&self.options,
)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
update(
event,
layout,
cursor_position,
shell,
self.on_selected.as_ref(),
self.selected.as_ref(),
&self.options,
|| tree.state.downcast_mut::<State<T>>(),
)
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(layout, cursor_position)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
let font = self.font.unwrap_or_else(|| renderer.default_font());
draw(
renderer,
theme,
layout,
cursor_position,
self.padding,
self.text_size,
font,
self.placeholder.as_deref(),
self.selected.as_ref(),
&self.handle,
&self.style,
|| tree.state.downcast_ref::<State<T>>(),
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_mut::<State<T>>();
overlay(
layout,
state,
self.padding,
self.text_size,
self.font.unwrap_or_else(|| renderer.default_font()),
&self.options,
self.style.clone(),
)
}
}
impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self {
Self::new(pick_list)
}
}
/// The local state of a [`PickList`].
#[derive(Debug)]
pub struct State<T> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
last_selection: Option<T>,
}
impl<T> State<T> {
/// Creates a new [`State`] for a [`PickList`].
pub fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
last_selection: Option::default(),
}
}
}
impl<T> Default for State<T> {
fn default() -> Self {
Self::new()
}
}
/// The handle to the right side of the [`PickList`].
#[derive(Debug, Clone, PartialEq)]
pub enum Handle<Font> {
/// Displays an arrow icon (▼).
///
/// This is the default.
Arrow {
/// Font size of the content.
size: Option<f32>,
},
/// A custom static handle.
Static(Icon<Font>),
/// A custom dynamic handle.
Dynamic {
/// The [`Icon`] used when [`PickList`] is closed.
closed: Icon<Font>,
/// The [`Icon`] used when [`PickList`] is open.
open: Icon<Font>,
},
/// No handle will be shown.
None,
}
impl<Font> Default for Handle<Font> {
fn default() -> Self {
Self::Arrow { size: None }
}
}
/// The icon of a [`Handle`].
#[derive(Debug, Clone, PartialEq)]
pub struct Icon<Font> {
/// Font that will be used to display the `code_point`,
pub font: Font,
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
pub size: Option<f32>,
}
/// Computes the layout of a [`PickList`].
pub fn layout<Renderer, T>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
text_size: Option<f32>,
font: Option<Renderer::Font>,
placeholder: Option<&str>,
options: &[T],
) -> layout::Node
where
Renderer: text::Renderer,
T: ToString,
{
use std::f32;
let limits = limits.width(width).height(Length::Shrink).pad(padding);
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
let max_width = match width {
Length::Shrink => {
let measure = |label: &str| -> f32 {
let (width, _) = renderer.measure(
label,
text_size,
font.unwrap_or_else(|| renderer.default_font()),
Size::new(f32::INFINITY, f32::INFINITY),
);
width.round()
};
let labels = options.iter().map(ToString::to_string);
let labels_width = labels
.map(|label| measure(&label))
.fold(100.0, |candidate, current| current.max(candidate));
let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
labels_width.max(placeholder_width)
}
_ => 0.0,
};
let size = {
let intrinsic =
Size::new(max_width + text_size + padding.left, text_size * 1.2);
limits.resolve(intrinsic).pad(padding)
};
layout::Node::new(size)
}
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
/// accordingly.
pub fn update<'a, T, Message>(
event: Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
state: impl FnOnce() -> &'a mut State<T>,
) -> event::Status
where
T: PartialEq + Clone + 'a,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
let event_status = if state.is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside it's
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
event::Status::Captured
} else if layout.bounds().contains(cursor_position) {
state.is_open = true;
state.hovered_option =
options.iter().position(|option| Some(option) == selected);
event::Status::Captured
} else {
event::Status::Ignored
};
if let Some(last_selection) = state.last_selection.take() {
shell.publish((on_selected)(last_selection));
state.is_open = false;
event::Status::Captured
} else {
event_status
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state = state();
if state.keyboard_modifiers.command()
&& layout.bounds().contains(cursor_position)
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let next_option = if y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
} else if y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
options.last()
}
} else {
None
};
if let Some(next_option) = next_option {
shell.publish((on_selected)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = state();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
}
_ => event::Status::Ignored,
}
}
/// Returns the current [`mouse::Interaction`] of a [`PickList`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor_position: Point,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
/// Returns the current overlay of a [`PickList`].
pub fn overlay<'a, T, Message, Renderer>(
layout: Layout<'_>,
state: &'a mut State<T>,
padding: Padding,
text_size: Option<f32>,
font: Renderer::Font,
options: &'a [T],
style: <Renderer::Theme as StyleSheet>::Style,
) -> Option<overlay::Element<'a, Message, Renderer>>
where
T: Clone + ToString,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style:
From<<Renderer::Theme as StyleSheet>::Style>,
{
if state.is_open {
let bounds = layout.bounds();
let mut menu = Menu::new(
&mut state.menu,
options,
&mut state.hovered_option,
&mut state.last_selection,
)
.width(bounds.width)
.padding(padding)
.font(font)
.style(style);
if let Some(text_size) = text_size {
menu = menu.text_size(text_size);
}
Some(menu.overlay(layout.position(), bounds.height))
} else {
None
}
}
/// Draws a [`PickList`].
pub fn draw<'a, T, Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor_position: Point,
padding: Padding,
text_size: Option<f32>,
font: Renderer::Font,
placeholder: Option<&str>,
selected: Option<&T>,
handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State<T>,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
T: ToString + 'a,
{
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let is_selected = selected.is_some();
let style = if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
renderer.fill_quad(
renderer::Quad {
bounds,
border_color: style.border_color,
border_width: style.border_width,
border_radius: style.border_radius.into(),
},
style.background,
);
let handle = match handle {
Handle::Arrow { size } => {
Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size))
}
Handle::Static(Icon {
font,
code_point,
size,
}) => Some((*font, *code_point, *size)),
Handle::Dynamic { open, closed } => {
if state().is_open {
Some((open.font, open.code_point, open.size))
} else {
Some((closed.font, closed.code_point, closed.size))
}
}
Handle::None => None,
};
if let Some((font, code_point, size)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(Text {
content: &code_point.to_string(),
size,
font,
color: style.handle_color,
bounds: Rectangle {
x: bounds.x + bounds.width - padding.horizontal(),
y: bounds.center_y(),
height: size * 1.2,
..bounds
},
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
});
}
let label = selected.map(ToString::to_string);
if let Some(label) = label.as_deref().or(placeholder) {
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(Text {
content: label,
size: text_size,
font,
color: if is_selected {
style.text_color
} else {
style.placeholder_color
},
bounds: Rectangle {
x: bounds.x + padding.left,
y: bounds.center_y(),
width: bounds.width - padding.horizontal(),
height: text_size * 1.2,
},
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
});
}
}

172
widget/src/progress_bar.rs Normal file
View file

@ -0,0 +1,172 @@
//! Provide progress feedback to your users.
use crate::core::layout;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
Color, Element, Layout, Length, Point, Rectangle, Size, Widget,
};
use std::ops::RangeInclusive;
pub use iced_style::progress_bar::{Appearance, StyleSheet};
/// A bar that displays progress.
///
/// # Example
/// ```
/// # type ProgressBar =
/// # iced_widget::ProgressBar<iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// let value = 50.0;
///
/// ProgressBar::new(0.0..=100.0, value);
/// ```
///
/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
#[allow(missing_debug_implementations)]
pub struct ProgressBar<Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
range: RangeInclusive<f32>,
value: f32,
width: Length,
height: Option<Length>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<Renderer> ProgressBar<Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default height of a [`ProgressBar`].
pub const DEFAULT_HEIGHT: f32 = 30.0;
/// Creates a new [`ProgressBar`].
///
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`ProgressBar`]
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
ProgressBar {
value: value.clamp(*range.start(), *range.end()),
range,
width: Length::Fill,
height: None,
style: Default::default(),
}
}
/// Sets the width of the [`ProgressBar`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`ProgressBar`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = Some(height.into());
self
}
/// Sets the style of the [`ProgressBar`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits
.width(self.width)
.height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)));
let size = limits.resolve(Size::ZERO);
layout::Node::new(size)
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let (range_start, range_end) = self.range.clone().into_inner();
let active_progress_width = if range_start >= range_end {
0.0
} else {
bounds.width * (self.value - range_start)
/ (range_end - range_start)
};
let style = theme.appearance(&self.style);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle { ..bounds },
border_radius: style.border_radius.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
style.background,
);
if active_progress_width > 0.0 {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
width: active_progress_width,
..bounds
},
border_radius: style.border_radius.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
style.bar,
);
}
}
}
impl<'a, Message, Renderer> From<ProgressBar<Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
progress_bar: ProgressBar<Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(progress_bar)
}
}

297
widget/src/qr_code.rs Normal file
View file

@ -0,0 +1,297 @@
//! Encode and display information in a QR code.
use crate::canvas;
use crate::core::layout;
use crate::core::renderer::{self, Renderer as _};
use crate::core::widget::Tree;
use crate::core::{
Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
};
use crate::Renderer;
use thiserror::Error;
const DEFAULT_CELL_SIZE: u16 = 4;
const QUIET_ZONE: usize = 2;
/// A type of matrix barcode consisting of squares arranged in a grid which
/// can be read by an imaging device, such as a camera.
#[derive(Debug)]
pub struct QRCode<'a> {
state: &'a State,
dark: Color,
light: Color,
cell_size: u16,
}
impl<'a> QRCode<'a> {
/// Creates a new [`QRCode`] with the provided [`State`].
pub fn new(state: &'a State) -> Self {
Self {
cell_size: DEFAULT_CELL_SIZE,
dark: Color::BLACK,
light: Color::WHITE,
state,
}
}
/// Sets both the dark and light [`Color`]s of the [`QRCode`].
pub fn color(mut self, dark: Color, light: Color) -> Self {
self.dark = dark;
self.light = light;
self
}
/// Sets the size of the squares of the grid cell of the [`QRCode`].
pub fn cell_size(mut self, cell_size: u16) -> Self {
self.cell_size = cell_size;
self
}
}
impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
fn width(&self) -> Length {
Length::Shrink
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
_renderer: &Renderer<Theme>,
_limits: &layout::Limits,
) -> layout::Node {
let side_length = (self.state.width + 2 * QUIET_ZONE) as f32
* f32::from(self.cell_size);
layout::Node::new(Size::new(side_length, side_length))
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer<Theme>,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let side_length = self.state.width + 2 * QUIET_ZONE;
// Reuse cache if possible
let geometry =
self.state.cache.draw(renderer, bounds.size(), |frame| {
// Scale units to cell size
frame.scale(f32::from(self.cell_size));
// Draw background
frame.fill_rectangle(
Point::ORIGIN,
Size::new(side_length as f32, side_length as f32),
self.light,
);
// Avoid drawing on the quiet zone
frame.translate(Vector::new(
QUIET_ZONE as f32,
QUIET_ZONE as f32,
));
// Draw contents
self.state
.contents
.iter()
.enumerate()
.filter(|(_, value)| **value == qrcode::Color::Dark)
.for_each(|(index, _)| {
let row = index / self.state.width;
let column = index % self.state.width;
frame.fill_rectangle(
Point::new(column as f32, row as f32),
Size::UNIT,
self.dark,
);
});
});
let translation = Vector::new(bounds.x, bounds.y);
renderer.with_translation(translation, |renderer| {
renderer.draw_primitive(geometry.0);
});
}
}
impl<'a, Message, Theme> From<QRCode<'a>>
for Element<'a, Message, Renderer<Theme>>
{
fn from(qr_code: QRCode<'a>) -> Self {
Self::new(qr_code)
}
}
/// The state of a [`QRCode`].
///
/// It stores the data that will be displayed.
#[derive(Debug)]
pub struct State {
contents: Vec<qrcode::Color>,
width: usize,
cache: canvas::Cache,
}
impl State {
/// Creates a new [`State`] with the provided data.
///
/// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest
/// size to display the data.
pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> {
let encoded = qrcode::QrCode::new(data)?;
Ok(Self::build(encoded))
}
/// Creates a new [`State`] with the provided [`ErrorCorrection`].
pub fn with_error_correction(
data: impl AsRef<[u8]>,
error_correction: ErrorCorrection,
) -> Result<Self, Error> {
let encoded = qrcode::QrCode::with_error_correction_level(
data,
error_correction.into(),
)?;
Ok(Self::build(encoded))
}
/// Creates a new [`State`] with the provided [`Version`] and
/// [`ErrorCorrection`].
pub fn with_version(
data: impl AsRef<[u8]>,
version: Version,
error_correction: ErrorCorrection,
) -> Result<Self, Error> {
let encoded = qrcode::QrCode::with_version(
data,
version.into(),
error_correction.into(),
)?;
Ok(Self::build(encoded))
}
fn build(encoded: qrcode::QrCode) -> Self {
let width = encoded.width();
let contents = encoded.into_colors();
Self {
contents,
width,
cache: canvas::Cache::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The size of a [`QRCode`].
///
/// The higher the version the larger the grid of cells, and therefore the more
/// information the [`QRCode`] can carry.
pub enum Version {
/// A normal QR code version. It should be between 1 and 40.
Normal(u8),
/// A micro QR code version. It should be between 1 and 4.
Micro(u8),
}
impl From<Version> for qrcode::Version {
fn from(version: Version) -> Self {
match version {
Version::Normal(v) => qrcode::Version::Normal(i16::from(v)),
Version::Micro(v) => qrcode::Version::Micro(i16::from(v)),
}
}
}
/// The error correction level.
///
/// It controls the amount of data that can be damaged while still being able
/// to recover the original information.
///
/// A higher error correction level allows for more corrupted data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCorrection {
/// Low error correction. 7% of the data can be restored.
Low,
/// Medium error correction. 15% of the data can be restored.
Medium,
/// Quartile error correction. 25% of the data can be restored.
Quartile,
/// High error correction. 30% of the data can be restored.
High,
}
impl From<ErrorCorrection> for qrcode::EcLevel {
fn from(ec_level: ErrorCorrection) -> Self {
match ec_level {
ErrorCorrection::Low => qrcode::EcLevel::L,
ErrorCorrection::Medium => qrcode::EcLevel::M,
ErrorCorrection::Quartile => qrcode::EcLevel::Q,
ErrorCorrection::High => qrcode::EcLevel::H,
}
}
}
/// An error that occurred when building a [`State`] for a [`QRCode`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum Error {
/// The data is too long to encode in a QR code for the chosen [`Version`].
#[error(
"The data is too long to encode in a QR code for the chosen version"
)]
DataTooLong,
/// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid.
#[error(
"The chosen version and error correction level combination is invalid."
)]
InvalidVersion,
/// One or more characters in the provided data are not supported by the
/// chosen [`Version`].
#[error(
"One or more characters in the provided data are not supported by the \
chosen version"
)]
UnsupportedCharacterSet,
/// The chosen ECI designator is invalid. A valid designator should be
/// between 0 and 999999.
#[error(
"The chosen ECI designator is invalid. A valid designator should be \
between 0 and 999999."
)]
InvalidEciDesignator,
/// A character that does not belong to the character set was found.
#[error("A character that does not belong to the character set was found")]
InvalidCharacter,
}
impl From<qrcode::types::QrError> for Error {
fn from(error: qrcode::types::QrError) -> Self {
use qrcode::types::QrError;
match error {
QrError::DataTooLong => Error::DataTooLong,
QrError::InvalidVersion => Error::InvalidVersion,
QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet,
QrError::InvalidEciDesignator => Error::InvalidEciDesignator,
QrError::InvalidCharacter => Error::InvalidCharacter,
}
}
}

300
widget/src/radio.rs Normal file
View file

@ -0,0 +1,300 @@
//! Create choices using radio buttons.
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
use crate::core::widget::Tree;
use crate::core::{
Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Rectangle, Shell, Widget,
};
use crate::{Row, Text};
pub use iced_style::radio::{Appearance, StyleSheet};
/// A circular button representing a choice.
///
/// # Example
/// ```
/// # type Radio<Message> =
/// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// pub enum Choice {
/// A,
/// B,
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Message {
/// RadioSelected(Choice),
/// }
///
/// let selected_choice = Some(Choice::A);
///
/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected);
///
/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected);
/// ```
///
/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true)
#[allow(missing_debug_implementations)]
pub struct Radio<Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
is_selected: bool,
on_click: Message,
label: String,
width: Length,
size: f32,
spacing: f32,
text_size: Option<f32>,
font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<Message, Renderer> Radio<Message, Renderer>
where
Message: Clone,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default size of a [`Radio`] button.
pub const DEFAULT_SIZE: f32 = 28.0;
/// The default spacing of a [`Radio`] button.
pub const DEFAULT_SPACING: f32 = 15.0;
/// Creates a new [`Radio`] button.
///
/// It expects:
/// * the value related to the [`Radio`] button
/// * the label of the [`Radio`] button
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>(
value: V,
label: impl Into<String>,
selected: Option<V>,
f: F,
) -> Self
where
V: Eq + Copy,
F: FnOnce(V) -> Message,
{
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING, //15
text_size: None,
font: None,
style: Default::default(),
}
}
/// Sets the size of the [`Radio`] button.
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = size.into().0;
self
}
/// Sets the width of the [`Radio`] button.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the spacing between the [`Radio`] button and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
self
}
/// Sets the text size of the [`Radio`] button.
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
self.text_size = Some(text_size.into().0);
self
}
/// Sets the text font of the [`Radio`] button.
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the style of the [`Radio`] button.
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
where
Message: Clone,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
Row::<(), Renderer>::new()
.width(self.width)
.spacing(self.spacing)
.align_items(Alignment::Center)
.push(Row::new().width(self.size).height(self.size))
.push(Text::new(&self.label).width(self.width).size(
self.text_size.unwrap_or_else(|| renderer.default_size()),
))
.layout(renderer, limits)
}
fn on_event(
&mut self,
_state: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if layout.bounds().contains(cursor_position) {
shell.publish(self.on_click.clone());
return event::Status::Captured;
}
}
_ => {}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
_state: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if layout.bounds().contains(cursor_position) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let mut children = layout.children();
let custom_style = if is_mouse_over {
theme.hovered(&self.style, self.is_selected)
} else {
theme.active(&self.style, self.is_selected)
};
{
let layout = children.next().unwrap();
let bounds = layout.bounds();
let size = bounds.width;
let dot_size = size / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: (size / 2.0).into(),
border_width: custom_style.border_width,
border_color: custom_style.border_color,
},
custom_style.background,
);
if self.is_selected {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + dot_size / 2.0,
y: bounds.y + dot_size / 2.0,
width: bounds.width - dot_size,
height: bounds.height - dot_size,
},
border_radius: (dot_size / 2.0).into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
custom_style.dot_color,
);
}
}
{
let label_layout = children.next().unwrap();
crate::text::draw(
renderer,
style,
label_layout,
&self.label,
self.text_size,
self.font,
crate::text::Appearance {
color: custom_style.text_color,
},
alignment::Horizontal::Left,
alignment::Vertical::Center,
);
}
}
}
impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(radio)
}
}

253
widget/src/row.rs Normal file
View file

@ -0,0 +1,253 @@
//! Distribute content horizontally.
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle,
Shell, Widget,
};
/// A container that distributes its contents horizontally.
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message, Renderer = crate::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
height: Length,
align_items: Alignment,
children: Vec<Element<'a, Message, Renderer>>,
}
impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
/// Creates an empty [`Row`].
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Row`] with the given elements.
pub fn with_children(
children: Vec<Element<'a, Message, Renderer>>,
) -> Self {
Row {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
align_items: Alignment::Start,
children,
}
}
/// Sets the horizontal spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the [`Padding`] of the [`Row`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the width of the [`Row`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Row`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the vertical alignment of the contents of the [`Row`] .
pub fn align_items(mut self, align: Alignment) -> Self {
self.align_items = align;
self
}
/// Adds an [`Element`] to the [`Row`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
self.children.push(child.into());
self
}
}
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> {
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Row<'a, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&self.children)
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
layout::flex::resolve(
layout::flex::Axis::Horizontal,
renderer,
&limits,
self.padding,
self.spacing,
self.align_items,
&self.children,
)
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.operate(state, layout, renderer, operation);
})
});
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor_position,
renderer,
clipboard,
shell,
)
})
.fold(event::Status::Ignored, event::Status::merge)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
state,
layout,
cursor_position,
viewport,
renderer,
)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
{
child.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor_position,
viewport,
);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
overlay::from_children(&mut self.children, tree, layout, renderer)
}
}
impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(row: Row<'a, Message, Renderer>) -> Self {
Self::new(row)
}
}

147
widget/src/rule.rs Normal file
View file

@ -0,0 +1,147 @@
//! Display a horizontal or vertical rule for dividing content.
use crate::core::layout;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
};
pub use crate::style::rule::{Appearance, FillMode, StyleSheet};
/// Display a horizontal or vertical rule for dividing content.
#[allow(missing_debug_implementations)]
pub struct Rule<Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
width: Length,
height: Length,
is_horizontal: bool,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<Renderer> Rule<Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a horizontal [`Rule`] with the given height.
pub fn horizontal(height: impl Into<Pixels>) -> Self {
Rule {
width: Length::Fill,
height: Length::Fixed(height.into().0),
is_horizontal: true,
style: Default::default(),
}
}
/// Creates a vertical [`Rule`] with the given width.
pub fn vertical(width: impl Into<Pixels>) -> Self {
Rule {
width: Length::Fixed(width.into().0),
height: Length::Fill,
is_horizontal: false,
style: Default::default(),
}
}
/// Sets the style of the [`Rule`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
layout::Node::new(limits.resolve(Size::ZERO))
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let style = theme.appearance(&self.style);
let bounds = if self.is_horizontal {
let line_y = (bounds.y + (bounds.height / 2.0)
- (style.width as f32 / 2.0))
.round();
let (offset, line_width) = style.fill_mode.fill(bounds.width);
let line_x = bounds.x + offset;
Rectangle {
x: line_x,
y: line_y,
width: line_width,
height: style.width as f32,
}
} else {
let line_x = (bounds.x + (bounds.width / 2.0)
- (style.width as f32 / 2.0))
.round();
let (offset, line_height) = style.fill_mode.fill(bounds.height);
let line_y = bounds.y + offset;
Rectangle {
x: line_x,
y: line_y,
width: style.width as f32,
height: line_height,
}
};
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: style.radius.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
style.color,
);
}
}
impl<'a, Message, Renderer> From<Rule<Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> {
Element::new(rule)
}
}

1325
widget/src/scrollable.rs Normal file

File diff suppressed because it is too large Load diff

471
widget/src/slider.rs Normal file
View file

@ -0,0 +1,471 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Rectangle, Shell, Size, Widget,
};
use std::ops::RangeInclusive;
pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
/// The [`Slider`] range of numeric values is generic and its step size defaults
/// to 1 unit.
///
/// # Example
/// ```
/// # type Slider<'a, T, Message> =
/// # iced_widget::Slider<'a, Message, T, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
/// SliderChanged(f32),
/// }
///
/// let value = 50.0;
///
/// Slider::new(0.0..=100.0, value, Message::SliderChanged);
/// ```
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
pub struct Slider<'a, T, Message, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
range: RangeInclusive<T>,
step: T,
value: T,
on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>,
width: Length,
height: f32,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default height of a [`Slider`].
pub const DEFAULT_HEIGHT: f32 = 22.0;
/// Creates a new [`Slider`].
///
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`Slider`]
/// * a function that will be called when the [`Slider`] is dragged.
/// It receives the new value of the [`Slider`] and must produce a
/// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
F: 'a + Fn(T) -> Message,
{
let value = if value >= *range.start() {
value
} else {
*range.start()
};
let value = if value <= *range.end() {
value
} else {
*range.end()
};
Slider {
value,
range,
step: T::from(1),
on_change: Box::new(on_change),
on_release: None,
width: Length::Fill,
height: Self::DEFAULT_HEIGHT,
style: Default::default(),
}
}
/// Sets the release message of the [`Slider`].
/// This is called when the mouse is released from the slider.
///
/// Typically, the user's interaction with the slider is finished when this message is produced.
/// This is useful if you need to spawn a long-running task from the slider's result, where
/// the default on_change message could create too many events.
pub fn on_release(mut self, on_release: Message) -> Self {
self.on_release = Some(on_release);
self
}
/// Sets the width of the [`Slider`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Slider`].
pub fn height(mut self, height: impl Into<Pixels>) -> Self {
self.height = height.into().0;
self
}
/// Sets the style of the [`Slider`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
/// Sets the step size of the [`Slider`].
pub fn step(mut self, step: T) -> Self {
self.step = step;
self
}
}
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
for Slider<'a, T, Message, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let size = limits.resolve(Size::ZERO);
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
update(
event,
layout,
cursor_position,
shell,
tree.state.downcast_mut::<State>(),
&mut self.value,
&self.range,
self.step,
self.on_change.as_ref(),
&self.on_release,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
draw(
renderer,
layout,
cursor_position,
tree.state.downcast_ref::<State>(),
self.value,
&self.range,
theme,
&self.style,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(
layout,
cursor_position,
tree.state.downcast_ref::<State>(),
)
}
}
impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
Message: 'a + Clone,
Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
slider: Slider<'a, T, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(slider)
}
}
/// Processes an [`Event`] and updates the [`State`] of a [`Slider`]
/// accordingly.
pub fn update<Message, T>(
event: Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
range: &RangeInclusive<T>,
step: T,
on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>,
) -> event::Status
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
{
let is_dragging = state.is_dragging;
let mut change = || {
let bounds = layout.bounds();
let new_value = if cursor_position.x <= bounds.x {
*range.start()
} else if cursor_position.x >= bounds.x + bounds.width {
*range.end()
} else {
let step = step.into();
let start = (*range.start()).into();
let end = (*range.end()).into();
let percent = f64::from(cursor_position.x - bounds.x)
/ f64::from(bounds.width);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
if let Some(value) = T::from_f64(value) {
value
} else {
return;
}
};
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
*value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if layout.bounds().contains(cursor_position) {
change();
state.is_dragging = true;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
change();
return event::Status::Captured;
}
}
_ => {}
}
event::Status::Ignored
}
/// Draws a [`Slider`].
pub fn draw<T, R>(
renderer: &mut R,
layout: Layout<'_>,
cursor_position: Point,
state: &State,
value: T,
range: &RangeInclusive<T>,
style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
style: &<R::Theme as StyleSheet>::Style,
) where
T: Into<f64> + Copy,
R: crate::core::Renderer,
R::Theme: StyleSheet,
{
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let style = if state.is_dragging {
style_sheet.dragging(style)
} else if is_mouse_over {
style_sheet.hovered(style)
} else {
style_sheet.active(style)
};
let rail_y = bounds.y + (bounds.height / 2.0).round();
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: rail_y - 1.0,
width: bounds.width,
height: 2.0,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
style.rail_colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: rail_y + 1.0,
width: bounds.width,
height: 2.0,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Background::Color(style.rail_colors.1),
);
let (handle_width, handle_height, handle_border_radius) = match style
.handle
.shape
{
HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.height, border_radius),
};
let value = value.into() as f32;
let (range_start, range_end) = {
let (start, end) = range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let handle_offset = if range_start >= range_end {
0.0
} else {
(bounds.width - handle_width) * (value - range_start)
/ (range_end - range_start)
};
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + handle_offset.round(),
y: rail_y - handle_height / 2.0,
width: handle_width,
height: handle_height,
},
border_radius: handle_border_radius.into(),
border_width: style.handle.border_width,
border_color: style.handle.border_color,
},
style.handle.color,
);
}
/// Computes the current [`mouse::Interaction`] of a [`Slider`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor_position: Point,
state: &State,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
if state.is_dragging {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
}
/// The local state of a [`Slider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_dragging: bool,
}
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State::default()
}
}

86
widget/src/space.rs Normal file
View file

@ -0,0 +1,86 @@
//! Distribute content vertically.
use crate::core;
use crate::core::layout;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{Element, Layout, Length, Point, Rectangle, Size, Widget};
/// An amount of empty space.
///
/// It can be useful if you want to fill some space with nothing.
#[derive(Debug)]
pub struct Space {
width: Length,
height: Length,
}
impl Space {
/// Creates an amount of empty [`Space`] with the given width and height.
pub fn new(width: impl Into<Length>, height: impl Into<Length>) -> Self {
Space {
width: width.into(),
height: height.into(),
}
}
/// Creates an amount of horizontal [`Space`].
pub fn with_width(width: impl Into<Length>) -> Self {
Space {
width: width.into(),
height: Length::Shrink,
}
}
/// Creates an amount of vertical [`Space`].
pub fn with_height(height: impl Into<Length>) -> Self {
Space {
width: Length::Shrink,
height: height.into(),
}
}
}
impl<Message, Renderer> Widget<Message, Renderer> for Space
where
Renderer: core::Renderer,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
layout::Node::new(limits.resolve(Size::ZERO))
}
fn draw(
&self,
_state: &Tree,
_renderer: &mut Renderer,
_theme: &Renderer::Theme,
_style: &renderer::Style,
_layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
}
}
impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer>
where
Renderer: core::Renderer,
Message: 'a,
{
fn from(space: Space) -> Element<'a, Message, Renderer> {
Element::new(space)
}
}

195
widget/src/svg.rs Normal file
View file

@ -0,0 +1,195 @@
//! Display vector graphics in your application.
use crate::core::layout;
use crate::core::renderer;
use crate::core::svg;
use crate::core::widget::Tree;
use crate::core::{
ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
};
use std::path::PathBuf;
pub use crate::style::svg::{Appearance, StyleSheet};
pub use svg::Handle;
/// A vector graphics image.
///
/// An [`Svg`] image resizes smoothly without losing any quality.
///
/// [`Svg`] images can have a considerable rendering cost when resized,
/// specially when they are complex.
#[allow(missing_debug_implementations)]
pub struct Svg<Renderer = crate::Renderer>
where
Renderer: svg::Renderer,
Renderer::Theme: StyleSheet,
{
handle: Handle,
width: Length,
height: Length,
content_fit: ContentFit,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<Renderer> Svg<Renderer>
where
Renderer: svg::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a new [`Svg`] from the given [`Handle`].
pub fn new(handle: impl Into<Handle>) -> Self {
Svg {
handle: handle.into(),
width: Length::Fill,
height: Length::Shrink,
content_fit: ContentFit::Contain,
style: Default::default(),
}
}
/// Creates a new [`Svg`] that will display the contents of the file at the
/// provided path.
#[must_use]
pub fn from_path(path: impl Into<PathBuf>) -> Self {
Self::new(Handle::from_path(path))
}
/// Sets the width of the [`Svg`].
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Svg`].
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the [`ContentFit`] of the [`Svg`].
///
/// Defaults to [`ContentFit::Contain`]
#[must_use]
pub fn content_fit(self, content_fit: ContentFit) -> Self {
Self {
content_fit,
..self
}
}
/// Sets the style variant of this [`Svg`].
#[must_use]
pub fn style(
mut self,
style: <Renderer::Theme as StyleSheet>::Style,
) -> Self {
self.style = style;
self
}
}
impl<Message, Renderer> Widget<Message, Renderer> for Svg<Renderer>
where
Renderer: svg::Renderer,
Renderer::Theme: iced_style::svg::StyleSheet,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
// The raw w/h of the underlying image
let Size { width, height } = renderer.dimensions(&self.handle);
let image_size = Size::new(width as f32, height as f32);
// The size to be available to the widget prior to `Shrink`ing
let raw_size = limits
.width(self.width)
.height(self.height)
.resolve(image_size);
// The uncropped size of the image when fit to the bounds above
let full_size = self.content_fit.fit(image_size, raw_size);
// Shrink the widget to fit the resized image, if requested
let final_size = Size {
width: match self.width {
Length::Shrink => f32::min(raw_size.width, full_size.width),
_ => raw_size.width,
},
height: match self.height {
Length::Shrink => f32::min(raw_size.height, full_size.height),
_ => raw_size.height,
},
};
layout::Node::new(final_size)
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
_viewport: &Rectangle,
) {
let Size { width, height } = renderer.dimensions(&self.handle);
let image_size = Size::new(width as f32, height as f32);
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
let render = |renderer: &mut Renderer| {
let offset = Vector::new(
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
);
let drawing_bounds = Rectangle {
width: adjusted_fit.width,
height: adjusted_fit.height,
..bounds
};
let appearance = theme.appearance(&self.style);
renderer.draw(
self.handle.clone(),
appearance.color,
drawing_bounds + offset,
);
};
if adjusted_fit.width > bounds.width
|| adjusted_fit.height > bounds.height
{
renderer.with_layer(bounds, render);
} else {
render(renderer);
}
}
}
impl<'a, Message, Renderer> From<Svg<Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: svg::Renderer + 'a,
Renderer::Theme: iced_style::svg::StyleSheet,
{
fn from(icon: Svg<Renderer>) -> Element<'a, Message, Renderer> {
Element::new(icon)
}
}

4
widget/src/text.rs Normal file
View file

@ -0,0 +1,4 @@
pub use crate::core::widget::text::*;
pub type Text<'a, Renderer = crate::Renderer> =
crate::core::widget::Text<'a, Renderer>;

1221
widget/src/text_input.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,189 @@
//! Track the cursor of a text input.
use crate::text_input::Value;
/// The cursor of a text input.
#[derive(Debug, Copy, Clone)]
pub struct Cursor {
state: State,
}
/// The state of a [`Cursor`].
#[derive(Debug, Copy, Clone)]
pub enum State {
/// Cursor without a selection
Index(usize),
/// Cursor selecting a range of text
Selection {
/// The start of the selection
start: usize,
/// The end of the selection
end: usize,
},
}
impl Default for Cursor {
fn default() -> Self {
Cursor {
state: State::Index(0),
}
}
}
impl Cursor {
/// Returns the [`State`] of the [`Cursor`].
pub fn state(&self, value: &Value) -> State {
match self.state {
State::Index(index) => State::Index(index.min(value.len())),
State::Selection { start, end } => {
let start = start.min(value.len());
let end = end.min(value.len());
if start == end {
State::Index(start)
} else {
State::Selection { start, end }
}
}
}
}
/// Returns the current selection of the [`Cursor`] for the given [`Value`].
///
/// `start` is guaranteed to be <= than `end`.
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
match self.state(value) {
State::Selection { start, end } => {
Some((start.min(end), start.max(end)))
}
_ => None,
}
}
pub(crate) fn move_to(&mut self, position: usize) {
self.state = State::Index(position);
}
pub(crate) fn move_right(&mut self, value: &Value) {
self.move_right_by_amount(value, 1)
}
pub(crate) fn move_right_by_words(&mut self, value: &Value) {
self.move_to(value.next_end_of_word(self.right(value)))
}
pub(crate) fn move_right_by_amount(
&mut self,
value: &Value,
amount: usize,
) {
match self.state(value) {
State::Index(index) => {
self.move_to(index.saturating_add(amount).min(value.len()))
}
State::Selection { start, end } => self.move_to(end.max(start)),
}
}
pub(crate) fn move_left(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) if index > 0 => self.move_to(index - 1),
State::Selection { start, end } => self.move_to(start.min(end)),
_ => self.move_to(0),
}
}
pub(crate) fn move_left_by_words(&mut self, value: &Value) {
self.move_to(value.previous_start_of_word(self.left(value)));
}
pub(crate) fn select_range(&mut self, start: usize, end: usize) {
if start == end {
self.state = State::Index(start);
} else {
self.state = State::Selection { start, end };
}
}
pub(crate) fn select_left(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) if index > 0 => {
self.select_range(index, index - 1)
}
State::Selection { start, end } if end > 0 => {
self.select_range(start, end - 1)
}
_ => {}
}
}
pub(crate) fn select_right(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) if index < value.len() => {
self.select_range(index, index + 1)
}
State::Selection { start, end } if end < value.len() => {
self.select_range(start, end + 1)
}
_ => {}
}
}
pub(crate) fn select_left_by_words(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) => {
self.select_range(index, value.previous_start_of_word(index))
}
State::Selection { start, end } => {
self.select_range(start, value.previous_start_of_word(end))
}
}
}
pub(crate) fn select_right_by_words(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) => {
self.select_range(index, value.next_end_of_word(index))
}
State::Selection { start, end } => {
self.select_range(start, value.next_end_of_word(end))
}
}
}
pub(crate) fn select_all(&mut self, value: &Value) {
self.select_range(0, value.len());
}
pub(crate) fn start(&self, value: &Value) -> usize {
let start = match self.state {
State::Index(index) => index,
State::Selection { start, .. } => start,
};
start.min(value.len())
}
pub(crate) fn end(&self, value: &Value) -> usize {
let end = match self.state {
State::Index(index) => index,
State::Selection { end, .. } => end,
};
end.min(value.len())
}
fn left(&self, value: &Value) -> usize {
match self.state(value) {
State::Index(index) => index,
State::Selection { start, end } => start.min(end),
}
}
fn right(&self, value: &Value) -> usize {
match self.state(value) {
State::Index(index) => index,
State::Selection { start, end } => start.max(end),
}
}
}

View file

@ -0,0 +1,70 @@
use crate::text_input::{Cursor, Value};
pub struct Editor<'a> {
value: &'a mut Value,
cursor: &'a mut Cursor,
}
impl<'a> Editor<'a> {
pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
Editor { value, cursor }
}
pub fn contents(&self) -> String {
self.value.to_string()
}
pub fn insert(&mut self, character: char) {
if let Some((left, right)) = self.cursor.selection(self.value) {
self.cursor.move_left(self.value);
self.value.remove_many(left, right);
}
self.value.insert(self.cursor.end(self.value), character);
self.cursor.move_right(self.value);
}
pub fn paste(&mut self, content: Value) {
let length = content.len();
if let Some((left, right)) = self.cursor.selection(self.value) {
self.cursor.move_left(self.value);
self.value.remove_many(left, right);
}
self.value.insert_many(self.cursor.end(self.value), content);
self.cursor.move_right_by_amount(self.value, length);
}
pub fn backspace(&mut self) {
match self.cursor.selection(self.value) {
Some((start, end)) => {
self.cursor.move_left(self.value);
self.value.remove_many(start, end);
}
None => {
let start = self.cursor.start(self.value);
if start > 0 {
self.cursor.move_left(self.value);
self.value.remove(start - 1);
}
}
}
}
pub fn delete(&mut self) {
match self.cursor.selection(self.value) {
Some(_) => {
self.backspace();
}
None => {
let end = self.cursor.end(self.value);
if end < self.value.len() {
self.value.remove(end);
}
}
}
}
}

View file

@ -0,0 +1,133 @@
use unicode_segmentation::UnicodeSegmentation;
/// The value of a [`TextInput`].
///
/// [`TextInput`]: crate::widget::TextInput
// TODO: Reduce allocations, cache results (?)
#[derive(Debug, Clone)]
pub struct Value {
graphemes: Vec<String>,
}
impl Value {
/// Creates a new [`Value`] from a string slice.
pub fn new(string: &str) -> Self {
let graphemes = UnicodeSegmentation::graphemes(string, true)
.map(String::from)
.collect();
Self { graphemes }
}
/// Returns whether the [`Value`] is empty or not.
///
/// A [`Value`] is empty when it contains no graphemes.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the total amount of graphemes in the [`Value`].
pub fn len(&self) -> usize {
self.graphemes.len()
}
/// Returns the position of the previous start of a word from the given
/// grapheme `index`.
pub fn previous_start_of_word(&self, index: usize) -> usize {
let previous_string =
&self.graphemes[..index.min(self.graphemes.len())].concat();
UnicodeSegmentation::split_word_bound_indices(previous_string as &str)
.filter(|(_, word)| !word.trim_start().is_empty())
.next_back()
.map(|(i, previous_word)| {
index
- UnicodeSegmentation::graphemes(previous_word, true)
.count()
- UnicodeSegmentation::graphemes(
&previous_string[i + previous_word.len()..] as &str,
true,
)
.count()
})
.unwrap_or(0)
}
/// Returns the position of the next end of a word from the given grapheme
/// `index`.
pub fn next_end_of_word(&self, index: usize) -> usize {
let next_string = &self.graphemes[index..].concat();
UnicodeSegmentation::split_word_bound_indices(next_string as &str)
.find(|(_, word)| !word.trim_start().is_empty())
.map(|(i, next_word)| {
index
+ UnicodeSegmentation::graphemes(next_word, true).count()
+ UnicodeSegmentation::graphemes(
&next_string[..i] as &str,
true,
)
.count()
})
.unwrap_or(self.len())
}
/// Returns a new [`Value`] containing the graphemes from `start` until the
/// given `end`.
pub fn select(&self, start: usize, end: usize) -> Self {
let graphemes =
self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
Self { graphemes }
}
/// Returns a new [`Value`] containing the graphemes until the given
/// `index`.
pub fn until(&self, index: usize) -> Self {
let graphemes = self.graphemes[..index.min(self.len())].to_vec();
Self { graphemes }
}
/// Converts the [`Value`] into a `String`.
pub fn to_string(&self) -> String {
self.graphemes.concat()
}
/// Inserts a new `char` at the given grapheme `index`.
pub fn insert(&mut self, index: usize, c: char) {
self.graphemes.insert(index, c.to_string());
self.graphemes =
UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
.map(String::from)
.collect();
}
/// Inserts a bunch of graphemes at the given grapheme `index`.
pub fn insert_many(&mut self, index: usize, mut value: Value) {
let _ = self
.graphemes
.splice(index..index, value.graphemes.drain(..));
}
/// Removes the grapheme at the given `index`.
pub fn remove(&mut self, index: usize) {
let _ = self.graphemes.remove(index);
}
/// Removes the graphemes from `start` to `end`.
pub fn remove_many(&mut self, start: usize, end: usize) {
let _ = self.graphemes.splice(start..end, std::iter::empty());
}
/// Returns a new [`Value`] with all its graphemes replaced with the
/// dot ('•') character.
pub fn secure(&self) -> Self {
Self {
graphemes: std::iter::repeat(String::from(""))
.take(self.graphemes.len())
.collect(),
}
}
}

326
widget/src/toggler.rs Normal file
View file

@ -0,0 +1,326 @@
//! Show toggle controls using togglers.
use crate::core::alignment;
use crate::core::event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::widget::Tree;
use crate::core::{
Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point,
Rectangle, Shell, Widget,
};
use crate::{Row, Text};
pub use crate::style::toggler::{Appearance, StyleSheet};
/// A toggler widget.
///
/// # Example
///
/// ```
/// # type Toggler<'a, Message> =
/// # iced_widget::Toggler<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// pub enum Message {
/// TogglerToggled(bool),
/// }
///
/// let is_toggled = true;
///
/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
/// ```
#[allow(missing_debug_implementations)]
pub struct Toggler<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
is_toggled: bool,
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
label: Option<String>,
width: Length,
size: f32,
text_size: Option<f32>,
text_alignment: alignment::Horizontal,
spacing: f32,
font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default size of a [`Toggler`].
pub const DEFAULT_SIZE: f32 = 20.0;
/// Creates a new [`Toggler`].
///
/// It expects:
/// * a boolean describing whether the [`Toggler`] is checked or not
/// * An optional label for the [`Toggler`]
/// * a function that will be called when the [`Toggler`] is toggled. It
/// will receive the new state of the [`Toggler`] and must produce a
/// `Message`.
pub fn new<F>(
label: impl Into<Option<String>>,
is_toggled: bool,
f: F,
) -> Self
where
F: 'a + Fn(bool) -> Message,
{
Toggler {
is_toggled,
on_toggle: Box::new(f),
label: label.into(),
width: Length::Fill,
size: Self::DEFAULT_SIZE,
text_size: None,
text_alignment: alignment::Horizontal::Left,
spacing: 0.0,
font: None,
style: Default::default(),
}
}
/// Sets the size of the [`Toggler`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = size.into().0;
self
}
/// Sets the width of the [`Toggler`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the text size o the [`Toggler`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
self.text_size = Some(text_size.into().0);
self
}
/// Sets the horizontal alignment of the text of the [`Toggler`]
pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
self.text_alignment = alignment;
self
}
/// Sets the spacing between the [`Toggler`] and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
self
}
/// Sets the [`Font`] of the text of the [`Toggler`]
///
/// [`Font`]: crate::text::Renderer::Font
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the style of the [`Toggler`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Toggler<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let mut row = Row::<(), Renderer>::new()
.width(self.width)
.spacing(self.spacing)
.align_items(Alignment::Center);
if let Some(label) = &self.label {
row = row.push(
Text::new(label)
.horizontal_alignment(self.text_alignment)
.font(self.font.unwrap_or_else(|| renderer.default_font()))
.width(self.width)
.size(
self.text_size
.unwrap_or_else(|| renderer.default_size()),
),
);
}
row = row.push(Row::new().width(2.0 * self.size).height(self.size));
row.layout(renderer, limits)
}
fn on_event(
&mut self,
_state: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let mouse_over = layout.bounds().contains(cursor_position);
if mouse_over {
shell.publish((self.on_toggle)(!self.is_toggled));
event::Status::Captured
} else {
event::Status::Ignored
}
}
_ => event::Status::Ignored,
}
}
fn mouse_interaction(
&self,
_state: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if layout.bounds().contains(cursor_position) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
/// Makes sure that the border radius of the toggler looks good at every size.
const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
/// The space ratio between the background Quad and the Toggler bounds, and
/// between the background Quad and foreground Quad.
const SPACE_RATIO: f32 = 0.05;
let mut children = layout.children();
if let Some(label) = &self.label {
let label_layout = children.next().unwrap();
crate::text::draw(
renderer,
style,
label_layout,
label,
self.text_size,
self.font,
Default::default(),
self.text_alignment,
alignment::Vertical::Center,
);
}
let toggler_layout = children.next().unwrap();
let bounds = toggler_layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let style = if is_mouse_over {
theme.hovered(&self.style, self.is_toggled)
} else {
theme.active(&self.style, self.is_toggled)
};
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
let space = SPACE_RATIO * bounds.height;
let toggler_background_bounds = Rectangle {
x: bounds.x + space,
y: bounds.y + space,
width: bounds.width - (2.0 * space),
height: bounds.height - (2.0 * space),
};
renderer.fill_quad(
renderer::Quad {
bounds: toggler_background_bounds,
border_radius: border_radius.into(),
border_width: 1.0,
border_color: style
.background_border
.unwrap_or(style.background),
},
style.background,
);
let toggler_foreground_bounds = Rectangle {
x: bounds.x
+ if self.is_toggled {
bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
} else {
2.0 * space
},
y: bounds.y + (2.0 * space),
width: bounds.height - (4.0 * space),
height: bounds.height - (4.0 * space),
};
renderer.fill_quad(
renderer::Quad {
bounds: toggler_foreground_bounds,
border_radius: border_radius.into(),
border_width: 1.0,
border_color: style
.foreground_border
.unwrap_or(style.foreground),
},
style.foreground,
);
}
}
impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn from(
toggler: Toggler<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(toggler)
}
}

388
widget/src/tooltip.rs Normal file
View file

@ -0,0 +1,388 @@
//! Display a widget over another.
use crate::container;
use crate::core;
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::text;
use crate::core::widget::Tree;
use crate::core::{
Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
Vector, Widget,
};
use crate::Text;
use std::borrow::Cow;
/// An element to display a widget over another.
#[allow(missing_debug_implementations)]
pub struct Tooltip<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
content: Element<'a, Message, Renderer>,
tooltip: Text<'a, Renderer>,
position: Position,
gap: f32,
padding: f32,
snap_within_viewport: bool,
style: <Renderer::Theme as container::StyleSheet>::Style,
}
impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
/// The default padding of a [`Tooltip`] drawn by this renderer.
const DEFAULT_PADDING: f32 = 5.0;
/// Creates a new [`Tooltip`].
///
/// [`Tooltip`]: struct.Tooltip.html
pub fn new(
content: impl Into<Element<'a, Message, Renderer>>,
tooltip: impl Into<Cow<'a, str>>,
position: Position,
) -> Self {
Tooltip {
content: content.into(),
tooltip: Text::new(tooltip),
position,
gap: 0.0,
padding: Self::DEFAULT_PADDING,
snap_within_viewport: true,
style: Default::default(),
}
}
/// Sets the size of the text of the [`Tooltip`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.tooltip = self.tooltip.size(size);
self
}
/// Sets the font of the [`Tooltip`].
///
/// [`Font`]: Renderer::Font
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.tooltip = self.tooltip.font(font);
self
}
/// Sets the gap between the content and its [`Tooltip`].
pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
self.gap = gap.into().0;
self
}
/// Sets the padding of the [`Tooltip`].
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
self.padding = padding.into().0;
self
}
/// Sets whether the [`Tooltip`] is snapped within the viewport.
pub fn snap_within_viewport(mut self, snap: bool) -> Self {
self.snap_within_viewport = snap;
self
}
/// Sets the style of the [`Tooltip`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Tooltip<'a, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
fn width(&self) -> Length {
self.content.as_widget().width()
}
fn height(&self) -> Length {
self.content.as_widget().height()
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget().layout(renderer, limits)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor_position,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
inherited_style,
layout,
cursor_position,
viewport,
);
let tooltip = &self.tooltip;
draw(
renderer,
theme,
inherited_style,
layout,
cursor_position,
viewport,
self.position,
self.gap,
self.padding,
self.snap_within_viewport,
&self.style,
|renderer, limits| {
Widget::<(), Renderer>::layout(tooltip, renderer, limits)
},
|renderer, defaults, layout, cursor_position, viewport| {
Widget::<(), Renderer>::draw(
tooltip,
&Tree::empty(),
renderer,
theme,
defaults,
layout,
cursor_position,
viewport,
);
},
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
)
}
}
impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
Renderer: 'a + text::Renderer,
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
fn from(
tooltip: Tooltip<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(tooltip)
}
}
/// The position of the tooltip. Defaults to following the cursor.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Position {
/// The tooltip will follow the cursor.
FollowCursor,
/// The tooltip will appear on the top of the widget.
Top,
/// The tooltip will appear on the bottom of the widget.
Bottom,
/// The tooltip will appear on the left of the widget.
Left,
/// The tooltip will appear on the right of the widget.
Right,
}
/// Draws a [`Tooltip`].
pub fn draw<Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
position: Position,
gap: f32,
padding: f32,
snap_within_viewport: bool,
style: &<Renderer::Theme as container::StyleSheet>::Style,
layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
draw_text: impl FnOnce(
&mut Renderer,
&renderer::Style,
Layout<'_>,
Point,
&Rectangle,
),
) where
Renderer: core::Renderer,
Renderer::Theme: container::StyleSheet,
{
use container::StyleSheet;
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
let style = theme.appearance(style);
let defaults = renderer::Style {
text_color: style.text_color.unwrap_or(inherited_style.text_color),
};
let text_layout = layout_text(
renderer,
&layout::Limits::new(
Size::ZERO,
snap_within_viewport
.then(|| viewport.size())
.unwrap_or(Size::INFINITY),
)
.pad(Padding::new(padding)),
);
let text_bounds = text_layout.bounds();
let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0;
let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0;
let mut tooltip_bounds = {
let offset = match position {
Position::Top => Vector::new(
x_center,
bounds.y - text_bounds.height - gap - padding,
),
Position::Bottom => Vector::new(
x_center,
bounds.y + bounds.height + gap + padding,
),
Position::Left => Vector::new(
bounds.x - text_bounds.width - gap - padding,
y_center,
),
Position::Right => Vector::new(
bounds.x + bounds.width + gap + padding,
y_center,
),
Position::FollowCursor => Vector::new(
cursor_position.x,
cursor_position.y - text_bounds.height,
),
};
Rectangle {
x: offset.x - padding,
y: offset.y - padding,
width: text_bounds.width + padding * 2.0,
height: text_bounds.height + padding * 2.0,
}
};
if snap_within_viewport {
if tooltip_bounds.x < viewport.x {
tooltip_bounds.x = viewport.x;
} else if viewport.x + viewport.width
< tooltip_bounds.x + tooltip_bounds.width
{
tooltip_bounds.x =
viewport.x + viewport.width - tooltip_bounds.width;
}
if tooltip_bounds.y < viewport.y {
tooltip_bounds.y = viewport.y;
} else if viewport.y + viewport.height
< tooltip_bounds.y + tooltip_bounds.height
{
tooltip_bounds.y =
viewport.y + viewport.height - tooltip_bounds.height;
}
}
renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| {
container::draw_background(renderer, &style, tooltip_bounds);
draw_text(
renderer,
&defaults,
Layout::with_offset(
Vector::new(
tooltip_bounds.x + padding,
tooltip_bounds.y + padding,
),
&text_layout,
),
cursor_position,
viewport,
)
});
}
}

View file

@ -0,0 +1,471 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`VerticalSlider`] has some local [`State`].
use std::ops::RangeInclusive;
pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet};
use crate::core;
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Background, Clipboard, Color, Element, Length, Pixels, Point, Rectangle,
Shell, Size, Widget,
};
/// An vertical bar and a handle that selects a single value from a range of
/// values.
///
/// A [`VerticalSlider`] will try to fill the vertical space of its container.
///
/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
/// to 1 unit.
///
/// # Example
/// ```
/// # type VerticalSlider<'a, T, Message> =
/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
/// SliderChanged(f32),
/// }
///
/// let value = 50.0;
///
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
/// ```
#[allow(missing_debug_implementations)]
pub struct VerticalSlider<'a, T, Message, Renderer = crate::Renderer>
where
Renderer: core::Renderer,
Renderer::Theme: StyleSheet,
{
range: RangeInclusive<T>,
step: T,
value: T,
on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>,
width: f32,
height: Length,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
Renderer: core::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default width of a [`VerticalSlider`].
pub const DEFAULT_WIDTH: f32 = 22.0;
/// Creates a new [`VerticalSlider`].
///
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`VerticalSlider`]
/// * a function that will be called when the [`VerticalSlider`] is dragged.
/// It receives the new value of the [`VerticalSlider`] and must produce a
/// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
F: 'a + Fn(T) -> Message,
{
let value = if value >= *range.start() {
value
} else {
*range.start()
};
let value = if value <= *range.end() {
value
} else {
*range.end()
};
VerticalSlider {
value,
range,
step: T::from(1),
on_change: Box::new(on_change),
on_release: None,
width: Self::DEFAULT_WIDTH,
height: Length::Fill,
style: Default::default(),
}
}
/// Sets the release message of the [`VerticalSlider`].
/// This is called when the mouse is released from the slider.
///
/// Typically, the user's interaction with the slider is finished when this message is produced.
/// This is useful if you need to spawn a long-running task from the slider's result, where
/// the default on_change message could create too many events.
pub fn on_release(mut self, on_release: Message) -> Self {
self.on_release = Some(on_release);
self
}
/// Sets the width of the [`VerticalSlider`].
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = width.into().0;
self
}
/// Sets the height of the [`VerticalSlider`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the style of the [`VerticalSlider`].
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
/// Sets the step size of the [`VerticalSlider`].
pub fn step(mut self, step: T) -> Self {
self.step = step;
self
}
}
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
for VerticalSlider<'a, T, Message, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
Renderer: core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn width(&self) -> Length {
Length::Shrink
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let size = limits.resolve(Size::ZERO);
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
update(
event,
layout,
cursor_position,
shell,
tree.state.downcast_mut::<State>(),
&mut self.value,
&self.range,
self.step,
self.on_change.as_ref(),
&self.on_release,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
draw(
renderer,
layout,
cursor_position,
tree.state.downcast_ref::<State>(),
self.value,
&self.range,
theme,
&self.style,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(
layout,
cursor_position,
tree.state.downcast_ref::<State>(),
)
}
}
impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
Message: 'a + Clone,
Renderer: 'a + core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
slider: VerticalSlider<'a, T, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(slider)
}
}
/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`]
/// accordingly.
pub fn update<Message, T>(
event: Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
range: &RangeInclusive<T>,
step: T,
on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>,
) -> event::Status
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
{
let is_dragging = state.is_dragging;
let mut change = || {
let bounds = layout.bounds();
let new_value = if cursor_position.y >= bounds.y + bounds.height {
*range.start()
} else if cursor_position.y <= bounds.y {
*range.end()
} else {
let step = step.into();
let start = (*range.start()).into();
let end = (*range.end()).into();
let percent = 1.0
- f64::from(cursor_position.y - bounds.y)
/ f64::from(bounds.height);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
if let Some(value) = T::from_f64(value) {
value
} else {
return;
}
};
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
*value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if layout.bounds().contains(cursor_position) {
change();
state.is_dragging = true;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
change();
return event::Status::Captured;
}
}
_ => {}
}
event::Status::Ignored
}
/// Draws a [`VerticalSlider`].
pub fn draw<T, R>(
renderer: &mut R,
layout: Layout<'_>,
cursor_position: Point,
state: &State,
value: T,
range: &RangeInclusive<T>,
style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>,
style: &<R::Theme as StyleSheet>::Style,
) where
T: Into<f64> + Copy,
R: core::Renderer,
R::Theme: StyleSheet,
{
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let style = if state.is_dragging {
style_sheet.dragging(style)
} else if is_mouse_over {
style_sheet.hovered(style)
} else {
style_sheet.active(style)
};
let rail_x = bounds.x + (bounds.width / 2.0).round();
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - 1.0,
y: bounds.y,
width: 2.0,
height: bounds.height,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
style.rail_colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x + 1.0,
y: bounds.y,
width: 2.0,
height: bounds.height,
},
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Background::Color(style.rail_colors.1),
);
let (handle_width, handle_height, handle_border_radius) = match style
.handle
.shape
{
HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius),
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.width, border_radius),
};
let value = value.into() as f32;
let (range_start, range_end) = {
let (start, end) = range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let handle_offset = if range_start >= range_end {
0.0
} else {
(bounds.height - handle_width) * (value - range_end)
/ (range_start - range_end)
};
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - (handle_height / 2.0),
y: bounds.y + handle_offset.round(),
width: handle_height,
height: handle_width,
},
border_radius: handle_border_radius.into(),
border_width: style.handle.border_width,
border_color: style.handle.border_color,
},
style.handle.color,
);
}
/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor_position: Point,
state: &State,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
if state.is_dragging {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
}
/// The local state of a [`VerticalSlider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_dragging: bool,
}
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State::default()
}
}