Integrate diagnostic decorations and current-source Text input boundaries

This commit is contained in:
2026-09-13 04:47:30 -07:00
parent 16904398db
commit 92ff99eb54
98 changed files with 35959 additions and 208 deletions
+85
View File
@@ -0,0 +1,85 @@
use crate::core::event;
use crate::core::time::Instant;
use crate::core::window;
/// A runtime action that can be performed by some widgets.
#[derive(Debug, Clone)]
pub struct Action<Message> {
message_to_publish: Option<Message>,
redraw_request: window::RedrawRequest,
event_status: event::Status,
}
impl<Message> Action<Message> {
fn new() -> Self {
Self {
message_to_publish: None,
redraw_request: window::RedrawRequest::Wait,
event_status: event::Status::Ignored,
}
}
/// Creates a new "capturing" [`Action`]. A capturing [`Action`]
/// will make other widgets consider it final and prevent further
/// processing.
///
/// Prevents "event bubbling".
pub fn capture() -> Self {
Self {
event_status: event::Status::Captured,
..Self::new()
}
}
/// Creates a new [`Action`] that publishes the given `Message` for
/// the application to handle.
///
/// Publishing a `Message` always produces a redraw.
pub fn publish(message: Message) -> Self {
Self {
message_to_publish: Some(message),
..Self::new()
}
}
/// Creates a new [`Action`] that requests a redraw to happen as
/// soon as possible; without publishing any `Message`.
pub fn request_redraw() -> Self {
Self {
redraw_request: window::RedrawRequest::NextFrame,
..Self::new()
}
}
/// Creates a new [`Action`] that requests a redraw to happen at
/// the given [`Instant`]; without publishing any `Message`.
///
/// This can be useful to efficiently animate content, like a
/// blinking caret on a text input.
pub fn request_redraw_at(at: Instant) -> Self {
Self {
redraw_request: window::RedrawRequest::At(at),
..Self::new()
}
}
/// Marks the [`Action`] as "capturing". See [`Self::capture`].
pub fn and_capture(mut self) -> Self {
self.event_status = event::Status::Captured;
self
}
/// Converts the [`Action`] into its internal parts.
///
/// This method is meant to be used by runtimes, libraries, or internal
/// widget implementations.
pub fn into_inner(
self,
) -> (Option<Message>, window::RedrawRequest, event::Status) {
(
self.message_to_publish,
self.redraw_request,
self.event_status,
)
}
}
+752
View File
@@ -0,0 +1,752 @@
//! Buttons allow your users to perform actions by pressing them.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::button;
//!
//! #[derive(Clone)]
//! enum Message {
//! ButtonPressed,
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! button("Press me!").on_press(Message::ButtonPressed).into()
//! }
//! ```
use crate::core::border::{self, Border};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::theme::palette;
use crate::core::touch;
use crate::core::widget::Operation;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Clipboard, Color, Element, Event, Layout, Length, Padding,
Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
};
/// A generic widget that produces a message when pressed.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::button;
///
/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// button("Press me!").on_press(Message::ButtonPressed).into()
/// }
/// ```
///
/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
/// be disabled:
///
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::button;
///
/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// button("I am disabled!").into()
/// }
/// ```
pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
Theme: Catalog,
{
content: Element<'a, Message, Theme, Renderer>,
on_press: Option<OnPress<'a, Message>>,
width: Length,
height: Length,
padding: Padding,
clip: bool,
class: Theme::Class<'a>,
status: Option<Status>,
}
enum OnPress<'a, Message> {
Direct(Message),
Closure(Box<dyn Fn() -> Message + 'a>),
}
impl<Message: Clone> OnPress<'_, Message> {
fn get(&self) -> Message {
match self {
OnPress::Direct(message) => message.clone(),
OnPress::Closure(f) => f(),
}
}
}
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
Theme: Catalog,
{
/// Creates a new [`Button`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let content = content.into();
let size = content.as_widget().size_hint();
Button {
content,
on_press: None,
width: size.width.fluid(),
height: size.height.fluid(),
padding: DEFAULT_PADDING,
clip: false,
class: Theme::default(),
status: None,
}
}
/// Sets the width of the [`Button`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Button`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the [`Padding`] of the [`Button`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(OnPress::Direct(on_press));
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// This is analogous to [`Button::on_press`], but using a closure to produce
/// the message.
///
/// This closure will only be called when the [`Button`] is actually pressed and,
/// therefore, this method is useful to reduce overhead if creating the resulting
/// message is slow.
pub fn on_press_with(
mut self,
on_press: impl Fn() -> Message + 'a,
) -> Self {
self.on_press = Some(OnPress::Closure(Box::new(on_press)));
self
}
/// Sets the message that will be produced when the [`Button`] is pressed,
/// if `Some`.
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press.map(OnPress::Direct);
self
}
/// Sets whether the contents of the [`Button`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Sets the style of the [`Button`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Button`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct State {
is_pressed: bool,
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Button<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + crate::core::Renderer,
Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::padded(
limits,
self.width,
self.height,
self.padding,
|limits| {
self.content.as_widget_mut().layout(
&mut tree.children[0],
renderer,
limits,
)
},
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if shell.is_event_captured() {
return;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if self.on_press.is_some() {
let bounds = layout.bounds();
if cursor.is_over(bounds) {
let state = tree.state.downcast_mut::<State>();
state.is_pressed = true;
shell.capture_event();
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = &self.on_press {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
state.is_pressed = false;
let bounds = layout.bounds();
if cursor.is_over(bounds) {
shell.publish(on_press.get());
}
shell.capture_event();
}
}
}
Event::Touch(touch::Event::FingerLost { .. }) => {
let state = tree.state.downcast_mut::<State>();
state.is_pressed = false;
}
_ => {}
}
let current_status = if self.on_press.is_none() {
Status::Disabled
} else if cursor.is_over(layout.bounds()) {
let state = tree.state.downcast_ref::<State>();
if state.is_pressed {
Status::Pressed
} else {
Status::Hovered
}
} else {
Status::Active
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.status = Some(current_status);
} else if self.status.is_some_and(|status| status != current_status) {
shell.request_redraw();
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
if style.background.is_some()
|| style.border.width > 0.0
|| style.shadow.color.a > 0.0
{
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
shadow: style.shadow,
snap: style.snap,
},
style
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
let viewport = if self.clip {
bounds.intersection(viewport).unwrap_or(*viewport)
} else {
*viewport
};
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
&renderer::Style {
text_color: style.text_color,
},
content_layout,
cursor,
&viewport,
);
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over && self.on_press.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
Self::new(button)
}
}
/// The default [`Padding`] of a [`Button`].
pub const DEFAULT_PADDING: Padding = Padding {
top: 5.0,
bottom: 5.0,
right: 10.0,
left: 10.0,
};
/// The possible status of a [`Button`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Button`] can be pressed.
Active,
/// The [`Button`] can be pressed and it is being hovered.
Hovered,
/// The [`Button`] is being pressed.
Pressed,
/// The [`Button`] cannot be pressed.
Disabled,
}
/// The style of a button.
///
/// If not specified with [`Button::style`]
/// the theme will provide the style.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the button.
pub background: Option<Background>,
/// The text [`Color`] of the button.
pub text_color: Color,
/// The [`Border`] of the button.
pub border: Border,
/// The [`Shadow`] of the button.
pub shadow: Shadow,
/// Whether the button should be snapped to the pixel grid.
pub snap: bool,
}
impl Style {
/// Updates the [`Style`] with the given [`Background`].
pub fn with_background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
}
impl Default for Style {
fn default() -> Self {
Self {
background: None,
text_color: Color::BLACK,
border: Border::default(),
shadow: Shadow::default(),
snap: cfg!(feature = "crisp"),
}
}
}
/// The theme catalog of a [`Button`].
///
/// All themes that can be used with [`Button`]
/// must implement this trait.
///
/// # Example
/// ```no_run
/// # use iced_widget::core::{Color, Background};
/// # use iced_widget::button::{Catalog, Status, Style};
/// # struct MyTheme;
/// #[derive(Debug, Default)]
/// pub enum ButtonClass {
/// #[default]
/// Primary,
/// Secondary,
/// Danger
/// }
///
/// impl Catalog for MyTheme {
/// type Class<'a> = ButtonClass;
///
/// fn default<'a>() -> Self::Class<'a> {
/// ButtonClass::default()
/// }
///
///
/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
/// let mut style = Style::default();
///
/// match class {
/// ButtonClass::Primary => {
/// style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
/// },
/// ButtonClass::Secondary => {
/// style.background = Some(Background::Color(Color::WHITE));
/// },
/// ButtonClass::Danger => {
/// style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
/// },
/// }
///
/// style
/// }
/// }
/// ```
///
/// Although, in order to use [`Button::style`]
/// with `MyTheme`, [`Catalog::Class`] must implement
/// `From<StyleFn<'_, MyTheme>>`.
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
/// A styling function for a [`Button`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(primary)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
/// A primary button; denoting a main action.
pub fn primary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.primary.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(Background::Color(palette.primary.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A secondary button; denoting a complementary action.
pub fn secondary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.secondary.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(Background::Color(palette.secondary.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A success button; denoting a good outcome.
pub fn success(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.success.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(Background::Color(palette.success.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A warning button; denoting a risky action.
pub fn warning(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.warning.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(Background::Color(palette.warning.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A danger button; denoting a destructive action.
pub fn danger(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.danger.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(Background::Color(palette.danger.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A text button; useful for links.
pub fn text(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = Style {
text_color: palette.background.base.text,
..Style::default()
};
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
text_color: palette.background.base.text.scale_alpha(0.8),
..base
},
Status::Disabled => disabled(base),
}
}
/// A button using background shades.
pub fn background(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.background.base);
match status {
Status::Active => base,
Status::Pressed => Style {
background: Some(Background::Color(
palette.background.strong.color,
)),
..base
},
Status::Hovered => Style {
background: Some(Background::Color(palette.background.weak.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A subtle button using weak background shades.
pub fn subtle(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = styled(palette.background.weakest);
match status {
Status::Active => base,
Status::Pressed => Style {
background: Some(Background::Color(
palette.background.strong.color,
)),
..base
},
Status::Hovered => Style {
background: Some(Background::Color(
palette.background.weaker.color,
)),
..base
},
Status::Disabled => disabled(base),
}
}
fn styled(pair: palette::Pair) -> Style {
Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: border::rounded(2),
..Style::default()
}
}
fn disabled(style: Style) -> Style {
Style {
background: style
.background
.map(|background| background.scale_alpha(0.5)),
text_color: style.text_color.scale_alpha(0.5),
..style
}
}
+327
View File
@@ -0,0 +1,327 @@
//! Canvases can be leveraged to draw interactive 2D graphics.
//!
//! # Example: Drawing a Simple Circle
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::mouse;
//! use iced::widget::canvas;
//! use iced::{Color, Rectangle, Renderer, Theme};
//!
//! // First, we define the data we need for drawing
//! #[derive(Debug)]
//! struct Circle {
//! radius: f32,
//! }
//!
//! // Then, we implement the `Program` trait
//! impl<Message> canvas::Program<Message> for Circle {
//! // No internal state
//! type State = ();
//!
//! fn draw(
//! &self,
//! _state: &(),
//! renderer: &Renderer,
//! _theme: &Theme,
//! bounds: Rectangle,
//! _cursor: mouse::Cursor
//! ) -> Vec<canvas::Geometry> {
//! // We prepare a new `Frame`
//! let mut frame = canvas::Frame::new(renderer, bounds.size());
//!
//! // We create a `Path` representing a simple circle
//! let circle = canvas::Path::circle(frame.center(), self.radius);
//!
//! // And fill it with some color
//! frame.fill(&circle, Color::BLACK);
//!
//! // Then, we produce the geometry
//! vec![frame.into_geometry()]
//! }
//! }
//!
//! // Finally, we simply use our `Circle` to create the `Canvas`!
//! fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> {
//! canvas(Circle { radius: 50.0 }).into()
//! }
//! ```
mod program;
pub use program::Program;
pub use crate::Action;
pub use crate::core::event::Event;
pub use crate::graphics::cache::Group;
pub use crate::graphics::geometry::{
Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style,
Text, fill, gradient, path, stroke,
};
use crate::core::event;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
};
use crate::graphics::geometry;
use std::marker::PhantomData;
/// A simple cache that stores generated [`Geometry`] to avoid recomputation.
///
/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer
/// change or it is explicitly cleared.
pub type Cache<Renderer = crate::Renderer> = geometry::Cache<Renderer>;
/// The geometry supported by a renderer.
pub type Geometry<Renderer = crate::Renderer> =
<Renderer as geometry::Renderer>::Geometry;
/// The frame supported by a renderer.
pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
/// A widget capable of drawing 2D graphics.
///
/// # Example: Drawing a Simple Circle
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::mouse;
/// use iced::widget::canvas;
/// use iced::{Color, Rectangle, Renderer, Theme};
///
/// // First, we define the data we need for drawing
/// #[derive(Debug)]
/// struct Circle {
/// radius: f32,
/// }
///
/// // Then, we implement the `Program` trait
/// impl<Message> canvas::Program<Message> for Circle {
/// // No internal state
/// type State = ();
///
/// fn draw(
/// &self,
/// _state: &(),
/// renderer: &Renderer,
/// _theme: &Theme,
/// bounds: Rectangle,
/// _cursor: mouse::Cursor
/// ) -> Vec<canvas::Geometry> {
/// // We prepare a new `Frame`
/// let mut frame = canvas::Frame::new(renderer, bounds.size());
///
/// // We create a `Path` representing a simple circle
/// let circle = canvas::Path::circle(frame.center(), self.radius);
///
/// // And fill it with some color
/// frame.fill(&circle, Color::BLACK);
///
/// // Then, we produce the geometry
/// vec![frame.into_geometry()]
/// }
/// }
///
/// // Finally, we simply use our `Circle` to create the `Canvas`!
/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> {
/// canvas(Circle { radius: 50.0 }).into()
/// }
/// ```
#[derive(Debug)]
pub struct Canvas<P, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Renderer: geometry::Renderer,
P: Program<Message, Theme, Renderer>,
{
width: Length,
height: Length,
program: P,
message_: PhantomData<Message>,
theme_: PhantomData<Theme>,
renderer_: PhantomData<Renderer>,
last_mouse_interaction: Option<mouse::Interaction>,
}
impl<P, Message, Theme, Renderer> Canvas<P, Message, Theme, Renderer>
where
P: Program<Message, Theme, Renderer>,
Renderer: geometry::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,
renderer_: PhantomData,
last_mouse_interaction: None,
}
}
/// 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, Theme, Renderer> Widget<Message, Theme, Renderer>
for Canvas<P, Message, Theme, Renderer>
where
Renderer: geometry::Renderer,
P: Program<Message, Theme, 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 size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width, self.height)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
let state = tree.state.downcast_mut::<P::State>();
let is_redraw_request = matches!(
event,
Event::Window(window::Event::RedrawRequested(_now)),
);
if let Some(action) = self.program.update(state, event, bounds, cursor)
{
let (message, redraw_request, event_status) = action.into_inner();
shell.request_redraw_at(redraw_request);
if let Some(message) = message {
shell.publish(message);
}
if event_status == event::Status::Captured {
shell.capture_event();
}
}
if shell.redraw_request() != window::RedrawRequest::NextFrame {
let mouse_interaction = self
.mouse_interaction(tree, layout, cursor, viewport, renderer);
if is_redraw_request {
self.last_mouse_interaction = Some(mouse_interaction);
} else if self.last_mouse_interaction.is_some_and(
|last_mouse_interaction| {
last_mouse_interaction != mouse_interaction
},
) {
shell.request_redraw();
}
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let state = tree.state.downcast_ref::<P::State>();
self.program.mouse_interaction(state, bounds, cursor)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
if bounds.width < 1.0 || bounds.height < 1.0 {
return;
}
let state = tree.state.downcast_ref::<P::State>();
renderer.with_translation(
Vector::new(bounds.x, bounds.y),
|renderer| {
let layers =
self.program.draw(state, renderer, theme, bounds, cursor);
for layer in layers {
renderer.draw_geometry(layer);
}
},
);
}
}
impl<'a, P, Message, Theme, Renderer> From<Canvas<P, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: 'a + geometry::Renderer,
P: 'a + Program<Message, Theme, Renderer>,
{
fn from(
canvas: Canvas<P, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(canvas)
}
}
+111
View File
@@ -0,0 +1,111 @@
use crate::Action;
use crate::canvas::mouse;
use crate::canvas::{Event, Geometry};
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::Canvas
pub trait Program<Message, Theme = crate::Theme, 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 an [`Action`] to either notify an
/// application of any meaningful interactions, capture the event, or
/// request a redraw.
///
/// By default, this method does and returns nothing.
///
/// [`Canvas`]: crate::Canvas
fn update(
&self,
_state: &mut Self::State,
_event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<Action<Message>> {
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`].
///
/// [`Geometry`]: crate::canvas::Geometry
/// [`Frame`]: crate::canvas::Frame
/// [`Cache`]: crate::canvas::Cache
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Vec<Geometry<Renderer>>;
/// 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::Canvas
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
}
impl<Message, Theme, Renderer, T> Program<Message, Theme, Renderer> for &T
where
Renderer: geometry::Renderer,
T: Program<Message, Theme, Renderer>,
{
type State = T::State;
fn update(
&self,
state: &mut Self::State,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
T::update(self, state, event, bounds, cursor)
}
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Vec<Geometry<Renderer>> {
T::draw(self, state, renderer, theme, bounds, cursor)
}
fn mouse_interaction(
&self,
state: &Self::State,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
T::mouse_interaction(self, state, bounds, cursor)
}
}
+707
View File
@@ -0,0 +1,707 @@
//! Checkboxes can be used to let users make binary choices.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::checkbox;
//!
//! struct State {
//! is_checked: bool,
//! }
//!
//! enum Message {
//! CheckboxToggled(bool),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! checkbox(state.is_checked)
//! .label("Toggle me!")
//! .on_toggle(Message::CheckboxToggled)
//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::CheckboxToggled(is_checked) => {
//! state.is_checked = is_checked;
//! }
//! }
//! }
//! ```
//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
use crate::core::alignment;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::theme::palette;
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
Pixels, Rectangle, Shell, Size, Theme, Widget,
};
/// A box that can be checked.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::checkbox;
///
/// struct State {
/// is_checked: bool,
/// }
///
/// enum Message {
/// CheckboxToggled(bool),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// checkbox(state.is_checked)
/// .label("Toggle me!")
/// .on_toggle(Message::CheckboxToggled)
/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::CheckboxToggled(is_checked) => {
/// state.is_checked = is_checked;
/// }
/// }
/// }
/// ```
/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
pub struct Checkbox<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Renderer: text::Renderer,
Theme: Catalog,
{
is_checked: bool,
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
label: Option<text::Fragment<'a>>,
width: Length,
size: f32,
spacing: f32,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
class: Theme::Class<'a>,
last_status: Option<Status>,
}
impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
where
Renderer: text::Renderer,
Theme: Catalog,
{
/// The default size of a [`Checkbox`].
const DEFAULT_SIZE: f32 = 16.0;
/// Creates a new [`Checkbox`].
///
/// It expects:
/// * a boolean describing whether the [`Checkbox`] is checked or not
pub fn new(is_checked: bool) -> Self {
Checkbox {
is_checked,
on_toggle: None,
label: None,
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SIZE / 2.0,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::default(),
text_wrapping: text::Wrapping::default(),
font: None,
icon: Icon {
font: Renderer::ICON_FONT,
code_point: Renderer::CHECKMARK_ICON,
size: None,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Basic,
},
class: Theme::default(),
last_status: None,
}
}
/// Sets the label of the [`Checkbox`].
pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
self.label = Some(label.into_fragment());
self
}
/// Sets the function that will be called when the [`Checkbox`] is toggled.
/// It will receive the new state of the [`Checkbox`] and must produce a
/// `Message`.
///
/// Unless `on_toggle` is called, the [`Checkbox`] will be disabled.
pub fn on_toggle<F>(mut self, f: F) -> Self
where
F: 'a + Fn(bool) -> Message,
{
self.on_toggle = Some(Box::new(f));
self
}
/// Sets the function that will be called when the [`Checkbox`] is toggled,
/// if `Some`.
///
/// If `None`, the checkbox will be disabled.
pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
where
F: Fn(bool) -> Message + 'a,
{
self.on_toggle = f.map(|f| Box::new(f) as _);
self
}
/// 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());
self
}
/// Sets the text [`text::LineHeight`] of the [`Checkbox`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
) -> Self {
self.text_line_height = line_height.into();
self
}
/// Sets the [`text::Shaping`] strategy of the [`Checkbox`].
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
self.text_shaping = shaping;
self
}
/// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
self.text_wrapping = wrapping;
self
}
/// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
///
/// [`Renderer::Font`]: crate::core::text::Renderer
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`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Checkbox`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Checkbox<'_, Message, Theme, Renderer>
where
Renderer: text::Renderer,
Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::next_to_each_other(
&limits.width(self.width),
if self.label.is_some() {
self.spacing
} else {
0.0
},
|_| layout::Node::new(Size::new(self.size, self.size)),
|limits| {
if let Some(label) = self.label.as_deref() {
let state = tree
.state
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
widget::text::layout(
state,
renderer,
limits,
label,
widget::text::Format {
width: self.width,
height: Length::Shrink,
line_height: self.text_line_height,
size: self.text_size,
font: self.font,
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Top,
shaping: self.text_shaping,
wrapping: self.text_wrapping,
},
)
} else {
layout::Node::new(Size::ZERO)
}
},
)
}
fn update(
&mut self,
_tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = cursor.is_over(layout.bounds());
if mouse_over && let Some(on_toggle) = &self.on_toggle {
shell.publish((on_toggle)(!self.is_checked));
shell.capture_event();
}
}
_ => {}
}
let current_status = {
let is_mouse_over = cursor.is_over(layout.bounds());
let is_disabled = self.on_toggle.is_none();
let is_checked = self.is_checked;
if is_disabled {
Status::Disabled { is_checked }
} else if is_mouse_over {
Status::Hovered { is_checked }
} else {
Status::Active { is_checked }
}
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_status = Some(current_status);
} else if self
.last_status
.is_some_and(|status| status != current_status)
{
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let mut children = layout.children();
let style = theme.style(
&self.class,
self.last_status.unwrap_or(Status::Disabled {
is_checked: self.is_checked,
}),
);
{
let layout = children.next().unwrap();
let bounds = layout.bounds();
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
..renderer::Quad::default()
},
style.background,
);
let Icon {
font,
code_point,
size,
line_height,
shaping,
} = &self.icon;
let size = size.unwrap_or(Pixels(bounds.height * 0.7));
if self.is_checked {
renderer.fill_text(
text::Text {
content: code_point.to_string(),
font: *font,
size,
line_height: *line_height,
bounds: bounds.size(),
align_x: text::Alignment::Center,
align_y: alignment::Vertical::Center,
shaping: *shaping,
wrapping: text::Wrapping::default(),
},
bounds.center(),
style.icon_color,
*viewport,
);
}
}
if self.label.is_none() {
return;
}
{
let label_layout = children.next().unwrap();
let state: &widget::text::State<Renderer::Paragraph> =
tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout.bounds(),
state.raw(),
crate::text::Style {
color: style.text_color,
},
viewport,
);
}
}
fn operate(
&mut self,
_tree: &mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
if let Some(label) = self.label.as_deref() {
operation.text(None, layout.bounds(), label);
}
}
}
impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a + Catalog,
Renderer: 'a + text::Renderer,
{
fn from(
checkbox: Checkbox<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(checkbox)
}
}
/// The icon in a [`Checkbox`].
#[derive(Debug, Clone, PartialEq)]
pub struct Icon<Font> {
/// Font that will be used to display the `code_point`,
pub font: Font,
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
pub size: Option<Pixels>,
/// The line height of the icon.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
pub shaping: text::Shaping,
}
/// The possible status of a [`Checkbox`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Checkbox`] can be interacted with.
Active {
/// Indicates if the [`Checkbox`] is currently checked.
is_checked: bool,
},
/// The [`Checkbox`] can be interacted with and it is being hovered.
Hovered {
/// Indicates if the [`Checkbox`] is currently checked.
is_checked: bool,
},
/// The [`Checkbox`] cannot be interacted with.
Disabled {
/// Indicates if the [`Checkbox`] is currently checked.
is_checked: bool,
},
}
/// The style of a checkbox.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the checkbox.
pub background: Background,
/// The icon [`Color`] of the checkbox.
pub icon_color: Color,
/// The [`Border`] of the checkbox.
pub border: Border,
/// The text [`Color`] of the checkbox.
pub text_color: Option<Color>,
}
/// The theme catalog of a [`Checkbox`].
pub trait Catalog: Sized {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
/// A styling function for a [`Checkbox`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(primary)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
/// A primary checkbox; denoting a main toggle.
pub fn primary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.background.strong.color,
palette.background.base,
palette.primary.base.text,
palette.primary.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.background.strong.color,
palette.background.weak,
palette.primary.base.text,
palette.primary.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.background.weak.color,
palette.background.weaker,
palette.primary.base.text,
palette.background.strong,
is_checked,
),
}
}
/// A secondary checkbox; denoting a complementary toggle.
pub fn secondary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.background.strong.color,
palette.background.base,
palette.background.base.text,
palette.background.strong,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.background.strong.color,
palette.background.weak,
palette.background.base.text,
palette.background.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.background.weak.color,
palette.background.weak,
palette.background.base.text,
palette.background.weak,
is_checked,
),
}
}
/// A success checkbox; denoting a positive toggle.
pub fn success(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.background.weak.color,
palette.background.base,
palette.success.base.text,
palette.success.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.background.strong.color,
palette.background.weak,
palette.success.base.text,
palette.success.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.background.weak.color,
palette.background.weak,
palette.success.base.text,
palette.success.weak,
is_checked,
),
}
}
/// A danger checkbox; denoting a negative toggle.
pub fn danger(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.background.strong.color,
palette.background.base,
palette.danger.base.text,
palette.danger.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.background.strong.color,
palette.background.weak,
palette.danger.base.text,
palette.danger.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.background.weak.color,
palette.background.weak,
palette.danger.base.text,
palette.danger.weak,
is_checked,
),
}
}
fn styled(
border_color: Color,
base: palette::Pair,
icon_color: Color,
accent: palette::Pair,
is_checked: bool,
) -> Style {
let (background, border) = if is_checked {
(accent, accent.color)
} else {
(base, border_color)
};
Style {
background: Background::Color(background.color),
icon_color,
border: Border {
radius: 2.0.into(),
width: 1.0,
color: border,
},
text_color: None,
}
}
+617
View File
@@ -0,0 +1,617 @@
//! Distribute content vertically.
use crate::core::alignment::{self, Alignment};
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::{
Clipboard, Element, Event, Layout, Length, Padding, Pixels, Rectangle,
Shell, Size, Vector, Widget,
};
/// A container that distributes its contents vertically.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::{button, column};
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// column![
/// "I am on top!",
/// button("I am in the center!"),
/// "I am below.",
/// ].into()
/// }
/// ```
pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
{
spacing: f32,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Column`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`Column`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`Column`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Column`] from an already allocated [`Vec`].
///
/// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means
/// it won't automatically adapt to the sizing strategy of its contents.
///
/// If any of the children have a [`Length::Fill`] strategy, you will need to
/// call [`Column::width`] or [`Column::height`] accordingly.
pub fn from_vec(
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
align: Alignment::Start,
clip: false,
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_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
self.align = Alignment::from(align.into());
self
}
/// Sets whether the contents of the [`Column`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Adds an element to the [`Column`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
if !child_size.is_void() {
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
self.children.push(child);
}
self
}
/// Extends the [`Column`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
/// Turns the [`Column`] into a [`Wrapping`] column.
///
/// The original alignment of the [`Column`] is preserved per column wrapped.
pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
Wrapping {
column: self,
horizontal_spacing: None,
align_y: alignment::Vertical::Top,
}
}
}
impl<Message, Renderer> Default for Column<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
FromIterator<Element<'a, Message, Theme, Renderer>>
for Column<'a, Message, Theme, Renderer>
{
fn from_iter<
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
>(
iter: T,
) -> Self {
Self::with_children(iter)
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Column<'_, Message, Theme, 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 size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.max_width(self.max_width);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
self.width,
self.height,
self.padding,
self.spacing,
self.align,
&mut self.children,
&mut tree.children,
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((child, tree), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
child.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, tree), layout)| {
child
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
for ((child, tree), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(viewport))
{
child.as_widget().draw(
tree, renderer, theme, style, layout, cursor, viewport,
);
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Column<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(column: Column<'a, Message, Theme, Renderer>) -> Self {
Self::new(column)
}
}
/// A [`Column`] that wraps its contents.
///
/// Create a [`Column`] first, and then call [`Column::wrap`] to
/// obtain a [`Column`] that wraps its contents.
///
/// The original alignment of the [`Column`] is preserved per column wrapped.
#[allow(missing_debug_implementations)]
pub struct Wrapping<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
column: Column<'a, Message, Theme, Renderer>,
horizontal_spacing: Option<f32>,
align_y: alignment::Vertical,
}
impl<Message, Theme, Renderer> Wrapping<'_, Message, Theme, Renderer> {
/// Sets the horizontal spacing _between_ columns.
pub fn horizontal_spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.horizontal_spacing = Some(amount.into().0);
self
}
/// Sets the vertical alignment of the wrapping [`Column`].
pub fn align_x(mut self, align_y: impl Into<alignment::Vertical>) -> Self {
self.align_y = align_y.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Wrapping<'_, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.column.children()
}
fn diff(&self, tree: &mut Tree) {
self.column.diff(tree);
}
fn size(&self) -> Size<Length> {
self.column.size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits
.width(self.column.width)
.height(self.column.height)
.shrink(self.column.padding);
let child_limits = limits.loose();
let spacing = self.column.spacing;
let horizontal_spacing = self.horizontal_spacing.unwrap_or(spacing);
let max_height = limits.max().height;
let mut children: Vec<layout::Node> = Vec::new();
let mut intrinsic_size = Size::ZERO;
let mut column_start = 0;
let mut column_width = 0.0;
let mut x = 0.0;
let mut y = 0.0;
let align_factor = match self.column.align {
Alignment::Start => 0.0,
Alignment::Center => 2.0,
Alignment::End => 1.0,
};
let align_x = |column_start: std::ops::Range<usize>,
column_width: f32,
children: &mut Vec<layout::Node>| {
if align_factor != 0.0 {
for node in &mut children[column_start] {
let width = node.size().width;
node.translate_mut(Vector::new(
(column_width - width) / align_factor,
0.0,
));
}
}
};
for (i, child) in self.column.children.iter_mut().enumerate() {
let node = child.as_widget_mut().layout(
&mut tree.children[i],
renderer,
&child_limits,
);
let child_size = node.size();
if y != 0.0 && y + child_size.height > max_height {
intrinsic_size.height = intrinsic_size.height.max(y - spacing);
align_x(column_start..i, column_width, &mut children);
x += column_width + horizontal_spacing;
y = 0.0;
column_start = i;
column_width = 0.0;
}
column_width = column_width.max(child_size.width);
children.push(node.move_to((
x + self.column.padding.left,
y + self.column.padding.top,
)));
y += child_size.height + spacing;
}
if y != 0.0 {
intrinsic_size.height = intrinsic_size.height.max(y - spacing);
}
intrinsic_size.width = x + column_width;
align_x(column_start..children.len(), column_width, &mut children);
let align_factor = match self.align_y {
alignment::Vertical::Top => 0.0,
alignment::Vertical::Center => 2.0,
alignment::Vertical::Bottom => 1.0,
};
if align_factor != 0.0 {
let total_height = intrinsic_size.height;
let mut column_start = 0;
for i in 0..children.len() {
let bounds = children[i].bounds();
let column_height = bounds.y + bounds.height;
let next_y = children
.get(i + 1)
.map(|node| node.bounds().y)
.unwrap_or_default();
if next_y == 0.0 {
let translation = Vector::new(
0.0,
(total_height - column_height) / align_factor,
);
for node in &mut children[column_start..=i] {
node.translate_mut(translation);
}
column_start = i + 1;
}
}
}
let size = limits.resolve(
self.column.width,
self.column.height,
intrinsic_size,
);
layout::Node::with_children(size.expand(self.column.padding), children)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.column.operate(tree, layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.column.update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.column
.mouse_interaction(tree, layout, cursor, viewport, renderer)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.column
.draw(tree, renderer, theme, style, layout, cursor, viewport);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.column
.overlay(tree, layout, renderer, viewport, translation)
}
}
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(column: Wrapping<'a, Message, Theme, Renderer>) -> Self {
Self::new(column)
}
}
File diff suppressed because it is too large Load Diff
+660
View File
@@ -0,0 +1,660 @@
//! Containers let you align a widget inside their boundaries.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::container;
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! container("This text is centered inside a rounded box!")
//! .padding(10)
//! .center(800)
//! .style(container::rounded_box)
//! .into()
//! }
//! ```
use crate::core::alignment::{self, Alignment};
use crate::core::border::{self, Border};
use crate::core::gradient::{self, Gradient};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::theme;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{
self, Background, Clipboard, Color, Element, Event, Layout, Length,
Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
color,
};
/// A widget that aligns its contents inside of its boundaries.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::container;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// container("This text is centered inside a rounded box!")
/// .padding(10)
/// .center(800)
/// .style(container::rounded_box)
/// .into()
/// }
/// ```
pub struct Container<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: Catalog,
Renderer: core::Renderer,
{
id: Option<widget::Id>,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
max_height: f32,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
clip: bool,
content: Element<'a, Message, Theme, Renderer>,
class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
/// Creates a [`Container`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let content = content.into();
let size = content.as_widget().size_hint();
Container {
id: None,
padding: Padding::ZERO,
width: size.width.fluid(),
height: size.height.fluid(),
max_width: f32::INFINITY,
max_height: f32::INFINITY,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
clip: false,
class: Theme::default(),
content,
}
}
/// Sets the [`widget::Id`] of the [`Container`].
pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
self.id = Some(id.into());
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 width of the [`Container`] and centers its contents horizontally.
pub fn center_x(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Center)
}
/// Sets the height of the [`Container`] and centers its contents vertically.
pub fn center_y(self, height: impl Into<Length>) -> Self {
self.height(height).align_y(alignment::Vertical::Center)
}
/// Sets the width and height of the [`Container`] and centers its contents in
/// both the horizontal and vertical axes.
///
/// This is equivalent to chaining [`center_x`] and [`center_y`].
///
/// [`center_x`]: Self::center_x
/// [`center_y`]: Self::center_y
pub fn center(self, length: impl Into<Length>) -> Self {
let length = length.into();
self.center_x(length).center_y(length)
}
/// Sets the width of the [`Container`] and aligns its contents to the left.
pub fn align_left(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Left)
}
/// Sets the width of the [`Container`] and aligns its contents to the right.
pub fn align_right(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Right)
}
/// Sets the height of the [`Container`] and aligns its contents to the top.
pub fn align_top(self, height: impl Into<Length>) -> Self {
self.height(height).align_y(alignment::Vertical::Top)
}
/// Sets the height of the [`Container`] and aligns its contents to the bottom.
pub fn align_bottom(self, height: impl Into<Length>) -> Self {
self.height(height).align_y(alignment::Vertical::Bottom)
}
/// Sets the content alignment for the horizontal axis of the [`Container`].
pub fn align_x(
mut self,
alignment: impl Into<alignment::Horizontal>,
) -> Self {
self.horizontal_alignment = alignment.into();
self
}
/// Sets the content alignment for the vertical axis of the [`Container`].
pub fn align_y(
mut self,
alignment: impl Into<alignment::Vertical>,
) -> Self {
self.vertical_alignment = alignment.into();
self
}
/// Sets whether the contents of the [`Container`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Sets the style of the [`Container`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Container`].
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Container<'_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
self.content.as_widget().tag()
}
fn state(&self) -> tree::State {
self.content.as_widget().state()
}
fn children(&self) -> Vec<Tree> {
self.content.as_widget().children()
}
fn diff(&self, tree: &mut Tree) {
self.content.as_widget().diff(tree);
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
limits,
self.width,
self.height,
self.max_width,
self.max_height,
self.padding,
self.horizontal_alignment,
self.vertical_alignment,
|limits| {
self.content.as_widget_mut().layout(tree, renderer, limits)
},
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(self.id.as_ref(), layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
tree,
layout.children().next().unwrap(),
renderer,
operation,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
tree,
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
tree,
layout.children().next().unwrap(),
cursor,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
let style = theme.style(&self.class);
if let Some(clipped_viewport) = bounds.intersection(viewport) {
draw_background(renderer, &style, bounds);
self.content.as_widget().draw(
tree,
renderer,
theme,
&renderer::Style {
text_color: style
.text_color
.unwrap_or(renderer_style.text_color),
},
layout.children().next().unwrap(),
cursor,
if self.clip {
&clipped_viewport
} else {
viewport
},
);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
tree,
layout.children().next().unwrap(),
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(
container: Container<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(container)
}
}
/// Computes the layout of a [`Container`].
pub fn layout(
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(&layout::Limits) -> layout::Node,
) -> layout::Node {
layout::positioned(
&limits.max_width(max_width).max_height(max_height),
width,
height,
padding,
|limits| layout_content(&limits.loose()),
|content, size| {
content.align(
Alignment::from(horizontal_alignment),
Alignment::from(vertical_alignment),
size,
)
},
)
}
/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`.
pub fn draw_background<Renderer>(
renderer: &mut Renderer,
style: &Style,
bounds: Rectangle,
) where
Renderer: core::Renderer,
{
if style.background.is_some()
|| style.border.width > 0.0
|| style.shadow.color.a > 0.0
{
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
shadow: style.shadow,
snap: style.snap,
},
style
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
}
/// The appearance of a container.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The text [`Color`] of the container.
pub text_color: Option<Color>,
/// The [`Background`] of the container.
pub background: Option<Background>,
/// The [`Border`] of the container.
pub border: Border,
/// The [`Shadow`] of the container.
pub shadow: Shadow,
/// Whether the container should be snapped to the pixel grid.
pub snap: bool,
}
impl Default for Style {
fn default() -> Self {
Self {
text_color: None,
background: None,
border: Border::default(),
shadow: Shadow::default(),
snap: cfg!(feature = "crisp"),
}
}
}
impl Style {
/// Updates the text color of the [`Style`].
pub fn color(self, color: impl Into<Color>) -> Self {
Self {
text_color: Some(color.into()),
..self
}
}
/// Updates the border of the [`Style`].
pub fn border(self, border: impl Into<Border>) -> Self {
Self {
border: border.into(),
..self
}
}
/// Updates the background of the [`Style`].
pub fn background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
/// Updates the shadow of the [`Style`].
pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
Self {
shadow: shadow.into(),
..self
}
}
}
impl From<Color> for Style {
fn from(color: Color) -> Self {
Self::default().background(color)
}
}
impl From<Gradient> for Style {
fn from(gradient: Gradient) -> Self {
Self::default().background(gradient)
}
}
impl From<gradient::Linear> for Style {
fn from(gradient: gradient::Linear) -> Self {
Self::default().background(gradient)
}
}
/// The theme catalog of a [`Container`].
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`Container`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl<Theme> From<Style> for StyleFn<'_, Theme> {
fn from(style: Style) -> Self {
Box::new(move |_theme| style)
}
}
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(transparent)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// A transparent [`Container`].
pub fn transparent<Theme>(_theme: &Theme) -> Style {
Style::default()
}
/// A [`Container`] with the given [`Background`].
pub fn background(background: impl Into<Background>) -> Style {
Style::default().background(background)
}
/// A rounded [`Container`] with a background.
pub fn rounded_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
background: Some(palette.background.weak.color.into()),
text_color: Some(palette.background.weak.text),
border: border::rounded(2),
..Style::default()
}
}
/// A bordered [`Container`] with a background.
pub fn bordered_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
background: Some(palette.background.weakest.color.into()),
text_color: Some(palette.background.weakest.text),
border: Border {
width: 1.0,
radius: 5.0.into(),
color: palette.background.weak.color,
},
..Style::default()
}
}
/// A [`Container`] with a dark background and white text.
pub fn dark(_theme: &Theme) -> Style {
style(theme::palette::Pair {
color: color!(0x111111),
text: Color::WHITE,
})
}
/// A [`Container`] with a primary background color.
pub fn primary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
style(palette.primary.base)
}
/// A [`Container`] with a secondary background color.
pub fn secondary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
style(palette.secondary.base)
}
/// A [`Container`] with a success background color.
pub fn success(theme: &Theme) -> Style {
let palette = theme.extended_palette();
style(palette.success.base)
}
/// A [`Container`] with a warning background color.
pub fn warning(theme: &Theme) -> Style {
let palette = theme.extended_palette();
style(palette.warning.base)
}
/// A [`Container`] with a danger background color.
pub fn danger(theme: &Theme) -> Style {
let palette = theme.extended_palette();
style(palette.danger.base)
}
fn style(pair: theme::palette::Pair) -> Style {
Style {
background: Some(pair.color.into()),
text_color: Some(pair.text),
border: border::rounded(2),
..Style::default()
}
}
+432
View File
@@ -0,0 +1,432 @@
//! Make elements float!
use crate::core;
use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree;
use crate::core::{
Clipboard, Element, Event, Layout, Length, Rectangle, Shadow, Shell, Size,
Transformation, Vector, Widget,
};
/// A widget that can make its contents float over other widgets.
pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Theme: Catalog,
{
content: Element<'a, Message, Theme, Renderer>,
scale: f32,
translate: Option<Box<dyn Fn(Rectangle, Rectangle) -> Vector + 'a>>,
class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Float<'a, Message, Theme, Renderer>
where
Theme: Catalog,
{
/// Creates a new [`Float`] widget with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
scale: 1.0,
translate: None,
class: Theme::default(),
}
}
/// Sets the scale to be applied to the contents of the [`Float`].
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
/// Sets the translation logic to be applied to the contents of the [`Float`].
///
/// The logic takes the original (non-scaled) bounds of the contents and the
/// viewport bounds. These bounds can be useful to ensure the floating elements
/// always stay on screen.
pub fn translate(
mut self,
translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a,
) -> Self {
self.translate = Some(Box::new(translate));
self
}
/// Sets the style of the [`Float`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Float`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
fn is_floating(&self, bounds: Rectangle, viewport: Rectangle) -> bool {
self.scale > 1.0
|| self.translate.as_ref().is_some_and(|translate| {
translate(bounds, viewport) != Vector::ZERO
})
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Float<'_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
self.content.as_widget().tag()
}
fn state(&self) -> tree::State {
self.content.as_widget().state()
}
fn children(&self) -> Vec<tree::Tree> {
self.content.as_widget().children()
}
fn diff(&self, tree: &mut widget::Tree) {
self.content.as_widget().diff(tree);
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.content.as_widget().size_hint()
}
fn layout(
&mut self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget_mut().layout(tree, renderer, limits)
}
fn update(
&mut self,
tree: &mut widget::Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if self.is_floating(layout.bounds(), *viewport) {
return;
}
self.content.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if self.is_floating(layout.bounds(), *viewport) {
return;
}
{
let style = theme.style(&self.class);
if style.shadow.color.a > 0.0 {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds().shrink(1.0),
shadow: style.shadow,
border: border::rounded(style.shadow_border_radius),
snap: false,
},
style.shadow.color,
);
}
}
self.content
.as_widget()
.draw(tree, renderer, theme, style, layout, cursor, viewport);
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
if self.is_floating(layout.bounds(), *viewport) {
return mouse::Interaction::None;
}
self.content
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
}
fn operate(
&mut self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content
.as_widget_mut()
.operate(tree, layout, renderer, operation);
}
fn overlay<'a>(
&'a mut self,
state: &'a mut widget::Tree,
layout: Layout<'a>,
renderer: &Renderer,
viewport: &Rectangle,
offset: Vector,
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
let bounds = layout.bounds();
let translation = self
.translate
.as_ref()
.map(|translate| translate(bounds + offset, *viewport))
.unwrap_or(Vector::ZERO);
if self.scale > 1.0 || translation != Vector::ZERO {
let translation = translation + offset;
let transformation = Transformation::translate(
bounds.x + bounds.width / 2.0 + translation.x,
bounds.y + bounds.height / 2.0 + translation.y,
) * Transformation::scale(self.scale)
* Transformation::translate(
-bounds.x - bounds.width / 2.0,
-bounds.y - bounds.height / 2.0,
);
Some(overlay::Element::new(Box::new(Overlay {
float: self,
state,
layout,
viewport: *viewport,
transformation,
})))
} else {
self.content
.as_widget_mut()
.overlay(state, layout, renderer, viewport, offset)
}
}
}
impl<'a, Message, Theme, Renderer> From<Float<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(float: Float<'a, Message, Theme, Renderer>) -> Self {
Element::new(float)
}
}
struct Overlay<'a, 'b, Message, Theme, Renderer>
where
Theme: Catalog,
{
float: &'a mut Float<'b, Message, Theme, Renderer>,
state: &'a mut widget::Tree,
layout: Layout<'a>,
viewport: Rectangle,
transformation: Transformation,
}
impl<Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
for Overlay<'_, '_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
let bounds = self.layout.bounds() * self.transformation;
layout::Node::new(bounds.size()).move_to(bounds.position())
}
fn update(
&mut self,
event: &Event,
_layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let inverse = self.transformation.inverse();
self.float.content.as_widget_mut().update(
self.state,
event,
self.layout,
cursor * inverse,
renderer,
clipboard,
shell,
&(self.viewport * inverse),
);
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
_layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let bounds = self.layout.bounds();
let inverse = self.transformation.inverse();
renderer.with_layer(self.viewport, |renderer| {
renderer.with_transformation(self.transformation, |renderer| {
{
let style = theme.style(&self.float.class);
if style.shadow.color.a > 0.0 {
renderer.fill_quad(
renderer::Quad {
bounds: bounds.shrink(1.0),
shadow: style.shadow,
border: border::rounded(
style.shadow_border_radius,
),
snap: false,
},
style.shadow.color,
);
}
}
self.float.content.as_widget().draw(
self.state,
renderer,
theme,
style,
self.layout,
cursor * inverse,
&(self.viewport * inverse),
);
});
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
if !cursor.is_over(layout.bounds()) {
return mouse::Interaction::None;
}
let inverse = self.transformation.inverse();
self.float.content.as_widget().mouse_interaction(
self.state,
self.layout,
cursor * inverse,
&(self.viewport * inverse),
renderer,
)
}
fn index(&self) -> f32 {
self.float.scale * 0.5
}
fn overlay<'a>(
&'a mut self,
_layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
self.float.content.as_widget_mut().overlay(
self.state,
self.layout,
renderer,
&(self.viewport * self.transformation.inverse()),
self.transformation.translation(),
)
}
}
/// The theme catalog of a [`Float`].
///
/// All themes that can be used with [`Float`]
/// must implement this trait.
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`Float`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for crate::Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(|_| Style::default())
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// The style of a [`Float`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Style {
/// The [`Shadow`] of the [`Float`].
pub shadow: Shadow,
/// The border radius of the shadow.
pub shadow_border_radius: border::Radius,
}
+415
View File
@@ -0,0 +1,415 @@
//! Distribute content on a grid.
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::{
Clipboard, Element, Event, Length, Pixels, Rectangle, Shell, Size, Vector,
Widget,
};
/// A container that distributes its contents on a responsive grid.
pub struct Grid<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
spacing: f32,
columns: Constraint,
width: Option<Pixels>,
height: Sizing,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
enum Constraint {
MaxWidth(Pixels),
Amount(usize),
}
impl<'a, Message, Theme, Renderer> Grid<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Grid`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`Grid`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`Grid`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Grid`] from an already allocated [`Vec`].
pub fn from_vec(
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
spacing: 0.0,
columns: Constraint::Amount(3),
width: None,
height: Sizing::AspectRatio(1.0),
children,
}
}
/// Sets the spacing _between_ cells in the [`Grid`].
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
/// Sets the width of the [`Grid`] in [`Pixels`].
///
/// By default, a [`Grid`] will [`Fill`] its parent.
///
/// [`Fill`]: Length::Fill
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = Some(width.into());
self
}
/// Sets the height of the [`Grid`].
///
/// By default, a [`Grid`] uses a cell aspect ratio of `1.0` (i.e. squares).
pub fn height(mut self, height: impl Into<Sizing>) -> Self {
self.height = height.into();
self
}
/// Sets the amount of columns in the [`Grid`].
pub fn columns(mut self, column: usize) -> Self {
self.columns = Constraint::Amount(column);
self
}
/// Makes the amount of columns dynamic in the [`Grid`], never
/// exceeding the provided `max_width`.
pub fn fluid(mut self, max_width: impl Into<Pixels>) -> Self {
self.columns = Constraint::MaxWidth(max_width.into());
self
}
/// Adds an [`Element`] to the [`Grid`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
self.children.push(child.into());
self
}
/// Adds an element to the [`Grid`], if `Some`.
pub fn push_maybe(
self,
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
) -> Self {
if let Some(child) = child {
self.push(child)
} else {
self
}
}
/// Extends the [`Grid`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
}
impl<Message, Renderer> Default for Grid<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
FromIterator<Element<'a, Message, Theme, Renderer>>
for Grid<'a, Message, Theme, Renderer>
{
fn from_iter<
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
>(
iter: T,
) -> Self {
Self::with_children(iter)
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Grid<'_, Message, Theme, 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 size(&self) -> Size<Length> {
Size {
width: self
.width
.map(|pixels| Length::Fixed(pixels.0))
.unwrap_or(Length::Fill),
height: match self.height {
Sizing::AspectRatio(_) => Length::Shrink,
Sizing::EvenlyDistribute(length) => length,
},
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let size = self.size();
let limits = limits.width(size.width).height(size.height);
let available = limits.max();
let cells_per_row = match self.columns {
// width = n * (cell + spacing) - spacing, given n > 0
Constraint::MaxWidth(pixels) => ((available.width + self.spacing)
/ (pixels.0 + self.spacing))
.ceil() as usize,
Constraint::Amount(amount) => amount,
};
if self.children.is_empty() || cells_per_row == 0 {
return layout::Node::new(limits.resolve(
size.width,
size.height,
Size::ZERO,
));
}
let cell_width = (available.width
- self.spacing * (cells_per_row - 1) as f32)
/ cells_per_row as f32;
let cell_height = match self.height {
Sizing::AspectRatio(ratio) => Some(cell_width / ratio),
Sizing::EvenlyDistribute(Length::Shrink) => None,
Sizing::EvenlyDistribute(_) => {
let total_rows = self.children.len().div_ceil(cells_per_row);
Some(
(available.height - self.spacing * (total_rows - 1) as f32)
/ total_rows as f32,
)
}
};
let cell_limits = layout::Limits::new(
Size::new(cell_width, cell_height.unwrap_or(0.0)),
Size::new(cell_width, cell_height.unwrap_or(available.height)),
);
let mut nodes = Vec::with_capacity(self.children.len());
let mut x = 0.0;
let mut y = 0.0;
let mut row_height = 0.0f32;
for (i, (child, tree)) in
self.children.iter_mut().zip(&mut tree.children).enumerate()
{
let node = child
.as_widget_mut()
.layout(tree, renderer, &cell_limits)
.move_to((x, y));
let size = node.size();
x += size.width + self.spacing;
row_height = row_height.max(size.height);
if (i + 1) % cells_per_row == 0 {
y += cell_height.unwrap_or(row_height) + self.spacing;
x = 0.0;
row_height = 0.0;
}
nodes.push(node);
}
if x == 0.0 {
y -= self.spacing;
} else {
y += cell_height.unwrap_or(row_height);
}
layout::Node::with_children(Size::new(available.width, y), nodes)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((child, tree), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
child.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, tree), layout)| {
child
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(viewport) = layout.bounds().intersection(viewport) {
for ((child, tree), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(&viewport))
{
child.as_widget().draw(
tree, renderer, theme, style, layout, cursor, &viewport,
);
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Grid<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(row: Grid<'a, Message, Theme, Renderer>) -> Self {
Self::new(row)
}
}
/// The sizing strategy of a [`Grid`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Sizing {
/// The [`Grid`] will ensure each cell follows the given aspect ratio and the
/// total size will be the sum of the cells and the spacing between them.
///
/// The ratio is the amount of horizontal pixels per each vertical pixel of a cell
/// in the [`Grid`].
AspectRatio(f32),
/// The [`Grid`] will evenly distribute the space available in the given [`Length`]
/// for each cell.
EvenlyDistribute(Length),
}
impl From<f32> for Sizing {
fn from(height: f32) -> Self {
Self::EvenlyDistribute(Length::from(height))
}
}
impl From<Length> for Sizing {
fn from(height: Length) -> Self {
Self::EvenlyDistribute(height)
}
}
/// Creates a new [`Sizing`] strategy that maintains the given aspect ratio.
pub fn aspect_ratio(
width: impl Into<Pixels>,
height: impl Into<Pixels>,
) -> Sizing {
Sizing::AspectRatio(width.into().0 / height.into().0)
}
File diff suppressed because it is too large Load Diff
+418
View File
@@ -0,0 +1,418 @@
//! Images display raster graphics in different formats (PNG, JPG, etc.).
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::image;
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! image("ferris.png").into()
//! }
//! ```
//! <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
pub mod viewer;
pub use viewer::Viewer;
use crate::core::border;
use crate::core::image;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size,
Vector, Widget,
};
pub use image::{FilterMethod, Handle};
/// Creates a new [`Viewer`] with the given image `Handle`.
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
Viewer::new(handle)
}
/// A frame that displays an image while keeping aspect ratio.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::image;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// image("ferris.png").into()
/// }
/// ```
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
pub struct Image<Handle = image::Handle> {
handle: Handle,
width: Length,
height: Length,
crop: Option<Rectangle<u32>>,
border_radius: border::Radius,
content_fit: ContentFit,
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
scale: f32,
expand: bool,
}
impl<Handle> Image<Handle> {
/// Creates a new [`Image`] with the given path.
pub fn new(handle: impl Into<Handle>) -> Self {
Image {
handle: handle.into(),
width: Length::Shrink,
height: Length::Shrink,
crop: None,
border_radius: border::Radius::default(),
content_fit: ContentFit::default(),
filter_method: FilterMethod::default(),
rotation: Rotation::default(),
opacity: 1.0,
scale: 1.0,
expand: false,
}
}
/// 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 whether the [`Image`] should try to fill as much space
/// available as possible while keeping aspect ratio and without
/// allocating extra space in any axis with a [`Length::Shrink`]
/// sizing strategy.
///
/// This is similar to using [`Length::Fill`] for both the
/// [`width`](Self::width) and the [`height`](Self::height),
/// but without the downside of blank space.
pub fn expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
/// Sets the [`ContentFit`] of the [`Image`].
///
/// Defaults to [`ContentFit::Contain`]
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
self.content_fit = content_fit;
self
}
/// Sets the [`FilterMethod`] of the [`Image`].
pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
self.filter_method = filter_method;
self
}
/// Applies the given [`Rotation`] to the [`Image`].
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
self.rotation = rotation.into();
self
}
/// Sets the opacity of the [`Image`].
///
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
/// and `1.0` meaning completely opaque.
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
self.opacity = opacity.into();
self
}
/// Sets the scale of the [`Image`].
///
/// The region of the [`Image`] drawn will be scaled from the center by the given scale factor.
/// This can be useful to create certain effects and animations, like smooth zoom in / out.
pub fn scale(mut self, scale: impl Into<f32>) -> Self {
self.scale = scale.into();
self
}
/// Crops the [`Image`] to the given region described by the [`Rectangle`] in absolute
/// coordinates.
///
/// Cropping is done before applying any transformation or [`ContentFit`]. In practice,
/// this means that cropping an [`Image`] with this method should produce the same result
/// as cropping it externally (e.g. with an image editor) and creating a new [`Handle`]
/// for the cropped version.
///
/// However, this method is much more efficient; since it just leverages scissoring during
/// rendering and no image cropping actually takes place. Instead, it reuses the existing
/// image allocations and should be as efficient as not cropping at all!
///
/// The `region` coordinates will be clamped to the image dimensions, if necessary.
pub fn crop(mut self, region: Rectangle<u32>) -> Self {
self.crop = Some(region);
self
}
/// Sets the [`border::Radius`] of the [`Image`].
///
/// Currently, it will only be applied around the rectangular bounding box
/// of the [`Image`].
pub fn border_radius(
mut self,
border_radius: impl Into<border::Radius>,
) -> Self {
self.border_radius = border_radius.into();
self
}
}
/// Computes the layout of an [`Image`].
pub fn layout<Renderer, Handle>(
renderer: &Renderer,
limits: &layout::Limits,
handle: &Handle,
width: Length,
height: Length,
region: Option<Rectangle<u32>>,
content_fit: ContentFit,
rotation: Rotation,
expand: bool,
) -> layout::Node
where
Renderer: image::Renderer<Handle = Handle>,
{
// The raw w/h of the underlying image
let image_size =
crop(renderer.measure_image(handle).unwrap_or_default(), region);
// The rotated size of the image
let rotated_size = rotation.apply(image_size);
// The size to be available to the widget prior to `Shrink`ing
let bounds = if expand {
limits.width(width).height(height).max()
} else {
limits.resolve(width, height, rotated_size)
};
// The uncropped size of the image when fit to the bounds above
let full_size = content_fit.fit(rotated_size, bounds);
// Shrink the widget to fit the resized image, if requested
let final_size = Size {
width: match width {
Length::Shrink => f32::min(bounds.width, full_size.width),
_ => bounds.width,
},
height: match height {
Length::Shrink => f32::min(bounds.height, full_size.height),
_ => bounds.height,
},
};
layout::Node::new(final_size)
}
fn drawing_bounds<Renderer, Handle>(
renderer: &Renderer,
bounds: Rectangle,
handle: &Handle,
region: Option<Rectangle<u32>>,
content_fit: ContentFit,
rotation: Rotation,
scale: f32,
) -> Rectangle
where
Renderer: image::Renderer<Handle = Handle>,
{
let original_size = renderer.measure_image(handle).unwrap_or_default();
let image_size = crop(original_size, region);
let rotated_size = rotation.apply(image_size);
let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
let fit_scale = Vector::new(
adjusted_fit.width / rotated_size.width,
adjusted_fit.height / rotated_size.height,
);
let final_size = image_size * fit_scale * scale;
let (crop_offset, final_size) = if let Some(region) = region {
let x = region.x.min(original_size.width) as f32;
let y = region.y.min(original_size.height) as f32;
let width = image_size.width;
let height = image_size.height;
let ratio = Vector::new(
original_size.width as f32 / width,
original_size.height as f32 / height,
);
let final_size = final_size * ratio;
let scale = Vector::new(
final_size.width / original_size.width as f32,
final_size.height / original_size.height as f32,
);
let offset = match content_fit {
ContentFit::None => Vector::new(x * scale.x, y * scale.y),
_ => Vector::new(
((original_size.width as f32 - width) / 2.0 - x) * scale.x,
((original_size.height as f32 - height) / 2.0 - y) * scale.y,
),
};
(offset, final_size)
} else {
(Vector::ZERO, final_size)
};
let position = match content_fit {
ContentFit::None => Point::new(
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
),
_ => Point::new(
bounds.center_x() - final_size.width / 2.0,
bounds.center_y() - final_size.height / 2.0,
),
};
Rectangle::new(position + crop_offset, final_size)
}
fn crop(size: Size<u32>, region: Option<Rectangle<u32>>) -> Size<f32> {
if let Some(region) = region {
Size::new(
region.width.min(size.width) as f32,
region.height.min(size.height) as f32,
)
} else {
Size::new(size.width as f32, size.height as f32)
}
}
/// Draws an [`Image`]
pub fn draw<Renderer, Handle>(
renderer: &mut Renderer,
layout: Layout<'_>,
handle: &Handle,
crop: Option<Rectangle<u32>>,
border_radius: border::Radius,
content_fit: ContentFit,
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
scale: f32,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
{
let bounds = layout.bounds();
let drawing_bounds = drawing_bounds(
renderer,
bounds,
handle,
crop,
content_fit,
rotation,
scale,
);
renderer.draw_image(
image::Image {
handle: handle.clone(),
border_radius,
filter_method,
rotation: rotation.radians(),
opacity,
snap: true,
},
drawing_bounds,
bounds,
);
}
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Image<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
&self.handle,
self.width,
self.height,
self.crop,
self.content_fit,
self.rotation,
self.expand,
)
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
renderer,
layout,
&self.handle,
self.crop,
self.border_radius,
self.content_fit,
self.filter_method,
self.rotation,
self.opacity,
self.scale,
);
}
}
impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + 'a,
{
fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
Element::new(image)
}
}
+447
View File
@@ -0,0 +1,447 @@
//! Zoom and pan on an image.
use crate::core::border;
use crate::core::image::{self, FilterMethod};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels,
Point, Radians, Rectangle, Shell, Size, Vector, Widget,
};
/// A frame that displays an image with the ability to zoom in/out and pan.
pub struct Viewer<Handle> {
padding: f32,
width: Length,
height: Length,
min_scale: f32,
max_scale: f32,
scale_step: f32,
handle: Handle,
filter_method: FilterMethod,
content_fit: ContentFit,
}
impl<Handle> Viewer<Handle> {
/// Creates a new [`Viewer`] with the given [`State`].
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Viewer {
handle: handle.into(),
padding: 0.0,
width: Length::Shrink,
height: Length::Shrink,
min_scale: 0.25,
max_scale: 10.0,
scale_step: 0.10,
filter_method: FilterMethod::default(),
content_fit: ContentFit::default(),
}
}
/// Sets the [`FilterMethod`] of the [`Viewer`].
pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
self.filter_method = filter_method;
self
}
/// Sets the [`ContentFit`] of the [`Viewer`].
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
self.content_fit = content_fit;
self
}
/// 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, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Viewer<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
// The raw w/h of the underlying image
let image_size =
renderer.measure_image(&self.handle).unwrap_or_default();
let image_size =
Size::new(image_size.width as f32, image_size.height as f32);
// The size to be available to the widget prior to `Shrink`ing
let raw_size = limits.resolve(self.width, self.height, 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 update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
let Some(cursor_position) = cursor.position_over(bounds) else {
return;
};
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 scaled_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);
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 scaled_size.width > bounds.width {
state.current_offset.x + adjustment.x
} else {
0.0
},
if scaled_size.height > bounds.height {
state.current_offset.y + adjustment.y
} else {
0.0
},
);
}
}
}
shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let Some(cursor_position) = cursor.position_over(bounds) else {
return;
};
let state = tree.state.downcast_mut::<State>();
state.cursor_grabbed_at = Some(cursor_position);
state.starting_offset = state.current_offset;
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree.state.downcast_mut::<State>();
state.cursor_grabbed_at = None;
}
Event::Mouse(mouse::Event::CursorMoved { position }) => {
let state = tree.state.downcast_mut::<State>();
if let Some(origin) = state.cursor_grabbed_at {
let scaled_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);
let hidden_width = (scaled_size.width - bounds.width / 2.0)
.max(0.0)
.round();
let hidden_height = (scaled_size.height
- bounds.height / 2.0)
.max(0.0)
.round();
let delta = *position - origin;
let x = if bounds.width < scaled_size.width {
(state.starting_offset.x - delta.x)
.clamp(-hidden_width, hidden_width)
} else {
0.0
};
let y = if bounds.height < scaled_size.height {
(state.starting_offset.y - delta.y)
.clamp(-hidden_height, hidden_height)
} else {
0.0
};
state.current_offset = Vector::new(x, y);
shell.request_redraw();
shell.capture_event();
}
}
_ => {}
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if state.is_cursor_grabbed() {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::None
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let final_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);
let translation = {
let diff_w = bounds.width - final_size.width;
let diff_h = bounds.height - final_size.height;
let image_top_left = match self.content_fit {
ContentFit::None => {
Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
}
_ => Vector::new(diff_w / 2.0, diff_h / 2.0),
};
image_top_left - state.offset(bounds, final_size)
};
let drawing_bounds = Rectangle::new(bounds.position(), final_size);
let render = |renderer: &mut Renderer| {
renderer.with_translation(translation, |renderer| {
renderer.draw_image(
Image {
handle: self.handle.clone(),
border_radius: border::Radius::default(),
filter_method: self.filter_method,
rotation: Radians(0.0),
opacity: 1.0,
snap: true,
},
drawing_bounds,
*viewport - translation,
);
});
};
renderer.with_layer(bounds, render);
}
}
/// 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, Theme, Renderer, Handle> From<Viewer<Handle>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: 'a + image::Renderer<Handle = Handle>,
Message: 'a,
Handle: Clone + 'a,
{
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, 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 scaled_image_size<Renderer>(
renderer: &Renderer,
handle: &<Renderer as image::Renderer>::Handle,
state: &State,
bounds: Size,
content_fit: ContentFit,
) -> Size
where
Renderer: image::Renderer,
{
let Size { width, height } =
renderer.measure_image(handle).unwrap_or_default();
let image_size = Size::new(width as f32, height as f32);
let adjusted_fit = content_fit.fit(image_size, bounds);
Size::new(
adjusted_fit.width * state.scale,
adjusted_fit.height * state.scale,
)
}
+75
View File
@@ -0,0 +1,75 @@
//! Keyed widgets can provide hints to ensure continuity.
//!
//! # What is continuity?
//! Continuity is the feeling of persistence of state.
//!
//! In a graphical user interface, users expect widgets to have a
//! certain degree of continuous state. For instance, a text input
//! that is focused should stay focused even if the widget tree
//! changes slightly.
//!
//! Continuity is tricky in `iced` and the Elm Architecture because
//! the whole widget tree is rebuilt during every `view` call. This is
//! very convenient from a developer perspective because you can build
//! extremely dynamic interfaces without worrying about changing state.
//!
//! However, the tradeoff is that determining what changed becomes hard
//! for `iced`. If you have a list of things, adding an element at the
//! top may cause a loss of continuity on every element on the list!
//!
//! # How can we keep continuity?
//! The good news is that user interfaces generally have a static widget
//! structure. This structure can be relied on to ensure some degree of
//! continuity. `iced` already does this.
//!
//! However, sometimes you have a certain part of your interface that is
//! quite dynamic. For instance, a list of things where items may be added
//! or removed at any place.
//!
//! There are different ways to mitigate this during the reconciliation
//! stage, but they involve comparing trees at certain depths and
//! backtracking... Quite computationally expensive.
//!
//! One approach that is cheaper consists in letting the user provide some hints
//! about the identities of the different widgets so that they can be compared
//! directly without going deeper.
//!
//! The widgets in this module will all ask for a "hint" of some sort. In order
//! to help them keep continuity, you need to make sure the hint stays the same
//! for the same items in your user interface between `view` calls.
pub mod column;
pub use column::Column;
/// Creates a keyed [`Column`] with the given children.
///
/// Keyed columns distribute content vertically while keeping continuity.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::keyed_column;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// keyed_column![
/// (0, "Item 0"),
/// (1, "Item 1"),
/// (2, "Item 2"),
/// ].into()
/// }
/// ```
#[macro_export]
macro_rules! keyed_column {
() => (
$crate::keyed::Column::new()
);
($(($key:expr, $x:expr)),+ $(,)?) => (
$crate::keyed::Column::with_children(vec![$(($key, $crate::core::Element::from($x))),+])
);
}
+398
View File
@@ -0,0 +1,398 @@
//! Keyed columns distribute content vertically while keeping continuity.
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::Operation;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Alignment, Clipboard, Element, Event, Layout, Length, Padding, Pixels,
Rectangle, Shell, Size, Vector, Widget,
};
/// A container that distributes its contents vertically while keeping continuity.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::{keyed_column, text};
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// keyed_column((0..=100).map(|i| {
/// (i, text!("Item {i}").into())
/// })).into()
/// }
/// ```
pub struct Column<
'a,
Key,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Key: Copy + PartialEq,
{
spacing: f32,
padding: Padding,
width: Length,
height: Length,
max_width: f32,
align_items: Alignment,
keys: Vec<Key>,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
impl<'a, Key, Message, Theme, Renderer>
Column<'a, Key, Message, Theme, Renderer>
where
Key: Copy + PartialEq,
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Column`].
pub fn new() -> Self {
Self::from_vecs(Vec::new(), Vec::new())
}
/// Creates a [`Column`] from already allocated [`Vec`]s.
///
/// Keep in mind that the [`Column`] will not inspect the [`Vec`]s, which means
/// it won't automatically adapt to the sizing strategy of its contents.
///
/// If any of the children have a [`Length::Fill`] strategy, you will need to
/// call [`Column::width`] or [`Column::height`] accordingly.
pub fn from_vecs(
keys: Vec<Key>,
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
keys,
children,
}
}
/// Creates a [`Column`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vecs(
Vec::with_capacity(capacity),
Vec::with_capacity(capacity),
)
}
/// Creates a [`Column`] with the given elements.
pub fn with_children(
children: impl IntoIterator<
Item = (Key, Element<'a, Message, Theme, Renderer>),
>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// 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,
key: Key,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
self.keys.push(key);
self.children.push(child);
self
}
/// Adds an element to the [`Column`], if `Some`.
pub fn push_maybe(
self,
key: Key,
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
) -> Self {
if let Some(child) = child {
self.push(key, child)
} else {
self
}
}
/// Extends the [`Column`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<
Item = (Key, Element<'a, Message, Theme, Renderer>),
>,
) -> Self {
children
.into_iter()
.fold(self, |column, (key, child)| column.push(key, child))
}
}
impl<Key, Message, Renderer> Default for Column<'_, Key, Message, Renderer>
where
Key: Copy + PartialEq,
Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
struct State<Key>
where
Key: Copy + PartialEq,
{
keys: Vec<Key>,
}
impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Column<'_, Key, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
Key: Copy + PartialEq + 'static,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Key>>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
keys: self.keys.clone(),
})
}
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&self, tree: &mut Tree) {
let Tree {
state, children, ..
} = tree;
let state = state.downcast_mut::<State<Key>>();
tree::diff_children_custom_with_search(
children,
&self.children,
|tree, child| child.as_widget().diff(tree),
|index| {
self.keys.get(index).or_else(|| self.keys.last()).copied()
!= Some(state.keys[index])
},
|child| Tree::new(child.as_widget()),
);
if state.keys != self.keys {
state.keys.clone_from(&self.keys);
}
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
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.width,
self.height,
self.padding,
self.spacing,
self.align_items,
&mut self.children,
&mut tree.children,
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((child, tree), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
child.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, tree), layout)| {
child
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
{
child
.as_widget()
.draw(state, renderer, theme, style, layout, cursor, viewport);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Key, Message, Theme, Renderer>
From<Column<'a, Key, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Key: Copy + PartialEq + 'static,
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(column: Column<'a, Key, Message, Theme, Renderer>) -> Self {
Self::new(column)
}
}
+417
View File
@@ -0,0 +1,417 @@
#![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)]
pub(crate) mod helpers;
pub mod component;
#[allow(deprecated)]
pub use component::Component;
mod cache;
use crate::core::Element;
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::{
self, Clipboard, Event, Length, Rectangle, Shell, Size, Vector,
};
use ouroboros::self_referencing;
use rustc_hash::FxHasher;
use std::cell::RefCell;
use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
/// A widget that only rebuilds its contents when necessary.
#[cfg(feature = "lazy")]
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
dependency: Dependency,
view: Box<dyn Fn(&Dependency) -> View + 'a>,
element: RefCell<
Option<Rc<RefCell<Option<Element<'static, Message, Theme, Renderer>>>>>,
>,
}
impl<'a, Message, Theme, Renderer, Dependency, View>
Lazy<'a, Message, Theme, Renderer, Dependency, View>
where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Theme, Renderer>>,
{
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
/// closure that can turn this data into a widget tree.
pub fn new(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
) -> Self {
Self {
dependency,
view: Box::new(view),
element: RefCell::new(None),
}
}
fn with_element<T>(
&self,
f: impl FnOnce(&Element<'_, Message, Theme, 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, Theme, Renderer>) -> T,
) -> T {
f(self
.element
.borrow()
.as_ref()
.unwrap()
.borrow_mut()
.as_mut()
.unwrap())
}
}
struct Internal<Message, Theme, Renderer> {
element: Rc<RefCell<Option<Element<'static, Message, Theme, Renderer>>>>,
hash: u64,
}
impl<'a, Message, Theme, Renderer, Dependency, View>
Widget<Message, Theme, Renderer>
for Lazy<'a, Message, Theme, Renderer, Dependency, View>
where
View: Into<Element<'static, Message, Theme, Renderer>> + 'static,
Dependency: Hash + 'a,
Message: 'static,
Theme: '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 hash = {
let mut hasher = FxHasher::default();
self.dependency.hash(&mut hasher);
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, Theme, Renderer>>();
let new_hash = {
let mut hasher = FxHasher::default();
self.dependency.hash(&mut hasher);
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 size(&self) -> Size<Length> {
self.with_element(|element| element.as_widget().size())
}
fn size_hint(&self) -> Size<Length> {
Size {
width: Length::Shrink,
height: Length::Shrink,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element_mut(|element| {
element.as_widget_mut().layout(
&mut tree.children[0],
renderer,
limits,
)
})
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.with_element_mut(|element| {
element.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.with_element_mut(|element| {
element.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
});
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_element(|element| {
element.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
})
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.with_element(|element| {
element.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
});
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let overlay = 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],
layout,
overlay_builder: |element, tree, layout| {
element
.as_widget_mut()
.overlay(tree, *layout, renderer, viewport, translation)
.map(|overlay| RefCell::new(overlay::Nested::new(overlay)))
},
}
.build();
#[allow(clippy::redundant_closure_for_method_calls)]
if overlay.with_overlay(|overlay| overlay.is_some()) {
Some(overlay::Element::new(Box::new(Overlay(Some(overlay)))))
} else {
let heads = overlay.into_heads();
// - You may not like it, but this is what peak performance looks like
// - TODO: Get rid of ouroboros, for good
// - What?!
*self.element.borrow().as_ref().unwrap().borrow_mut() =
Some(heads.element);
None
}
}
}
#[self_referencing]
struct Inner<'a, Message: 'a, Theme: 'a, Renderer: 'a> {
cell: Rc<RefCell<Option<Element<'static, Message, Theme, Renderer>>>>,
element: Element<'static, Message, Theme, Renderer>,
tree: &'a mut Tree,
layout: Layout<'a>,
#[borrows(mut element, mut tree, layout)]
#[not_covariant]
overlay: Option<RefCell<overlay::Nested<'this, Message, Theme, Renderer>>>,
}
struct Overlay<'a, Message, Theme, Renderer>(
Option<Inner<'a, Message, Theme, Renderer>>,
);
impl<Message, Theme, Renderer> Drop for Overlay<'_, Message, Theme, Renderer> {
fn drop(&mut self) {
let heads = self.0.take().unwrap().into_heads();
(*heads.cell.borrow_mut()) = Some(heads.element);
}
}
impl<Message, Theme, Renderer> Overlay<'_, Message, Theme, Renderer> {
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T,
) -> Option<T> {
self.0.as_ref().unwrap().with_overlay(|overlay| {
overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
})
}
fn with_overlay_mut_maybe<T>(
&mut self,
f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T,
) -> Option<T> {
self.0.as_mut().unwrap().with_overlay_mut(|overlay| {
overlay.as_mut().map(|nested| (f)(nested.get_mut()))
})
}
}
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
self.with_overlay_maybe(|overlay| overlay.layout(renderer, bounds))
.unwrap_or_default()
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let _ = self.with_overlay_maybe(|overlay| {
overlay.draw(renderer, theme, style, layout, cursor);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
overlay.mouse_interaction(layout, cursor, renderer)
})
.unwrap_or_default()
}
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let _ = self.with_overlay_mut_maybe(|overlay| {
overlay.update(event, layout, cursor, renderer, clipboard, shell);
});
}
}
impl<'a, Message, Theme, Renderer, Dependency, View>
From<Lazy<'a, Message, Theme, Renderer, Dependency, View>>
for Element<'a, Message, Theme, Renderer>
where
View: Into<Element<'static, Message, Theme, Renderer>> + 'static,
Renderer: core::Renderer + 'static,
Message: 'static,
Theme: 'static,
Dependency: Hash + 'a,
{
fn from(
lazy: Lazy<'a, Message, Theme, Renderer, Dependency, View>,
) -> Self {
Self::new(lazy)
}
}
+14
View File
@@ -0,0 +1,14 @@
#![allow(dead_code)]
use crate::core::Element;
use crate::core::overlay;
use ouroboros::self_referencing;
#[self_referencing(pub_extras)]
pub struct Cache<'a, Message: 'a, Theme: 'a, Renderer: 'a> {
pub element: Element<'a, Message, Theme, Renderer>,
#[borrows(mut element)]
#[covariant]
overlay: Option<overlay::Element<'this, Message, Theme, Renderer>>,
}
+669
View File
@@ -0,0 +1,669 @@
//! Build and reuse custom widgets using The Elm Architecture.
#![allow(deprecated)]
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, Rectangle, Shell, Size, Vector, Widget,
};
use ouroboros::self_referencing;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
/// A reusable, custom widget that uses The Elm Architecture.
///
/// A [`Component`] allows you to implement custom widgets as if they were
/// `iced` applications with encapsulated state.
///
/// In other words, a [`Component`] allows you to turn `iced` applications into
/// custom widgets and embed them without cumbersome wiring.
///
/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event)
/// and update the internal state of the [`Component`].
///
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
/// the parent application of any relevant interactions.
///
/// # State
/// A component can store its state in one of two ways: either as data within the
/// implementor of the trait, or in a type [`State`][Component::State] that is managed
/// by the runtime and provided to the trait methods. These two approaches are not
/// mutually exclusive and have opposite pros and cons.
///
/// For instance, if a piece of state is needed by multiple components that reside
/// in different branches of the tree, then it's more convenient to let a common
/// ancestor store it and pass it down.
///
/// On the other hand, if a piece of state is only needed by the component itself,
/// you can store it as part of its internal [`State`][Component::State].
#[cfg(feature = "lazy")]
#[deprecated(
since = "0.13.0",
note = "components introduce encapsulated state and hamper the use of a single source of truth. \
Instead, leverage the Elm Architecture directly, or implement a custom widget"
)]
pub trait Component<Message, Theme = crate::Theme, Renderer = crate::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, Theme, Renderer>;
/// Update the [`Component`] state based on the provided [`Operation`](widget::Operation)
///
/// By default, it does nothing.
fn operate(
&self,
_bounds: Rectangle,
_state: &mut Self::State,
_operation: &mut dyn widget::Operation,
) {
}
/// Returns a [`Size`] hint for laying out the [`Component`].
///
/// This hint may be used by some widget containers to adjust their sizing strategy
/// during construction.
fn size_hint(&self) -> Size<Length> {
Size {
width: Length::Shrink,
height: Length::Shrink,
}
}
}
struct Tag<T>(T);
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
pub fn view<'a, C, Message, Theme, Renderer>(
component: C,
) -> Element<'a, Message, Theme, Renderer>
where
C: Component<Message, Theme, Renderer> + 'a,
C::State: 'static,
Message: 'a,
Theme: 'a,
Renderer: core::Renderer + 'a,
{
Element::new(Instance {
state: RefCell::new(Some(
StateBuilder {
component: Box::new(component),
message: PhantomData,
state: PhantomData,
element_builder: |_| None,
}
.build(),
)),
tree: RefCell::new(Rc::new(RefCell::new(None))),
})
}
struct Instance<'a, Message, Theme, Renderer, Event, S> {
state: RefCell<Option<State<'a, Message, Theme, Renderer, Event, S>>>,
tree: RefCell<Rc<RefCell<Option<Tree>>>>,
}
#[self_referencing]
struct State<'a, Message: 'a, Theme: 'a, Renderer: 'a, Event: 'a, S: 'a> {
component: Box<
dyn Component<Message, Theme, Renderer, Event = Event, State = S> + 'a,
>,
message: PhantomData<Message>,
state: PhantomData<S>,
#[borrows(component)]
#[covariant]
element: Option<Element<'this, Event, Theme, Renderer>>,
}
impl<Message, Theme, Renderer, Event, S>
Instance<'_, Message, Theme, Renderer, Event, S>
where
S: Default + 'static,
Renderer: renderer::Renderer,
{
fn diff_self(&self) {
self.with_element(|element| {
self.tree
.borrow_mut()
.borrow_mut()
.as_mut()
.unwrap()
.diff_children(std::slice::from_ref(&element));
});
}
fn rebuild_element_if_necessary(&self) {
let inner = self.state.borrow_mut().take().unwrap();
if inner.borrow_element().is_none() {
let heads = inner.into_heads();
*self.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |component| {
Some(
component.view(
self.tree
.borrow()
.borrow()
.as_ref()
.unwrap()
.state
.downcast_ref::<S>(),
),
)
},
}
.build(),
);
self.diff_self();
} else {
*self.state.borrow_mut() = Some(inner);
}
}
fn rebuild_element_with_operation(
&self,
layout: Layout<'_>,
operation: &mut dyn widget::Operation,
) {
let heads = self.state.borrow_mut().take().unwrap().into_heads();
heads.component.operate(
layout.bounds(),
self.tree
.borrow_mut()
.borrow_mut()
.as_mut()
.unwrap()
.state
.downcast_mut(),
operation,
);
*self.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |component| {
Some(
component.view(
self.tree
.borrow()
.borrow()
.as_ref()
.unwrap()
.state
.downcast_ref(),
),
)
},
}
.build(),
);
self.diff_self();
}
fn with_element<T>(
&self,
f: impl FnOnce(&Element<'_, Event, Theme, Renderer>) -> T,
) -> T {
self.with_element_mut(|element| f(element))
}
fn with_element_mut<T>(
&self,
f: impl FnOnce(&mut Element<'_, Event, Theme, Renderer>) -> T,
) -> T {
self.rebuild_element_if_necessary();
self.state
.borrow_mut()
.as_mut()
.unwrap()
.with_element_mut(|element| f(element.as_mut().unwrap()))
}
}
impl<Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer>
for Instance<'_, Message, Theme, Renderer, Event, S>
where
S: 'static + Default,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Tag<S>>()
}
fn state(&self) -> tree::State {
let state = Rc::new(RefCell::new(Some(Tree {
tag: tree::Tag::of::<Tag<S>>(),
state: tree::State::new(S::default()),
children: vec![Tree::empty()],
})));
*self.tree.borrow_mut() = state.clone();
self.diff_self();
tree::State::new(state)
}
fn children(&self) -> Vec<Tree> {
vec![]
}
fn diff(&self, tree: &mut Tree) {
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
*self.tree.borrow_mut() = tree.clone();
self.rebuild_element_if_necessary();
}
fn size(&self) -> Size<Length> {
self.with_element(|element| element.as_widget().size())
}
fn size_hint(&self) -> Size<Length> {
self.state
.borrow()
.as_ref()
.expect("Borrow instance state")
.borrow_component()
.size_hint()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
self.with_element_mut(|element| {
element.as_widget_mut().layout(
&mut t.borrow_mut().as_mut().unwrap().children[0],
renderer,
limits,
)
})
}
fn update(
&mut self,
tree: &mut Tree,
event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
self.with_element_mut(|element| {
element.as_widget_mut().update(
&mut t.borrow_mut().as_mut().unwrap().children[0],
event,
layout,
cursor,
renderer,
clipboard,
&mut local_shell,
viewport,
);
});
if local_shell.is_event_captured() {
shell.capture_event();
}
local_shell.revalidate_layout(|| shell.invalidate_layout());
shell.request_redraw_at(local_shell.redraw_request());
shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut heads = self.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| {
heads.component.update(
t.borrow_mut().as_mut().unwrap().state.downcast_mut(),
message,
)
}) {
shell.publish(message);
}
self.state = RefCell::new(Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |_| None,
}
.build(),
));
shell.invalidate_layout();
shell.request_redraw();
}
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.rebuild_element_with_operation(layout, operation);
let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
self.with_element_mut(|element| {
element.as_widget_mut().operate(
&mut tree.borrow_mut().as_mut().unwrap().children[0],
layout,
renderer,
operation,
);
});
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| {
element.as_widget().draw(
&tree.borrow().as_ref().unwrap().children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
});
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| {
element.as_widget().mouse_interaction(
&tree.borrow().as_ref().unwrap().children[0],
layout,
cursor,
viewport,
renderer,
)
})
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.rebuild_element_if_necessary();
let state = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
let tree = state.borrow_mut().take().unwrap();
let overlay = InnerBuilder {
instance: self,
tree,
types: PhantomData,
overlay_builder: |instance, tree| {
instance.state.get_mut().as_mut().unwrap().with_element_mut(
move |element| {
element
.as_mut()
.unwrap()
.as_widget_mut()
.overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
.map(|overlay| {
RefCell::new(overlay::Nested::new(overlay))
})
},
)
},
}
.build();
#[allow(clippy::redundant_closure_for_method_calls)]
if overlay.with_overlay(|overlay| overlay.is_some()) {
Some(overlay::Element::new(Box::new(OverlayInstance {
overlay: Some(Overlay(Some(overlay))), // Beautiful, I know
})))
} else {
let heads = overlay.into_heads();
// - You may not like it, but this is what peak performance looks like
// - TODO: Get rid of ouroboros, for good
// - What?!
*state.borrow_mut() = Some(heads.tree);
None
}
}
}
struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>(
Option<Inner<'a, 'b, Message, Theme, Renderer, Event, S>>,
);
impl<Message, Theme, Renderer, Event, S> Drop
for Overlay<'_, '_, Message, Theme, Renderer, Event, S>
{
fn drop(&mut self) {
if let Some(heads) = self.0.take().map(Inner::into_heads) {
*heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree);
}
}
}
#[self_referencing]
struct Inner<'a, 'b, Message, Theme, Renderer, Event, S> {
instance: &'a mut Instance<'b, Message, Theme, Renderer, Event, S>,
tree: Tree,
types: PhantomData<(Message, Event, S)>,
#[borrows(mut instance, mut tree)]
#[not_covariant]
overlay: Option<RefCell<overlay::Nested<'this, Event, Theme, Renderer>>>,
}
struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> {
overlay: Option<Overlay<'a, 'b, Message, Theme, Renderer, Event, S>>,
}
impl<Message, Theme, Renderer, Event, S>
OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
{
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T,
) -> Option<T> {
self.overlay
.as_ref()
.unwrap()
.0
.as_ref()
.unwrap()
.with_overlay(|overlay| {
overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
})
}
fn with_overlay_mut_maybe<T>(
&mut self,
f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T,
) -> Option<T> {
self.overlay
.as_mut()
.unwrap()
.0
.as_mut()
.unwrap()
.with_overlay_mut(|overlay| {
overlay.as_mut().map(|nested| (f)(nested.get_mut()))
})
}
}
impl<Message, Theme, Renderer, Event, S>
overlay::Overlay<Message, Theme, Renderer>
for OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
where
Renderer: core::Renderer,
S: 'static + Default,
{
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
self.with_overlay_maybe(|overlay| overlay.layout(renderer, bounds))
.unwrap_or_default()
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let _ = self.with_overlay_maybe(|overlay| {
overlay.draw(renderer, theme, style, layout, cursor);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
overlay.mouse_interaction(layout, cursor, renderer)
})
.unwrap_or_default()
}
fn update(
&mut self,
event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
let _ = self.with_overlay_mut_maybe(|overlay| {
overlay.update(
event,
layout,
cursor,
renderer,
clipboard,
&mut local_shell,
);
});
if local_shell.is_event_captured() {
shell.capture_event();
}
local_shell.revalidate_layout(|| shell.invalidate_layout());
shell.request_redraw_at(local_shell.redraw_request());
shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut inner =
self.overlay.take().unwrap().0.take().unwrap().into_heads();
let mut heads = inner.instance.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| {
heads
.component
.update(inner.tree.state.downcast_mut(), message)
}) {
shell.publish(message);
}
*inner.instance.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
element_builder: |_| None,
}
.build(),
);
self.overlay = Some(Overlay(Some(
InnerBuilder {
instance: inner.instance,
tree: inner.tree,
types: PhantomData,
overlay_builder: |_, _| None,
}
.build(),
)));
shell.invalidate_layout();
}
}
}
+43
View File
@@ -0,0 +1,43 @@
use crate::core::{self, Element};
use crate::lazy::component;
use std::hash::Hash;
#[allow(deprecated)]
pub use crate::lazy::{Component, Lazy};
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
/// closure that can turn this data into a widget tree.
#[cfg(feature = "lazy")]
pub fn lazy<'a, Message, Theme, Renderer, Dependency, View>(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
) -> Lazy<'a, Message, Theme, Renderer, Dependency, View>
where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Theme, Renderer>>,
{
Lazy::new(dependency, view)
}
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
#[cfg(feature = "lazy")]
#[deprecated(
since = "0.13.0",
note = "components introduce encapsulated state and hamper the use of a single source of truth. \
Instead, leverage the Elm Architecture directly, or implement a custom widget"
)]
#[allow(deprecated)]
pub fn component<'a, C, Message, Theme, Renderer>(
component: C,
) -> Element<'a, Message, Theme, Renderer>
where
C: Component<Message, Theme, Renderer> + 'a,
C::State: 'static,
Message: 'a,
Theme: 'a,
Renderer: core::Renderer + 'a,
{
component::view(component)
}
+153
View File
@@ -0,0 +1,153 @@
//! Use the built-in widgets or create your own.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use iced_renderer as renderer;
pub use iced_renderer::core;
pub use iced_renderer::graphics;
pub use core::widget::Id;
mod action;
mod column;
mod mouse_area;
mod pin;
mod responsive;
mod stack;
mod themer;
pub mod button;
pub mod checkbox;
pub mod combo_box;
pub mod container;
pub mod float;
pub mod grid;
pub mod keyed;
pub mod overlay;
pub mod pane_grid;
pub mod pick_list;
pub mod progress_bar;
pub mod radio;
pub mod row;
pub mod rule;
pub mod scrollable;
pub mod sensor;
pub mod slider;
pub mod space;
pub mod table;
pub mod text;
pub mod text_editor;
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::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 combo_box::ComboBox;
#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
pub use float::Float;
#[doc(no_inline)]
pub use grid::Grid;
#[doc(no_inline)]
pub use mouse_area::MouseArea;
#[doc(no_inline)]
pub use pane_grid::PaneGrid;
#[doc(no_inline)]
pub use pick_list::PickList;
#[doc(no_inline)]
pub use pin::Pin;
#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
#[doc(no_inline)]
pub use responsive::Responsive;
#[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 sensor::Sensor;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
pub use space::Space;
#[doc(no_inline)]
pub use stack::Stack;
#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
pub use text_editor::TextEditor;
#[doc(no_inline)]
pub use text_input::TextInput;
#[doc(no_inline)]
pub use themer::Themer;
#[doc(no_inline)]
pub use toggler::Toggler;
#[doc(no_inline)]
pub use tooltip::Tooltip;
#[doc(no_inline)]
pub use vertical_slider::VerticalSlider;
#[cfg(feature = "wgpu")]
pub mod shader;
#[cfg(feature = "wgpu")]
#[doc(no_inline)]
pub use shader::Shader;
#[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;
#[cfg(feature = "markdown")]
pub mod markdown;
pub use crate::core::theme::{self, Theme};
pub use action::Action;
pub use renderer::Renderer;
File diff suppressed because it is too large Load Diff
+437
View File
@@ -0,0 +1,437 @@
//! A container for capturing mouse events.
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{Operation, Tree, tree};
use crate::core::{
Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size,
Vector, Widget,
};
/// Emit messages on mouse events.
pub struct MouseArea<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
content: Element<'a, Message, Theme, Renderer>,
on_press: Option<Message>,
on_release: Option<Message>,
on_double_click: Option<Message>,
on_right_press: Option<Message>,
on_right_release: Option<Message>,
on_middle_press: Option<Message>,
on_middle_release: Option<Message>,
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
on_enter: Option<Message>,
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
/// The message to emit on a left button press.
#[must_use]
pub fn on_press(mut self, message: Message) -> Self {
self.on_press = Some(message);
self
}
/// The message to emit on a left button release.
#[must_use]
pub fn on_release(mut self, message: Message) -> Self {
self.on_release = Some(message);
self
}
/// The message to emit on a double click.
///
/// If you use this with [`on_press`]/[`on_release`], those
/// event will be emit as normal.
///
/// The events stream will be: on_press -> on_release -> on_press
/// -> on_double_click -> on_release -> on_press ...
///
/// [`on_press`]: Self::on_press
/// [`on_release`]: Self::on_release
#[must_use]
pub fn on_double_click(mut self, message: Message) -> Self {
self.on_double_click = Some(message);
self
}
/// The message to emit on a right button press.
#[must_use]
pub fn on_right_press(mut self, message: Message) -> Self {
self.on_right_press = Some(message);
self
}
/// The message to emit on a right button release.
#[must_use]
pub fn on_right_release(mut self, message: Message) -> Self {
self.on_right_release = Some(message);
self
}
/// The message to emit on a middle button press.
#[must_use]
pub fn on_middle_press(mut self, message: Message) -> Self {
self.on_middle_press = Some(message);
self
}
/// The message to emit on a middle button release.
#[must_use]
pub fn on_middle_release(mut self, message: Message) -> Self {
self.on_middle_release = Some(message);
self
}
/// The message to emit when scroll wheel is used
#[must_use]
pub fn on_scroll(
mut self,
on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a,
) -> Self {
self.on_scroll = Some(Box::new(on_scroll));
self
}
/// The message to emit when the mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: Message) -> Self {
self.on_enter = Some(message);
self
}
/// The message to emit when the mouse moves in the area.
#[must_use]
pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
self.on_move = Some(Box::new(on_move));
self
}
/// The message to emit when the mouse exits the area.
#[must_use]
pub fn on_exit(mut self, message: Message) -> Self {
self.on_exit = Some(message);
self
}
/// The [`mouse::Interaction`] to use when hovering the area.
#[must_use]
pub fn interaction(mut self, interaction: mouse::Interaction) -> Self {
self.interaction = Some(interaction);
self
}
}
/// Local state of the [`MouseArea`].
#[derive(Default)]
struct State {
is_hovered: bool,
bounds: Rectangle,
cursor_position: Option<Point>,
previous_click: Option<mouse::Click>,
}
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
/// Creates a [`MouseArea`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
MouseArea {
content: content.into(),
on_press: None,
on_release: None,
on_double_click: None,
on_right_press: None,
on_right_release: None,
on_middle_press: None,
on_middle_release: None,
on_scroll: None,
on_enter: None,
on_move: None,
on_exit: None,
interaction: None,
}
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for MouseArea<'_, Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
Message: Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget_mut().layout(
&mut tree.children[0],
renderer,
limits,
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
if shell.is_event_captured() {
return;
}
update(self, tree, event, layout, cursor, shell);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let content_interaction = self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
);
match (self.interaction, content_interaction) {
(Some(interaction), mouse::Interaction::None)
if cursor.is_over(layout.bounds()) =>
{
interaction
}
_ => content_interaction,
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
renderer_style,
layout,
cursor,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<MouseArea<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a,
Renderer: 'a + renderer::Renderer,
{
fn from(
area: MouseArea<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(area)
}
}
/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`]
/// accordingly.
fn update<Message: Clone, Theme, Renderer>(
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) {
let state: &mut State = tree.state.downcast_mut();
let cursor_position = cursor.position();
let bounds = layout.bounds();
if state.cursor_position != cursor_position || state.bounds != bounds {
let was_hovered = state.is_hovered;
state.is_hovered = cursor.is_over(layout.bounds());
state.cursor_position = cursor_position;
state.bounds = bounds;
match (
widget.on_enter.as_ref(),
widget.on_move.as_ref(),
widget.on_exit.as_ref(),
) {
(Some(on_enter), _, _) if state.is_hovered && !was_hovered => {
shell.publish(on_enter.clone());
}
(_, Some(on_move), _) if state.is_hovered => {
if let Some(position) = cursor.position_in(layout.bounds()) {
shell.publish(on_move(position));
}
}
(_, _, Some(on_exit)) if !state.is_hovered && was_hovered => {
shell.publish(on_exit.clone());
}
_ => {}
}
}
if !cursor.is_over(layout.bounds()) {
return;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(message) = widget.on_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
if let Some(position) = cursor_position
&& let Some(message) = widget.on_double_click.as_ref()
{
let new_click = mouse::Click::new(
position,
mouse::Button::Left,
state.previous_click,
);
if new_click.kind() == mouse::click::Kind::Double {
shell.publish(message.clone());
}
state.previous_click = Some(new_click);
// Even if this is not a double click, but the press is nevertheless
// processed by us and should not be popup to parent widgets.
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(message) = widget.on_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
if let Some(message) = widget.on_middle_press.as_ref() {
shell.publish(message.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => {
if let Some(message) = widget.on_middle_release.as_ref() {
shell.publish(message.clone());
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if let Some(on_scroll) = widget.on_scroll.as_ref() {
shell.publish(on_scroll(*delta));
shell.capture_event();
}
}
_ => {}
}
}
+2
View File
@@ -0,0 +1,2 @@
//! Display interactive elements on top of other widgets.
pub mod menu;
+661
View File
@@ -0,0 +1,661 @@
//! Build and show dropdown menus.
use crate::core::alignment;
use crate::core::border::{self, Border};
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::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Clipboard, Color, Event, Length, Padding, Pixels, Point,
Rectangle, Shadow, Size, Theme, Vector,
};
use crate::core::{Element, Shell, Widget};
use crate::scrollable::{self, Scrollable};
/// A list of selectable options.
pub struct Menu<
'a,
'b,
T,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: Catalog,
Renderer: text::Renderer,
'b: 'a,
{
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
width: f32,
padding: Padding,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
class: &'a <Theme as Catalog>::Class<'b>,
}
impl<'a, 'b, T, Message, Theme, Renderer>
Menu<'a, 'b, T, Message, Theme, Renderer>
where
T: ToString + Clone,
Message: 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
'b: 'a,
{
/// Creates a new [`Menu`] with the given [`State`], a list of options,
/// the message to produced when an option is selected, and its [`Style`].
pub fn new(
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: impl FnMut(T) -> Message + 'a,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
class: &'a <Theme as Catalog>::Class<'b>,
) -> Self {
Menu {
state,
options,
hovered_option,
on_selected: Box::new(on_selected),
on_option_hovered,
width: 0.0,
padding: Padding::ZERO,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::default(),
font: None,
class,
}
}
/// 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());
self
}
/// Sets the text [`text::LineHeight`] of the [`Menu`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
) -> Self {
self.text_line_height = line_height.into();
self
}
/// Sets the [`text::Shaping`] strategy of the [`Menu`].
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
self.text_shaping = shaping;
self
}
/// Sets the font of the [`Menu`].
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Turns the [`Menu`] into an overlay [`Element`] at the given target
/// position.
///
/// The `target_height` will be used to display the menu either on top
/// of the target or under it, depending on the screen position and the
/// dimensions of the [`Menu`].
pub fn overlay(
self,
position: Point,
viewport: Rectangle,
target_height: f32,
menu_height: Length,
) -> overlay::Element<'a, Message, Theme, Renderer> {
overlay::Element::new(Box::new(Overlay::new(
position,
viewport,
self,
target_height,
menu_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, 'b, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
position: Point,
viewport: Rectangle,
tree: &'a mut Tree,
list: Scrollable<'a, Message, Theme, Renderer>,
width: f32,
target_height: f32,
class: &'a <Theme as Catalog>::Class<'b>,
}
impl<'a, 'b, Message, Theme, Renderer> Overlay<'a, 'b, Message, Theme, Renderer>
where
Message: 'a,
Theme: Catalog + scrollable::Catalog + 'a,
Renderer: text::Renderer + 'a,
'b: 'a,
{
pub fn new<T>(
position: Point,
viewport: Rectangle,
menu: Menu<'a, 'b, T, Message, Theme, Renderer>,
target_height: f32,
menu_height: Length,
) -> Self
where
T: Clone + ToString,
{
let Menu {
state,
options,
hovered_option,
on_selected,
on_option_hovered,
width,
padding,
font,
text_size,
text_line_height,
text_shaping,
class,
} = menu;
let list = Scrollable::new(List {
options,
hovered_option,
on_selected,
on_option_hovered,
font,
text_size,
text_line_height,
text_shaping,
padding,
class,
})
.height(menu_height);
state.tree.diff(&list as &dyn Widget<_, _, _>);
Self {
position,
viewport,
tree: &mut state.tree,
list,
width,
target_height,
class,
}
}
}
impl<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer>
for Overlay<'_, '_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
let space_below =
bounds.height - (self.position.y + self.target_height);
let space_above = self.position.y;
let limits = layout::Limits::new(
Size::ZERO,
Size::new(
bounds.width - self.position.x,
if space_below > space_above {
space_below
} else {
space_above
},
),
)
.width(self.width);
let node = self.list.layout(self.tree, renderer, &limits);
let size = node.size();
node.move_to(if space_below > space_above {
self.position + Vector::new(0.0, self.target_height)
} else {
self.position - Vector::new(0.0, size.height)
})
}
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let bounds = layout.bounds();
self.list.update(
self.tree, event, layout, cursor, renderer, clipboard, shell,
&bounds,
);
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
self.list.mouse_interaction(
self.tree,
layout,
cursor,
&self.viewport,
renderer,
)
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let bounds = layout.bounds();
let style = Catalog::style(theme, self.class);
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
shadow: style.shadow,
..renderer::Quad::default()
},
style.background,
);
self.list.draw(
self.tree, renderer, theme, defaults, layout, cursor, &bounds,
);
}
}
struct List<'a, 'b, T, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
padding: Padding,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
class: &'a <Theme as Catalog>::Class<'b>,
}
struct ListState {
is_hovered: Option<bool>,
}
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for List<'_, '_, T, Message, Theme, Renderer>
where
T: Clone + ToString,
Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Option<bool>>()
}
fn state(&self) -> tree::State {
tree::State::new(ListState { is_hovered: None })
}
fn size(&self) -> Size<Length> {
Size {
width: Length::Fill,
height: Length::Shrink,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
use std::f32;
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let text_line_height = self.text_line_height.to_absolute(text_size);
let size = {
let intrinsic = Size::new(
0.0,
(f32::from(text_line_height) + self.padding.y())
* self.options.len() as f32,
);
limits.resolve(Length::Fill, Length::Shrink, intrinsic)
};
layout::Node::new(size)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if cursor.is_over(layout.bounds())
&& let Some(index) = *self.hovered_option
&& let Some(option) = self.options.get(index)
{
shell.publish((self.on_selected)(option.clone()));
shell.capture_event();
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor_position) =
cursor.position_in(layout.bounds())
{
let text_size = self
.text_size
.unwrap_or_else(|| renderer.default_size());
let option_height =
f32::from(self.text_line_height.to_absolute(text_size))
+ self.padding.y();
let new_hovered_option =
(cursor_position.y / option_height) as usize;
if *self.hovered_option != Some(new_hovered_option)
&& let Some(option) =
self.options.get(new_hovered_option)
{
if let Some(on_option_hovered) = self.on_option_hovered
{
shell.publish(on_option_hovered(option.clone()));
}
shell.request_redraw();
}
*self.hovered_option = Some(new_hovered_option);
}
}
Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) =
cursor.position_in(layout.bounds())
{
let text_size = self
.text_size
.unwrap_or_else(|| renderer.default_size());
let option_height =
f32::from(self.text_line_height.to_absolute(text_size))
+ self.padding.y();
*self.hovered_option =
Some((cursor_position.y / option_height) as usize);
if let Some(index) = *self.hovered_option
&& let Some(option) = self.options.get(index)
{
shell.publish((self.on_selected)(option.clone()));
shell.capture_event();
}
}
}
_ => {}
}
let state = tree.state.downcast_mut::<ListState>();
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
state.is_hovered = Some(cursor.is_over(layout.bounds()));
} else if state.is_hovered.is_some_and(|is_hovered| {
is_hovered != cursor.is_over(layout.bounds())
}) {
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let style = Catalog::style(theme, self.class);
let bounds = layout.bounds();
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let option_height =
f32::from(self.text_line_height.to_absolute(text_size))
+ self.padding.y();
let offset = viewport.y - bounds.y;
let start = (offset / option_height) as usize;
let end = ((offset + viewport.height) / option_height).ceil() as usize;
let visible_options = &self.options[start..end.min(self.options.len())];
for (i, option) in visible_options.iter().enumerate() {
let i = start + i;
let is_selected = *self.hovered_option == Some(i);
let bounds = Rectangle {
x: bounds.x,
y: bounds.y + (option_height * i as f32),
width: bounds.width,
height: option_height,
};
if is_selected {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + style.border.width,
width: bounds.width - style.border.width * 2.0,
..bounds
},
border: border::rounded(style.border.radius),
..renderer::Quad::default()
},
style.selected_background,
);
}
renderer.fill_text(
Text {
content: option.to_string(),
bounds: Size::new(f32::INFINITY, bounds.height),
size: text_size,
line_height: self.text_line_height,
font: self.font.unwrap_or_else(|| renderer.default_font()),
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
style.selected_text_color
} else {
style.text_color
},
*viewport,
);
}
}
}
impl<'a, 'b, T, Message, Theme, Renderer>
From<List<'a, 'b, T, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
T: ToString + Clone,
Message: 'a,
Theme: 'a + Catalog,
Renderer: 'a + text::Renderer,
'b: 'a,
{
fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self {
Element::new(list)
}
}
/// The appearance of a [`Menu`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the menu.
pub background: Background,
/// The [`Border`] of the menu.
pub border: Border,
/// The text [`Color`] of the menu.
pub text_color: Color,
/// The text [`Color`] of a selected option in the menu.
pub selected_text_color: Color,
/// The background [`Color`] of a selected option in the menu.
pub selected_background: Background,
/// The [`Shadow`] of the menu.
pub shadow: Shadow,
}
/// The theme catalog of a [`Menu`].
pub trait Catalog: scrollable::Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> <Self as Catalog>::Class<'a>;
/// The default class for the scrollable of the [`Menu`].
fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
<Self as scrollable::Catalog>::default()
}
/// The [`Style`] of a class with the given status.
fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
}
/// A styling function for a [`Menu`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> StyleFn<'a, Self> {
Box::new(default)
}
fn style(&self, class: &StyleFn<'_, Self>) -> Style {
class(self)
}
}
/// The default style of the list of a [`Menu`].
pub fn default(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
background: palette.background.weak.color.into(),
border: Border {
width: 1.0,
radius: 0.0.into(),
color: palette.background.strong.color,
},
text_color: palette.background.weak.text,
selected_text_color: palette.primary.strong.text,
selected_background: palette.primary.strong.color.into(),
shadow: Shadow::default(),
}
}
File diff suppressed because it is too large Load Diff
+259
View File
@@ -0,0 +1,259 @@
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,
min_size_a: f32,
min_size_b: f32,
) -> (Rectangle, Rectangle, f32) {
match self {
Axis::Horizontal => {
let height_top = (rectangle.height * ratio - spacing / 2.0)
.round()
.max(min_size_a)
.min(rectangle.height - min_size_b - spacing);
let height_bottom =
(rectangle.height - height_top - spacing).max(min_size_b);
let ratio = (height_top + spacing / 2.0) / rectangle.height;
(
Rectangle {
height: height_top,
..*rectangle
},
Rectangle {
y: rectangle.y + height_top + spacing,
height: height_bottom,
..*rectangle
},
ratio,
)
}
Axis::Vertical => {
let width_left = (rectangle.width * ratio - spacing / 2.0)
.round()
.max(min_size_a)
.min(rectangle.width - min_size_b - spacing);
let width_right =
(rectangle.width - width_left - spacing).max(min_size_b);
let ratio = (width_left + spacing / 2.0) / rectangle.width;
(
Rectangle {
width: width_left,
..*rectangle
},
Rectangle {
x: rectangle.x + width_left + spacing,
width: width_right,
..*rectangle
},
ratio,
)
}
}
}
/// 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, _ratio) =
a.split(&r, 0.5, spacing, 0.0, 0.0);
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, _ratio) =
a.split(&r, 0.5, spacing, 0.0, 0.0);
assert_eq!(
left,
Rectangle {
width: left_width,
..r
}
);
assert_eq!(
right,
Rectangle {
x: right_x,
width: right_width,
..r
}
);
}
}
}
}
}
@@ -0,0 +1,26 @@
use crate::pane_grid::Axis;
/// The arrangement of a [`PaneGrid`].
///
/// [`PaneGrid`]: super::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`]: super::Pane
Pane(T),
}
+439
View File
@@ -0,0 +1,439 @@
use crate::container;
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::{
self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size,
Vector,
};
use crate::pane_grid::{Draggable, TitleBar};
/// The content of a [`Pane`].
///
/// [`Pane`]: super::Pane
pub struct Content<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: container::Catalog,
Renderer: core::Renderer,
{
title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>,
body: Element<'a, Message, Theme, Renderer>,
class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
/// Creates a new [`Content`] with the provided body.
pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
Self {
title_bar: None,
body: body.into(),
class: Theme::default(),
}
}
/// Sets the [`TitleBar`] of the [`Content`].
pub fn title_bar(
mut self,
title_bar: TitleBar<'a, Message, Theme, Renderer>,
) -> Self {
self.title_bar = Some(title_bar);
self
}
/// Sets the style of the [`Content`].
#[must_use]
pub fn style(
mut self,
style: impl Fn(&Theme) -> container::Style + 'a,
) -> Self
where
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Content`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Content<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
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`]: core::Renderer
pub fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
{
let style = theme.style(&self.class);
container::draw_background(renderer, &style, bounds);
}
if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
let title_bar_layout = children.next().unwrap();
let body_layout = children.next().unwrap();
let show_controls = cursor.is_over(bounds);
self.body.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
body_layout,
cursor,
viewport,
);
title_bar.draw(
&tree.children[1],
renderer,
theme,
style,
title_bar_layout,
cursor,
viewport,
show_controls,
);
} else {
self.body.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
}
pub(crate) fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
if let Some(title_bar) = &mut self.title_bar {
let max_size = limits.max();
let title_bar_layout = title_bar.layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let title_bar_size = title_bar_layout.size();
let body_layout = self.body.as_widget_mut().layout(
&mut tree.children[0],
renderer,
&layout::Limits::new(
Size::ZERO,
Size::new(
max_size.width,
max_size.height - title_bar_size.height,
),
),
);
layout::Node::with_children(
max_size,
vec![
title_bar_layout,
body_layout.move_to(Point::new(0.0, title_bar_size.height)),
],
)
} else {
self.body.as_widget_mut().layout(
&mut tree.children[0],
renderer,
limits,
)
}
}
pub(crate) fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
let body_layout = if let Some(title_bar) = &mut 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_mut().operate(
&mut tree.children[0],
body_layout,
renderer,
operation,
);
}
pub(crate) fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
is_picked: bool,
) {
let body_layout = if let Some(title_bar) = &mut self.title_bar {
let mut children = layout.children();
title_bar.update(
&mut tree.children[1],
event,
children.next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
);
children.next().unwrap()
} else {
layout
};
if !is_picked {
self.body.as_widget_mut().update(
&mut tree.children[0],
event,
body_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
}
pub(crate) fn grid_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
drag_enabled: bool,
) -> Option<mouse::Interaction> {
let title_bar = self.title_bar.as_ref()?;
let mut children = layout.children();
let title_bar_layout = children.next().unwrap();
let is_over_pick_area = cursor
.position()
.map(|cursor_position| {
title_bar.is_over_pick_area(title_bar_layout, cursor_position)
})
.unwrap_or_default();
if is_over_pick_area && drag_enabled {
return Some(mouse::Interaction::Grab);
}
None
}
pub(crate) fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
drag_enabled: bool,
) -> mouse::Interaction {
let (body_layout, title_bar_interaction) = if let Some(title_bar) =
&self.title_bar
{
let mut children = layout.children();
let title_bar_layout = children.next().unwrap();
let is_over_pick_area = cursor
.position()
.map(|cursor_position| {
title_bar
.is_over_pick_area(title_bar_layout, cursor_position)
})
.unwrap_or_default();
if is_over_pick_area && drag_enabled {
return mouse::Interaction::Grab;
}
let mouse_interaction = title_bar.mouse_interaction(
&tree.children[1],
title_bar_layout,
cursor,
viewport,
renderer,
);
(children.next().unwrap(), mouse_interaction)
} else {
(layout, mouse::Interaction::default())
};
self.body
.as_widget()
.mouse_interaction(
&tree.children[0],
body_layout,
cursor,
viewport,
renderer,
)
.max(title_bar_interaction)
}
pub(crate) fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, 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,
viewport,
translation,
) {
Some(overlay) => Some(overlay),
None => self.body.as_widget_mut().overlay(
body_state,
children.next()?,
renderer,
viewport,
translation,
),
}
} else {
self.body.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
}
impl<Message, Theme, Renderer> Draggable
for &Content<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
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, Theme, Renderer> From<T>
for Content<'a, Message, Theme, Renderer>
where
T: Into<Element<'a, Message, Theme, Renderer>>,
Theme: container::Catalog + 'a,
Renderer: core::Renderer,
{
fn from(element: T) -> Self {
Self::new(element)
}
}
+58
View File
@@ -0,0 +1,58 @@
use crate::container;
use crate::core::{self, Element};
/// The controls of a [`Pane`].
///
/// [`Pane`]: super::Pane
pub struct Controls<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: container::Catalog,
Renderer: core::Renderer,
{
pub(super) full: Element<'a, Message, Theme, Renderer>,
pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>,
}
impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
/// Creates a new [`Controls`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
full: content.into(),
compact: None,
}
}
/// Creates a new [`Controls`] with a full and compact variant.
/// If there is not enough room to show the full variant without overlap,
/// then the compact variant will be shown instead.
pub fn dynamic(
full: impl Into<Element<'a, Message, Theme, Renderer>>,
compact: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
full: full.into(),
compact: Some(compact.into()),
}
}
}
impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>>
for Controls<'a, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
fn from(value: Element<'a, Message, Theme, Renderer>) -> Self {
Self::new(value)
}
}
+12
View File
@@ -0,0 +1,12 @@
/// A four cardinal direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// ↑
Up,
/// ↓
Down,
/// ←
Left,
/// →
Right,
}
+8
View File
@@ -0,0 +1,8 @@
use crate::core::{Layout, Point};
/// A pane that can be dragged.
pub trait Draggable {
/// Returns whether the [`Draggable`] with the given [`Layout`] can be picked
/// at the provided cursor position.
fn can_be_dragged_at(&self, layout: Layout<'_>, cursor: Point) -> bool;
}
+381
View File
@@ -0,0 +1,381 @@
use crate::core::{Rectangle, Size};
use crate::pane_grid::{Axis, Pane, Split};
use std::collections::BTreeMap;
/// A layout node of a [`PaneGrid`].
///
/// [`PaneGrid`]: super::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),
}
#[derive(Debug)]
enum Count {
Split {
horizontal: usize,
vertical: usize,
a: Box<Count>,
b: Box<Count>,
},
Pane,
}
impl Count {
fn horizontal(&self) -> usize {
match self {
Count::Split { horizontal, .. } => *horizontal,
Count::Pane => 0,
}
}
fn vertical(&self) -> usize {
match self {
Count::Split { vertical, .. } => *vertical,
Count::Pane => 0,
}
}
}
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
})
}
fn count(&self) -> Count {
match self {
Node::Split { a, b, axis, .. } => {
let a = a.count();
let b = b.count();
let (horizontal, vertical) = match axis {
Axis::Horizontal => (
1 + a.horizontal() + b.horizontal(),
a.vertical().max(b.vertical()),
),
Axis::Vertical => (
a.horizontal().max(b.horizontal()),
1 + a.vertical() + b.vertical(),
),
};
Count::Split {
horizontal,
vertical,
a: Box::new(a),
b: Box::new(b),
}
}
Node::Pane(_) => Count::Pane,
}
}
/// 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,
min_size: f32,
bounds: Size,
) -> BTreeMap<Pane, Rectangle> {
let mut regions = BTreeMap::new();
let count = self.count();
self.compute_regions(
spacing,
min_size,
&Rectangle {
x: 0.0,
y: 0.0,
width: bounds.width,
height: bounds.height,
},
&count,
&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,
min_size: f32,
bounds: Size,
) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
let mut splits = BTreeMap::new();
let count = self.count();
self.compute_splits(
spacing,
min_size,
&Rectangle {
x: 0.0,
y: 0.0,
width: bounds.width,
height: bounds.height,
},
&count,
&mut splits,
);
splits
}
pub(crate) fn find(&mut self, pane: Pane) -> Option<&mut Node> {
match self {
Node::Split { a, b, .. } => {
a.find(pane).or_else(move || b.find(pane))
}
Node::Pane(p) => {
if *p == pane {
Some(self)
} else {
None
}
}
}
}
pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
*self = Node::Split {
id,
axis,
ratio: 0.5,
a: Box::new(self.clone()),
b: Box::new(Node::Pane(new_pane)),
};
}
pub(crate) fn split_inverse(&mut self, id: Split, axis: Axis, pane: Pane) {
*self = Node::Split {
id,
axis,
ratio: 0.5,
a: Box::new(Node::Pane(pane)),
b: Box::new(self.clone()),
};
}
pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
if let Node::Split { a, b, .. } = self {
a.update(f);
b.update(f);
}
f(self);
}
pub(crate) fn resize(&mut self, split: Split, percentage: f32) -> bool {
match self {
Node::Split {
id, ratio, a, b, ..
} => {
if *id == split {
*ratio = percentage;
true
} else if a.resize(split, percentage) {
true
} else {
b.resize(split, percentage)
}
}
Node::Pane(_) => false,
}
}
pub(crate) fn remove(&mut self, pane: Pane) -> Option<Pane> {
match self {
Node::Split { a, b, .. } => {
if a.pane() == Some(pane) {
*self = *b.clone();
Some(self.first_pane())
} else if b.pane() == Some(pane) {
*self = *a.clone();
Some(self.first_pane())
} else {
a.remove(pane).or_else(|| b.remove(pane))
}
}
Node::Pane(_) => None,
}
}
fn pane(&self) -> Option<Pane> {
match self {
Node::Split { .. } => None,
Node::Pane(pane) => Some(*pane),
}
}
fn first_pane(&self) -> Pane {
match self {
Node::Split { a, .. } => a.first_pane(),
Node::Pane(pane) => *pane,
}
}
fn compute_regions(
&self,
spacing: f32,
min_size: f32,
current: &Rectangle,
count: &Count,
regions: &mut BTreeMap<Pane, Rectangle>,
) {
match (self, count) {
(
Node::Split {
axis, ratio, a, b, ..
},
Count::Split {
a: count_a,
b: count_b,
..
},
) => {
let (a_factor, b_factor) = match axis {
Axis::Horizontal => {
(count_a.horizontal(), count_b.horizontal())
}
Axis::Vertical => (count_a.vertical(), count_b.vertical()),
};
let (region_a, region_b, _ratio) = axis.split(
current,
*ratio,
spacing,
min_size * (a_factor + 1) as f32
+ spacing * a_factor as f32,
min_size * (b_factor + 1) as f32
+ spacing * b_factor as f32,
);
a.compute_regions(
spacing, min_size, &region_a, count_a, regions,
);
b.compute_regions(
spacing, min_size, &region_b, count_b, regions,
);
}
(Node::Pane(pane), Count::Pane) => {
let _ = regions.insert(*pane, *current);
}
_ => {
unreachable!("Node configuration and count do not match")
}
}
}
fn compute_splits(
&self,
spacing: f32,
min_size: f32,
current: &Rectangle,
count: &Count,
splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,
) {
match (self, count) {
(
Node::Split {
axis,
ratio,
a,
b,
id,
},
Count::Split {
a: count_a,
b: count_b,
..
},
) => {
let (a_factor, b_factor) = match axis {
Axis::Horizontal => {
(count_a.horizontal(), count_b.horizontal())
}
Axis::Vertical => (count_a.vertical(), count_b.vertical()),
};
let (region_a, region_b, ratio) = axis.split(
current,
*ratio,
spacing,
min_size * (a_factor + 1) as f32
+ spacing * a_factor as f32,
min_size * (b_factor + 1) as f32
+ spacing * b_factor as f32,
);
let _ = splits.insert(*id, (*axis, *current, ratio));
a.compute_splits(spacing, min_size, &region_a, count_a, splits);
b.compute_splits(spacing, min_size, &region_b, count_b, splits);
}
(Node::Pane(_), Count::Pane) => {}
_ => {
unreachable!("Node configuration and split count do not match")
}
}
}
}
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);
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
/// A rectangular region in a [`PaneGrid`] used to display widgets.
///
/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Pane(pub(super) usize);
+5
View File
@@ -0,0 +1,5 @@
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
///
/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Split(pub(super) usize);
+468
View File
@@ -0,0 +1,468 @@
//! The state of a [`PaneGrid`].
//!
//! [`PaneGrid`]: super::PaneGrid
use crate::core::{Point, Size};
use crate::pane_grid::{
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
};
use std::borrow::Cow;
use std::collections::BTreeMap;
/// 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`]: super::PaneGrid
/// [`PaneGrid::new`]: super::PaneGrid::new
#[derive(Debug, Clone)]
pub struct State<T> {
/// The panes of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub panes: BTreeMap<Pane, T>,
/// The internal state of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub internal: Internal,
}
impl<T> State<T> {
/// Creates a new [`State`], initializing the first pane with the provided
/// state.
///
/// Alongside the [`State`], it returns the first [`Pane`] identifier.
pub fn new(first_pane_state: T) -> (Self, Pane) {
(
Self::with_configuration(Configuration::Pane(first_pane_state)),
Pane(0),
)
}
/// Creates a new [`State`] with the given [`Configuration`].
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
let mut panes = BTreeMap::default();
let internal =
Internal::from_configuration(&mut panes, config.into(), 0);
State { panes, internal }
}
/// 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,
0.0,
Size::new(4096.0, 4096.0),
);
let current_region = regions.get(&pane)?;
let target = match direction {
Direction::Left => {
Point::new(current_region.x - 1.0, current_region.y + 1.0)
}
Direction::Right => Point::new(
current_region.x + current_region.width + 1.0,
current_region.y + 1.0,
),
Direction::Up => {
Point::new(current_region.x + 1.0, current_region.y - 1.0)
}
Direction::Down => Point::new(
current_region.x + 1.0,
current_region.y + current_region.height + 1.0,
),
};
let mut colliding_regions =
regions.iter().filter(|(_, region)| region.contains(target));
let (pane, _) = colliding_regions.next()?;
Some(*pane)
}
/// Splits the given [`Pane`] into two in the given [`Axis`] and
/// initializing the new [`Pane`] with the provided internal state.
pub fn split(
&mut self,
axis: Axis,
pane: Pane,
state: T,
) -> Option<(Pane, Split)> {
self.split_node(axis, Some(pane), state, false)
}
/// Split a target [`Pane`] with a given [`Pane`] on a given [`Region`].
///
/// Panes will be swapped by default for [`Region::Center`].
pub fn split_with(&mut self, target: Pane, pane: Pane, region: Region) {
match region {
Region::Center => self.swap(pane, target),
Region::Edge(edge) => match edge {
Edge::Top => {
self.split_and_swap(Axis::Horizontal, target, pane, true);
}
Edge::Bottom => {
self.split_and_swap(Axis::Horizontal, target, pane, false);
}
Edge::Left => {
self.split_and_swap(Axis::Vertical, target, pane, true);
}
Edge::Right => {
self.split_and_swap(Axis::Vertical, target, pane, false);
}
},
}
}
/// Drops the given [`Pane`] into the provided [`Target`].
pub fn drop(&mut self, pane: Pane, target: Target) {
match target {
Target::Edge(edge) => self.move_to_edge(pane, edge),
Target::Pane(target, region) => {
self.split_with(target, pane, region);
}
}
}
fn split_node(
&mut self,
axis: Axis,
pane: Option<Pane>,
state: T,
inverse: bool,
) -> Option<(Pane, Split)> {
let node = if let Some(pane) = pane {
self.internal.layout.find(pane)?
} else {
// Major node
&mut self.internal.layout
};
let new_pane = {
self.internal.last_id = self.internal.last_id.checked_add(1)?;
Pane(self.internal.last_id)
};
let new_split = {
self.internal.last_id = self.internal.last_id.checked_add(1)?;
Split(self.internal.last_id)
};
if inverse {
node.split_inverse(new_split, axis, new_pane);
} else {
node.split(new_split, axis, new_pane);
}
let _ = self.panes.insert(new_pane, state);
let _ = self.internal.maximized.take();
Some((new_pane, new_split))
}
fn split_and_swap(
&mut self,
axis: Axis,
target: Pane,
pane: Pane,
swap: bool,
) {
if let Some((state, _)) = self.close(pane)
&& let Some((new_pane, _)) = self.split(axis, target, state)
{
// Ensure new node corresponds to original closed `Pane` for state continuity
self.relabel(new_pane, pane);
if swap {
self.swap(target, pane);
}
}
}
/// Move [`Pane`] to an [`Edge`] of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub fn move_to_edge(&mut self, pane: Pane, edge: Edge) {
match edge {
Edge::Top => {
self.split_major_node_and_swap(Axis::Horizontal, pane, true);
}
Edge::Bottom => {
self.split_major_node_and_swap(Axis::Horizontal, pane, false);
}
Edge::Left => {
self.split_major_node_and_swap(Axis::Vertical, pane, true);
}
Edge::Right => {
self.split_major_node_and_swap(Axis::Vertical, pane, false);
}
}
}
fn split_major_node_and_swap(
&mut self,
axis: Axis,
pane: Pane,
inverse: bool,
) {
if let Some((state, _)) = self.close(pane)
&& let Some((new_pane, _)) =
self.split_node(axis, None, state, inverse)
{
// Ensure new node corresponds to original closed `Pane` for state continuity
self.relabel(new_pane, pane);
}
}
fn relabel(&mut self, target: Pane, label: Pane) {
self.swap(target, label);
let _ = self
.panes
.remove(&target)
.and_then(|state| self.panes.insert(label, state));
}
/// 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`]: super::PaneGrid
/// [`DragEvent`]: super::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`]: super::PaneGrid
/// [`ResizeEvent`]: super::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.internal.maximized == Some(pane) {
let _ = self.internal.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`]: super::PaneGrid
pub fn maximize(&mut self, pane: Pane) {
self.internal.maximized = Some(pane);
}
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
/// will be rendered by the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub fn restore(&mut self) {
let _ = self.internal.maximized.take();
}
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub fn maximized(&self) -> Option<Pane> {
self.internal.maximized
}
}
/// The internal state of a [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone)]
pub struct Internal {
layout: Node,
last_id: usize,
maximized: Option<Pane>,
}
impl Internal {
/// Initializes the [`Internal`] state of a [`PaneGrid`] from a
/// [`Configuration`].
///
/// [`PaneGrid`]: super::PaneGrid
pub fn from_configuration<T>(
panes: &mut BTreeMap<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,
maximized: None,
}
}
pub(super) fn layout(&self) -> Cow<'_, Node> {
match self.maximized {
Some(pane) => Cow::Owned(Node::Pane(pane)),
None => Cow::Borrowed(&self.layout),
}
}
pub(super) fn maximized(&self) -> Option<Pane> {
self.maximized
}
}
/// The current action of a [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Action {
/// The [`PaneGrid`] is idle.
///
/// [`PaneGrid`]: super::PaneGrid
#[default]
Idle,
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
///
/// [`PaneGrid`]: super::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`]: super::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,
}
}
}
+632
View File
@@ -0,0 +1,632 @@
use crate::container;
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::{
self, Clipboard, Element, Event, Layout, Padding, Point, Rectangle, Shell,
Size, Vector,
};
use crate::pane_grid::controls::Controls;
/// The title bar of a [`Pane`].
///
/// [`Pane`]: super::Pane
pub struct TitleBar<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: container::Catalog,
Renderer: core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
controls: Option<Controls<'a, Message, Theme, Renderer>>,
padding: Padding,
always_show_controls: bool,
class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
/// Creates a new [`TitleBar`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
controls: None,
padding: Padding::ZERO,
always_show_controls: false,
class: Theme::default(),
}
}
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
controls: impl Into<Controls<'a, Message, Theme, 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 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`]: super::Pane
pub fn always_show_controls(mut self) -> Self {
self.always_show_controls = true;
self
}
/// Sets the style of the [`TitleBar`].
#[must_use]
pub fn style(
mut self,
style: impl Fn(&Theme) -> container::Style + 'a,
) -> Self
where
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`TitleBar`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> TitleBar<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
pub(super) fn state(&self) -> Tree {
let children = match self.controls.as_ref() {
Some(controls) => match controls.compact.as_ref() {
Some(compact) => vec![
Tree::new(&self.content),
Tree::new(&controls.full),
Tree::new(compact),
],
None => vec![
Tree::new(&self.content),
Tree::new(&controls.full),
Tree::empty(),
],
},
None => {
vec![Tree::new(&self.content), Tree::empty(), Tree::empty()]
}
};
Tree {
children,
..Tree::empty()
}
}
pub(super) fn diff(&self, tree: &mut Tree) {
if tree.children.len() == 3 {
if let Some(controls) = self.controls.as_ref() {
if let Some(compact) = controls.compact.as_ref() {
tree.children[2].diff(compact);
}
tree.children[1].diff(&controls.full);
}
tree.children[0].diff(&self.content);
} else {
*tree = self.state();
}
}
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
///
/// [`Renderer`]: core::Renderer
pub fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
show_controls: bool,
) {
let bounds = layout.bounds();
let style = theme.style(&self.class);
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
&& (show_controls || self.always_show_controls)
{
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
if let Some(compact) = controls.compact.as_ref() {
let compact_layout = children.next().unwrap();
compact.as_widget().draw(
&tree.children[2],
renderer,
theme,
&inherited_style,
compact_layout,
cursor,
viewport,
);
} else {
show_title = false;
controls.full.as_widget().draw(
&tree.children[1],
renderer,
theme,
&inherited_style,
controls_layout,
cursor,
viewport,
);
}
} else {
controls.full.as_widget().draw(
&tree.children[1],
renderer,
theme,
&inherited_style,
controls_layout,
cursor,
viewport,
);
}
}
if show_title {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
&inherited_style,
title_layout,
cursor,
viewport,
);
}
}
/// Returns whether the mouse cursor is over the pick area of the
/// [`TitleBar`] or not.
///
/// The whole [`TitleBar`] is a pick area, except its controls.
pub fn is_over_pick_area(
&self,
layout: Layout<'_>,
cursor_position: Point,
) -> bool {
if layout.bounds().contains(cursor_position) {
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
if let Some(controls) = self.controls.as_ref() {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
if controls.compact.is_some() {
let compact_layout = children.next().unwrap();
!compact_layout.bounds().contains(cursor_position)
&& !title_layout.bounds().contains(cursor_position)
} else {
!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(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.shrink(self.padding);
let max_size = limits.max();
let title_layout = self.content.as_widget_mut().layout(
&mut tree.children[0],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let title_size = title_layout.size();
let node = if let Some(controls) = &mut self.controls {
let controls_layout = controls.full.as_widget_mut().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
if title_layout.bounds().width + controls_layout.bounds().width
> max_size.width
{
if let Some(compact) = controls.compact.as_mut() {
let compact_layout = compact.as_widget_mut().layout(
&mut tree.children[2],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let compact_size = compact_layout.size();
let space_before_controls =
max_size.width - compact_size.width;
let height = title_size.height.max(compact_size.height);
layout::Node::with_children(
Size::new(max_size.width, height),
vec![
title_layout,
controls_layout,
compact_layout.move_to(Point::new(
space_before_controls,
0.0,
)),
],
)
} else {
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);
layout::Node::with_children(
Size::new(max_size.width, height),
vec![
title_layout,
controls_layout.move_to(Point::new(
space_before_controls,
0.0,
)),
],
)
}
} else {
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);
layout::Node::with_children(
Size::new(max_size.width, height),
vec![
title_layout,
controls_layout
.move_to(Point::new(space_before_controls, 0.0)),
],
)
}
} else {
layout::Node::with_children(
Size::new(max_size.width, title_size.height),
vec![title_layout],
)
};
layout::Node::container(node, self.padding)
}
pub(crate) fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
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) = &mut self.controls {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
if let Some(compact) = controls.compact.as_mut() {
let compact_layout = children.next().unwrap();
compact.as_widget_mut().operate(
&mut tree.children[2],
compact_layout,
renderer,
operation,
);
} else {
show_title = false;
controls.full.as_widget_mut().operate(
&mut tree.children[1],
controls_layout,
renderer,
operation,
);
}
} else {
controls.full.as_widget_mut().operate(
&mut tree.children[1],
controls_layout,
renderer,
operation,
);
}
};
if show_title {
self.content.as_widget_mut().operate(
&mut tree.children[0],
title_layout,
renderer,
operation,
);
}
}
pub(crate) fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
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) = &mut self.controls {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
if let Some(compact) = controls.compact.as_mut() {
let compact_layout = children.next().unwrap();
compact.as_widget_mut().update(
&mut tree.children[2],
event,
compact_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
} else {
show_title = false;
controls.full.as_widget_mut().update(
&mut tree.children[1],
event,
controls_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
} else {
controls.full.as_widget_mut().update(
&mut tree.children[1],
event,
controls_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
}
if show_title {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
title_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
}
pub(crate) fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let mut children = layout.children();
let padded = children.next().unwrap();
let mut children = padded.children();
let title_layout = children.next().unwrap();
let title_interaction = self.content.as_widget().mouse_interaction(
&tree.children[0],
title_layout,
cursor,
viewport,
renderer,
);
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
let controls_interaction =
controls.full.as_widget().mouse_interaction(
&tree.children[1],
controls_layout,
cursor,
viewport,
renderer,
);
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
if let Some(compact) = controls.compact.as_ref() {
let compact_layout = children.next().unwrap();
let compact_interaction =
compact.as_widget().mouse_interaction(
&tree.children[2],
compact_layout,
cursor,
viewport,
renderer,
);
compact_interaction.max(title_interaction)
} else {
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<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, 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, viewport, translation)
.or_else(move || {
controls.as_mut().and_then(|controls| {
let controls_layout = children.next()?;
if title_layout.bounds().width
+ controls_layout.bounds().width
> padded.bounds().width
{
if let Some(compact) = controls.compact.as_mut() {
let compact_state = states.next().unwrap();
let compact_layout = children.next()?;
compact.as_widget_mut().overlay(
compact_state,
compact_layout,
renderer,
viewport,
translation,
)
} else {
controls.full.as_widget_mut().overlay(
controls_state,
controls_layout,
renderer,
viewport,
translation,
)
}
} else {
controls.full.as_widget_mut().overlay(
controls_state,
controls_layout,
renderer,
viewport,
translation,
)
}
})
})
}
}
+929
View File
@@ -0,0 +1,929 @@
//! Pick lists display a dropdown list of selectable options.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::pick_list;
//!
//! struct State {
//! favorite: Option<Fruit>,
//! }
//!
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
//! enum Fruit {
//! Apple,
//! Orange,
//! Strawberry,
//! Tomato,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Message {
//! FruitSelected(Fruit),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! let fruits = [
//! Fruit::Apple,
//! Fruit::Orange,
//! Fruit::Strawberry,
//! Fruit::Tomato,
//! ];
//!
//! pick_list(
//! fruits,
//! state.favorite,
//! Message::FruitSelected,
//! )
//! .placeholder("Select your favorite fruit...")
//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::FruitSelected(fruit) => {
//! state.favorite = Some(fruit);
//! }
//! }
//! }
//!
//! impl std::fmt::Display for Fruit {
//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
//! f.write_str(match self {
//! Self::Apple => "Apple",
//! Self::Orange => "Orange",
//! Self::Strawberry => "Strawberry",
//! Self::Tomato => "Tomato",
//! })
//! }
//! }
//! ```
use crate::core::alignment;
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::text::paragraph;
use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
use crate::overlay::menu::{self, Menu};
use std::borrow::Borrow;
use std::f32;
/// A widget for selecting a single value from a list of options.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::pick_list;
///
/// struct State {
/// favorite: Option<Fruit>,
/// }
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// enum Fruit {
/// Apple,
/// Orange,
/// Strawberry,
/// Tomato,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// FruitSelected(Fruit),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// let fruits = [
/// Fruit::Apple,
/// Fruit::Orange,
/// Fruit::Strawberry,
/// Fruit::Tomato,
/// ];
///
/// pick_list(
/// fruits,
/// state.favorite,
/// Message::FruitSelected,
/// )
/// .placeholder("Select your favorite fruit...")
/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::FruitSelected(fruit) => {
/// state.favorite = Some(fruit);
/// }
/// }
/// }
///
/// impl std::fmt::Display for Fruit {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// f.write_str(match self {
/// Self::Apple => "Apple",
/// Self::Orange => "Orange",
/// Self::Strawberry => "Strawberry",
/// Self::Tomato => "Tomato",
/// })
/// }
/// }
/// ```
pub struct PickList<
'a,
T,
L,
V,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
T: ToString + PartialEq + Clone,
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
Theme: Catalog,
Renderer: text::Renderer,
{
on_select: Box<dyn Fn(T) -> Message + 'a>,
on_open: Option<Message>,
on_close: Option<Message>,
options: L,
placeholder: Option<String>,
selected: Option<V>,
width: Length,
padding: Padding,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
handle: Handle<Renderer::Font>,
class: <Theme as Catalog>::Class<'a>,
menu_class: <Theme as menu::Catalog>::Class<'a>,
last_status: Option<Status>,
menu_height: Length,
}
impl<'a, T, L, V, Message, Theme, Renderer>
PickList<'a, T, L, V, Message, Theme, Renderer>
where
T: ToString + PartialEq + Clone,
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
/// 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: L,
selected: Option<V>,
on_select: impl Fn(T) -> Message + 'a,
) -> Self {
Self {
on_select: Box::new(on_select),
on_open: None,
on_close: None,
options,
placeholder: None,
selected,
width: Length::Shrink,
padding: crate::button::DEFAULT_PADDING,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::default(),
font: None,
handle: Handle::default(),
class: <Theme as Catalog>::default(),
menu_class: <Theme as Catalog>::default_menu(),
last_status: None,
menu_height: Length::Shrink,
}
}
/// 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 height of the [`Menu`].
pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
self.menu_height = menu_height.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());
self
}
/// Sets the text [`text::LineHeight`] of the [`PickList`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
) -> Self {
self.text_line_height = line_height.into();
self
}
/// Sets the [`text::Shaping`] strategy of the [`PickList`].
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
self.text_shaping = shaping;
self
}
/// Sets the font of the [`PickList`].
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the [`Handle`] of the [`PickList`].
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
self.handle = handle;
self
}
/// Sets the message that will be produced when the [`PickList`] is opened.
pub fn on_open(mut self, on_open: Message) -> Self {
self.on_open = Some(on_open);
self
}
/// Sets the message that will be produced when the [`PickList`] is closed.
pub fn on_close(mut self, on_close: Message) -> Self {
self.on_close = Some(on_close);
self
}
/// Sets the style of the [`PickList`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style of the [`Menu`].
#[must_use]
pub fn menu_style(
mut self,
style: impl Fn(&Theme) -> menu::Style + 'a,
) -> Self
where
<Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
{
self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`PickList`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(
mut self,
class: impl Into<<Theme as Catalog>::Class<'a>>,
) -> Self {
self.class = class.into();
self
}
/// Sets the style class of the [`Menu`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn menu_class(
mut self,
class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
) -> Self {
self.menu_class = class.into();
self
}
}
impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for PickList<'a, T, L, V, Message, Theme, Renderer>
where
T: Clone + ToString + PartialEq + 'a,
L: Borrow<[T]>,
V: Borrow<T>,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<Renderer::Paragraph>::new())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let options = self.options.borrow();
state.options.resize_with(options.len(), Default::default);
let option_text = Text {
content: "",
bounds: Size::new(
f32::INFINITY,
self.text_line_height.to_absolute(text_size).into(),
),
size: text_size,
line_height: self.text_line_height,
font,
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
{
let label = option.to_string();
let _ = paragraph.update(Text {
content: &label,
..option_text
});
}
if let Some(placeholder) = &self.placeholder {
let _ = state.placeholder.update(Text {
content: placeholder,
..option_text
});
}
let max_width = match self.width {
Length::Shrink => {
let labels_width =
state.options.iter().fold(0.0, |width, paragraph| {
f32::max(width, paragraph.min_width())
});
labels_width.max(
self.placeholder
.as_ref()
.map(|_| state.placeholder.min_width())
.unwrap_or(0.0),
)
}
_ => 0.0,
};
let size = {
let intrinsic = Size::new(
max_width + text_size.0 + self.padding.left,
f32::from(self.text_line_height.to_absolute(text_size)),
);
limits
.width(self.width)
.shrink(self.padding)
.resolve(self.width, Length::Shrink, intrinsic)
.expand(self.padding)
};
layout::Node::new(size)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if state.is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside its
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
if let Some(on_close) = &self.on_close {
shell.publish(on_close.clone());
}
shell.capture_event();
} else if cursor.is_over(layout.bounds()) {
let selected = self.selected.as_ref().map(Borrow::borrow);
state.is_open = true;
state.hovered_option = self
.options
.borrow()
.iter()
.position(|option| Some(option) == selected);
if let Some(on_open) = &self.on_open {
shell.publish(on_open.clone());
}
shell.capture_event();
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
if state.keyboard_modifiers.command()
&& cursor.is_over(layout.bounds())
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let options = self.options.borrow();
let selected = self.selected.as_ref().map(Borrow::borrow);
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((self.on_select)(next_option.clone()));
}
shell.capture_event();
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = *modifiers;
}
_ => {}
};
let status = {
let is_hovered = cursor.is_over(layout.bounds());
if state.is_open {
Status::Opened { is_hovered }
} else if is_hovered {
Status::Hovered
} else {
Status::Active
}
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_status = Some(status);
} else if self
.last_status
.is_some_and(|last_status| last_status != status)
{
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let font = self.font.unwrap_or_else(|| renderer.default_font());
let selected = self.selected.as_ref().map(Borrow::borrow);
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let bounds = layout.bounds();
let style = Catalog::style(
theme,
&self.class,
self.last_status.unwrap_or(Status::Active),
);
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
..renderer::Quad::default()
},
style.background,
);
let handle = match &self.handle {
Handle::Arrow { size } => Some((
Renderer::ICON_FONT,
Renderer::ARROW_DOWN_ICON,
*size,
text::LineHeight::default(),
text::Shaping::Basic,
)),
Handle::Static(Icon {
font,
code_point,
size,
line_height,
shaping,
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
Handle::Dynamic { open, closed } => {
if state.is_open {
Some((
open.font,
open.code_point,
open.size,
open.line_height,
open.shaping,
))
} else {
Some((
closed.font,
closed.code_point,
closed.size,
closed.line_height,
closed.shaping,
))
}
}
Handle::None => None,
};
if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: code_point.to_string(),
size,
line_height,
font,
bounds: Size::new(
bounds.width,
f32::from(line_height.to_absolute(size)),
),
align_x: text::Alignment::Right,
align_y: alignment::Vertical::Center,
shaping,
wrapping: text::Wrapping::default(),
},
Point::new(
bounds.x + bounds.width - self.padding.right,
bounds.center_y(),
),
style.handle_color,
*viewport,
);
}
let label = selected.map(ToString::to_string);
if let Some(label) = label.or_else(|| self.placeholder.clone()) {
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: label,
size: text_size,
line_height: self.text_line_height,
font,
bounds: Size::new(
bounds.width - self.padding.x(),
f32::from(self.text_line_height.to_absolute(text_size)),
),
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if selected.is_some() {
style.text_color
} else {
style.placeholder_color
},
*viewport,
);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
if state.is_open {
let bounds = layout.bounds();
let on_select = &self.on_select;
let mut menu = Menu::new(
&mut state.menu,
self.options.borrow(),
&mut state.hovered_option,
|option| {
state.is_open = false;
(on_select)(option)
},
None,
&self.menu_class,
)
.width(bounds.width)
.padding(self.padding)
.font(font)
.text_shaping(self.text_shaping);
if let Some(text_size) = self.text_size {
menu = menu.text_size(text_size);
}
Some(menu.overlay(
layout.position() + translation,
*viewport,
bounds.height,
self.menu_height,
))
} else {
None
}
}
}
impl<'a, T, L, V, Message, Theme, Renderer>
From<PickList<'a, T, L, V, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
T: Clone + ToString + PartialEq + 'a,
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>,
) -> Self {
Self::new(pick_list)
}
}
#[derive(Debug)]
struct State<P: text::Paragraph> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
options: Vec<paragraph::Plain<P>>,
placeholder: paragraph::Plain<P>,
}
impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`] for a [`PickList`].
fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
options: Vec::new(),
placeholder: paragraph::Plain::default(),
}
}
}
impl<P: text::Paragraph> Default for State<P> {
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<Pixels>,
},
/// 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<Pixels>,
/// Line height of the content.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
pub shaping: text::Shaping,
}
/// The possible status of a [`PickList`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`PickList`] can be interacted with.
Active,
/// The [`PickList`] is being hovered.
Hovered,
/// The [`PickList`] is open.
Opened {
/// Whether the [`PickList`] is hovered, while open.
is_hovered: bool,
},
}
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The text [`Color`] of the pick list.
pub text_color: Color,
/// The placeholder [`Color`] of the pick list.
pub placeholder_color: Color,
/// The handle [`Color`] of the pick list.
pub handle_color: Color,
/// The [`Background`] of the pick list.
pub background: Background,
/// The [`Border`] of the pick list.
pub border: Border,
}
/// The theme catalog of a [`PickList`].
pub trait Catalog: menu::Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> <Self as Catalog>::Class<'a>;
/// The default class for the menu of the [`PickList`].
fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
<Self as menu::Catalog>::default()
}
/// The [`Style`] of a class with the given status.
fn style(
&self,
class: &<Self as Catalog>::Class<'_>,
status: Status,
) -> Style;
}
/// A styling function for a [`PickList`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> StyleFn<'a, Self> {
Box::new(default)
}
fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
class(self, status)
}
}
/// The default style of the field of a [`PickList`].
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let active = Style {
text_color: palette.background.weak.text,
background: palette.background.weak.color.into(),
placeholder_color: palette.secondary.base.color,
handle_color: palette.background.weak.text,
border: Border {
radius: 2.0.into(),
width: 1.0,
color: palette.background.strong.color,
},
};
match status {
Status::Active => active,
Status::Hovered | Status::Opened { .. } => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
},
..active
},
}
}
+271
View File
@@ -0,0 +1,271 @@
//! A pin widget positions a widget at some fixed coordinates inside its boundaries.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::pin;
//! use iced::Fill;
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! pin("This text is displayed at coordinates (50, 50)!")
//! .x(50)
//! .y(50)
//! .into()
//! }
//! ```
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::{
self, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle,
Shell, Size, Vector, Widget,
};
/// A widget that positions its contents at some fixed coordinates inside of its boundaries.
///
/// By default, a [`Pin`] widget will try to fill its parent.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::pin;
/// use iced::Fill;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// pin("This text is displayed at coordinates (50, 50)!")
/// .x(50)
/// .y(50)
/// .into()
/// }
/// ```
pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Renderer: core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
width: Length,
height: Length,
position: Point,
}
impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
/// Creates a [`Pin`] widget with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
width: Length::Fill,
height: Length::Fill,
position: Point::ORIGIN,
}
}
/// Sets the width of the [`Pin`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Pin`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets the position of the [`Pin`]; where the pinned widget will be displayed.
pub fn position(mut self, position: impl Into<Point>) -> Self {
self.position = position.into();
self
}
/// Sets the X coordinate of the [`Pin`].
pub fn x(mut self, x: impl Into<Pixels>) -> Self {
self.position.x = x.into().0;
self
}
/// Sets the Y coordinate of the [`Pin`].
pub fn y(mut self, y: impl Into<Pixels>) -> Self {
self.position.y = y.into().0;
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Pin<'_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
fn tag(&self) -> widget::tree::Tag {
self.content.as_widget().tag()
}
fn state(&self) -> widget::tree::State {
self.content.as_widget().state()
}
fn children(&self) -> Vec<widget::Tree> {
self.content.as_widget().children()
}
fn diff(&self, tree: &mut widget::Tree) {
self.content.as_widget().diff(tree);
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let available =
limits.max() - Size::new(self.position.x, self.position.y);
let node = self
.content
.as_widget_mut()
.layout(tree, renderer, &layout::Limits::new(Size::ZERO, available))
.move_to(self.position);
let size = limits.resolve(self.width, self.height, node.size());
layout::Node::with_children(size, vec![node])
}
fn operate(
&mut self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content.as_widget_mut().operate(
tree,
layout.children().next().unwrap(),
renderer,
operation,
);
}
fn update(
&mut self,
tree: &mut widget::Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
tree,
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
tree,
layout.children().next().unwrap(),
cursor,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
if let Some(clipped_viewport) = bounds.intersection(viewport) {
self.content.as_widget().draw(
tree,
renderer,
theme,
style,
layout.children().next().unwrap(),
cursor,
&clipped_viewport,
);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut widget::Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
tree,
layout.children().next().unwrap(),
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Pin<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: core::Renderer + 'a,
{
fn from(
pin: Pin<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(pin)
}
}
+333
View File
@@ -0,0 +1,333 @@
//! Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::progress_bar;
//!
//! struct State {
//! progress: f32,
//! }
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! progress_bar(0.0..=100.0, state.progress).into()
//! }
//! ```
use crate::core::border::{self, Border};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
Widget,
};
use std::ops::RangeInclusive;
/// A bar that displays progress.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::progress_bar;
///
/// struct State {
/// progress: f32,
/// }
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// progress_bar(0.0..=100.0, state.progress).into()
/// }
/// ```
pub struct ProgressBar<'a, Theme = crate::Theme>
where
Theme: Catalog,
{
range: RangeInclusive<f32>,
value: f32,
length: Length,
girth: Length,
is_vertical: bool,
class: Theme::Class<'a>,
}
impl<'a, Theme> ProgressBar<'a, Theme>
where
Theme: Catalog,
{
/// The default girth of a [`ProgressBar`].
pub const DEFAULT_GIRTH: 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,
length: Length::Fill,
girth: Length::from(Self::DEFAULT_GIRTH),
is_vertical: false,
class: Theme::default(),
}
}
/// Sets the width of the [`ProgressBar`].
pub fn length(mut self, length: impl Into<Length>) -> Self {
self.length = length.into();
self
}
/// Sets the height of the [`ProgressBar`].
pub fn girth(mut self, girth: impl Into<Length>) -> Self {
self.girth = girth.into();
self
}
/// Turns the [`ProgressBar`] into a vertical [`ProgressBar`].
///
/// By default, a [`ProgressBar`] is horizontal.
pub fn vertical(mut self) -> Self {
self.is_vertical = true;
self
}
/// Sets the style of the [`ProgressBar`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`ProgressBar`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
fn width(&self) -> Length {
if self.is_vertical {
self.girth
} else {
self.length
}
}
fn height(&self) -> Length {
if self.is_vertical {
self.length
} else {
self.girth
}
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ProgressBar<'_, Theme>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width(),
height: self.height(),
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width(), self.height())
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let (range_start, range_end) = self.range.clone().into_inner();
let length = if self.is_vertical {
bounds.height
} else {
bounds.width
};
let active_progress_length = if range_start >= range_end {
0.0
} else {
length * (self.value - range_start) / (range_end - range_start)
};
let style = theme.style(&self.class);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle { ..bounds },
border: style.border,
..renderer::Quad::default()
},
style.background,
);
if active_progress_length > 0.0 {
let bounds = if self.is_vertical {
Rectangle {
y: bounds.y + bounds.height - active_progress_length,
height: active_progress_length,
..bounds
}
} else {
Rectangle {
width: active_progress_length,
..bounds
}
};
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
color: Color::TRANSPARENT,
..style.border
},
..renderer::Quad::default()
},
style.bar,
);
}
}
}
impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a + Catalog,
Renderer: 'a + core::Renderer,
{
fn from(
progress_bar: ProgressBar<'a, Theme>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(progress_bar)
}
}
/// The appearance of a progress bar.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the progress bar.
pub background: Background,
/// The [`Background`] of the bar of the progress bar.
pub bar: Background,
/// The [`Border`] of the progress bar.
pub border: Border,
}
/// The theme catalog of a [`ProgressBar`].
pub trait Catalog: Sized {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`ProgressBar`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(primary)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// The primary style of a [`ProgressBar`].
pub fn primary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.primary.base.color)
}
/// The secondary style of a [`ProgressBar`].
pub fn secondary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(
palette.background.strong.color,
palette.secondary.base.color,
)
}
/// The success style of a [`ProgressBar`].
pub fn success(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.success.base.color)
}
/// The warning style of a [`ProgressBar`].
pub fn warning(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.warning.base.color)
}
/// The danger style of a [`ProgressBar`].
pub fn danger(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.danger.base.color)
}
fn styled(
background: impl Into<Background>,
bar: impl Into<Background>,
) -> Style {
Style {
background: background.into(),
bar: bar.into(),
border: border::rounded(2),
}
}
+439
View File
@@ -0,0 +1,439 @@
//! QR codes display information in a type of two-dimensional matrix barcode.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::qr_code;
//!
//! struct State {
//! data: qr_code::Data,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! qr_code(&state.data).into()
//! }
//! ```
use crate::Renderer;
use crate::canvas;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer::{self, Renderer as _};
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
Vector, Widget,
};
use std::cell::RefCell;
use thiserror::Error;
const DEFAULT_CELL_SIZE: f32 = 4.0;
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.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::qr_code;
///
/// struct State {
/// data: qr_code::Data,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// qr_code(&state.data).into()
/// }
/// ```
pub struct QRCode<'a, Theme = crate::Theme>
where
Theme: Catalog,
{
data: &'a Data,
cell_size: f32,
class: Theme::Class<'a>,
}
impl<'a, Theme> QRCode<'a, Theme>
where
Theme: Catalog,
{
/// Creates a new [`QRCode`] with the provided [`Data`].
pub fn new(data: &'a Data) -> Self {
Self {
data,
cell_size: DEFAULT_CELL_SIZE,
class: Theme::default(),
}
}
/// Sets the size of the squares of the grid cell of the [`QRCode`].
pub fn cell_size(mut self, cell_size: impl Into<Pixels>) -> Self {
self.cell_size = cell_size.into().0;
self
}
/// Sets the size of the entire [`QRCode`].
pub fn total_size(mut self, total_size: impl Into<Pixels>) -> Self {
self.cell_size =
total_size.into().0 / (self.data.width + 2 * QUIET_ZONE) as f32;
self
}
/// Sets the style of the [`QRCode`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`QRCode`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'_, Theme>
where
Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
Size {
width: Length::Shrink,
height: Length::Shrink,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
_limits: &layout::Limits,
) -> layout::Node {
let side_length =
(self.data.width + 2 * QUIET_ZONE) as f32 * self.cell_size;
layout::Node::new(Size::new(side_length, side_length))
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let side_length = self.data.width + 2 * QUIET_ZONE;
let style = theme.style(&self.class);
let mut last_style = state.last_style.borrow_mut();
if Some(style) != *last_style {
self.data.cache.clear();
*last_style = Some(style);
}
// Reuse cache if possible
let geometry = self.data.cache.draw(renderer, bounds.size(), |frame| {
// Scale units to cell size
frame.scale(self.cell_size);
// Draw background
frame.fill_rectangle(
Point::ORIGIN,
Size::new(side_length as f32, side_length as f32),
style.background,
);
// Avoid drawing on the quiet zone
frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32));
// Draw contents
self.data
.contents
.iter()
.enumerate()
.filter(|(_, value)| **value == qrcode::Color::Dark)
.for_each(|(index, _)| {
let row = index / self.data.width;
let column = index % self.data.width;
frame.fill_rectangle(
Point::new(column as f32, row as f32),
Size::UNIT,
style.cell,
);
});
});
renderer.with_translation(
bounds.position() - Point::ORIGIN,
|renderer| {
use crate::graphics::geometry::Renderer as _;
renderer.draw_geometry(geometry);
},
);
}
}
impl<'a, Message, Theme> From<QRCode<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
Theme: Catalog + 'a,
{
fn from(qr_code: QRCode<'a, Theme>) -> Self {
Self::new(qr_code)
}
}
/// The data of a [`QRCode`].
///
/// It stores the contents that will be displayed.
#[derive(Debug)]
pub struct Data {
contents: Vec<qrcode::Color>,
width: usize,
cache: canvas::Cache<Renderer>,
}
impl Data {
/// Creates a new [`Data`] 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 [`Data`] 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 [`Data`] 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 [`Data`] 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,
}
}
}
#[derive(Default)]
struct State {
last_style: RefCell<Option<Style>>,
}
/// The appearance of a QR code.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The color of the QR code data cells
pub cell: Color,
/// The color of the QR code background
pub background: Color,
}
/// The theme catalog of a [`QRCode`].
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`QRCode`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// The default style of a [`QRCode`].
pub fn default(theme: &Theme) -> Style {
let palette = theme.palette();
Style {
cell: palette.text,
background: palette.background,
}
}
+551
View File
@@ -0,0 +1,551 @@
//! Radio buttons let users choose a single option from a bunch of options.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::{column, radio};
//!
//! struct State {
//! selection: Option<Choice>,
//! }
//!
//! #[derive(Debug, Clone, Copy)]
//! enum Message {
//! RadioSelected(Choice),
//! }
//!
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
//! enum Choice {
//! A,
//! B,
//! C,
//! All,
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! let a = radio(
//! "A",
//! Choice::A,
//! state.selection,
//! Message::RadioSelected,
//! );
//!
//! let b = radio(
//! "B",
//! Choice::B,
//! state.selection,
//! Message::RadioSelected,
//! );
//!
//! let c = radio(
//! "C",
//! Choice::C,
//! state.selection,
//! Message::RadioSelected,
//! );
//!
//! let all = radio(
//! "All of the above",
//! Choice::All,
//! state.selection,
//! Message::RadioSelected
//! );
//!
//! column![a, b, c, all].into()
//! }
//! ```
use crate::core::alignment;
use crate::core::border::{self, Border};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
Rectangle, Shell, Size, Theme, Widget,
};
/// A circular button representing a choice.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::{column, radio};
///
/// struct State {
/// selection: Option<Choice>,
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// enum Message {
/// RadioSelected(Choice),
/// }
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// enum Choice {
/// A,
/// B,
/// C,
/// All,
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// let a = radio(
/// "A",
/// Choice::A,
/// state.selection,
/// Message::RadioSelected,
/// );
///
/// let b = radio(
/// "B",
/// Choice::B,
/// state.selection,
/// Message::RadioSelected,
/// );
///
/// let c = radio(
/// "C",
/// Choice::C,
/// state.selection,
/// Message::RadioSelected,
/// );
///
/// let all = radio(
/// "All of the above",
/// Choice::All,
/// state.selection,
/// Message::RadioSelected
/// );
///
/// column![a, b, c, all].into()
/// }
/// ```
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
is_selected: bool,
on_click: Message,
label: String,
width: Length,
size: f32,
spacing: f32,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
last_status: Option<Status>,
}
impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
/// The default size of a [`Radio`] button.
pub const DEFAULT_SIZE: f32 = 16.0;
/// The default spacing of a [`Radio`] button.
pub const DEFAULT_SPACING: f32 = 8.0;
/// Creates a new [`Radio`] button.
///
/// It expects:
/// * the value related to the [`Radio`] button
/// * the label of the [`Radio`] button
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>(
label: impl Into<String>,
value: V,
selected: Option<V>,
f: F,
) -> Self
where
V: Eq + Copy,
F: FnOnce(V) -> Message,
{
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::default(),
text_wrapping: text::Wrapping::default(),
font: None,
class: Theme::default(),
last_status: None,
}
}
/// 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());
self
}
/// Sets the text [`text::LineHeight`] of the [`Radio`] button.
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
) -> Self {
self.text_line_height = line_height.into();
self
}
/// Sets the [`text::Shaping`] strategy of the [`Radio`] button.
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
self.text_shaping = shaping;
self
}
/// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
self.text_wrapping = wrapping;
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.
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Radio`] button.
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Radio<'_, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::next_to_each_other(
&limits.width(self.width),
self.spacing,
|_| layout::Node::new(Size::new(self.size, self.size)),
|limits| {
let state = tree
.state
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
widget::text::layout(
state,
renderer,
limits,
&self.label,
widget::text::Format {
width: self.width,
height: Length::Shrink,
line_height: self.text_line_height,
size: self.text_size,
font: self.font,
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Top,
shaping: self.text_shaping,
wrapping: self.text_wrapping,
},
)
},
)
}
fn update(
&mut self,
_tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if cursor.is_over(layout.bounds()) {
shell.publish(self.on_click.clone());
shell.capture_event();
}
}
_ => {}
}
let current_status = {
let is_mouse_over = cursor.is_over(layout.bounds());
let is_selected = self.is_selected;
if is_mouse_over {
Status::Hovered { is_selected }
} else {
Status::Active { is_selected }
}
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_status = Some(current_status);
} else if self
.last_status
.is_some_and(|last_status| last_status != current_status)
{
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let mut children = layout.children();
let style = theme.style(
&self.class,
self.last_status.unwrap_or(Status::Active {
is_selected: 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: Border {
radius: (size / 2.0).into(),
width: style.border_width,
color: style.border_color,
},
..renderer::Quad::default()
},
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: border::rounded(dot_size / 2.0),
..renderer::Quad::default()
},
style.dot_color,
);
}
}
{
let label_layout = children.next().unwrap();
let state: &widget::text::State<Renderer::Paragraph> =
tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout.bounds(),
state.raw(),
crate::text::Style {
color: style.text_color,
},
viewport,
);
}
}
}
impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a + Catalog,
Renderer: 'a + text::Renderer,
{
fn from(
radio: Radio<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(radio)
}
}
/// The possible status of a [`Radio`] button.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Radio`] button can be interacted with.
Active {
/// Indicates whether the [`Radio`] button is currently selected.
is_selected: bool,
},
/// The [`Radio`] button is being hovered.
Hovered {
/// Indicates whether the [`Radio`] button is currently selected.
is_selected: bool,
},
}
/// The appearance of a radio button.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the radio button.
pub background: Background,
/// The [`Color`] of the dot of the radio button.
pub dot_color: Color,
/// The border width of the radio button.
pub border_width: f32,
/// The border [`Color`] of the radio button.
pub border_color: Color,
/// The text [`Color`] of the radio button.
pub text_color: Option<Color>,
}
/// The theme catalog of a [`Radio`].
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
/// A styling function for a [`Radio`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
/// The default style of a [`Radio`] button.
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let active = Style {
background: Color::TRANSPARENT.into(),
dot_color: palette.primary.strong.color,
border_width: 1.0,
border_color: palette.primary.strong.color,
text_color: None,
};
match status {
Status::Active { .. } => active,
Status::Hovered { .. } => Style {
dot_color: palette.primary.strong.color,
background: palette.primary.weak.color.into(),
..active
},
}
}
+207
View File
@@ -0,0 +1,207 @@
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;
use crate::core::{
self, Clipboard, Element, Event, Length, Rectangle, Shell, Size, Vector,
Widget,
};
use crate::space;
/// A widget that is aware of its dimensions.
///
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
pub struct Responsive<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
view: Box<dyn Fn(Size) -> Element<'a, Message, Theme, Renderer> + 'a>,
width: Length,
height: Length,
content: Element<'a, Message, Theme, Renderer>,
}
impl<'a, Message, Theme, Renderer> Responsive<'a, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
/// Creates a new [`Responsive`] widget with a closure that produces its
/// contents.
///
/// The `view` closure will receive the maximum available space for
/// the [`Responsive`] during layout. You can use this [`Size`] to
/// conditionally build the contents.
pub fn new(
view: impl Fn(Size) -> Element<'a, Message, Theme, Renderer> + 'a,
) -> Self {
Self {
view: Box::new(view),
width: Length::Fill,
height: Length::Fill,
content: Element::new(space()),
}
}
/// Sets the width of the [`Responsive`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Responsive`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Responsive<'_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
fn diff(&self, _tree: &mut Tree) {
// Diff is deferred to layout
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let size = limits.max();
self.content = (self.view)(size);
tree.diff_children(std::slice::from_ref(&self.content));
let node = self.content.as_widget_mut().layout(
&mut tree.children[0],
renderer,
&limits.loose(),
);
let size = limits.resolve(self.width, self.height, node.size());
layout::Node::with_children(size, vec![node])
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout.children().next().unwrap(),
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout.children().next().unwrap(),
cursor,
viewport,
renderer,
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
}
fn overlay<'a>(
&'a mut self,
tree: &'a mut Tree,
layout: Layout<'a>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer>
From<Responsive<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: core::Renderer + 'a,
{
fn from(responsive: Responsive<'a, Message, Theme, Renderer>) -> Self {
Self::new(responsive)
}
}
+605
View File
@@ -0,0 +1,605 @@
//! Distribute content horizontally.
use crate::core::alignment::{self, Alignment};
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::{
Clipboard, Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size,
Vector, Widget,
};
/// A container that distributes its contents horizontally.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::{button, row};
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// row![
/// "I am to the left!",
/// button("I am in the middle!"),
/// "I am to the right!",
/// ].into()
/// }
/// ```
pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
height: Length,
align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
impl<'a, Message, Theme, Renderer> Row<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Row`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`Row`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`Row`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Row`] from an already allocated [`Vec`].
///
/// Keep in mind that the [`Row`] will not inspect the [`Vec`], which means
/// it won't automatically adapt to the sizing strategy of its contents.
///
/// If any of the children have a [`Length::Fill`] strategy, you will need to
/// call [`Row::width`] or [`Row::height`] accordingly.
pub fn from_vec(
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
align: Alignment::Start,
clip: false,
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_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
self.align = Alignment::from(align.into());
self
}
/// Sets whether the contents of the [`Row`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Adds an [`Element`] to the [`Row`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
if !child_size.is_void() {
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
self.children.push(child);
}
self
}
/// Extends the [`Row`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
/// Turns the [`Row`] into a [`Wrapping`] row.
///
/// The original alignment of the [`Row`] is preserved per row wrapped.
pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
Wrapping {
row: self,
vertical_spacing: None,
align_x: alignment::Horizontal::Left,
}
}
}
impl<Message, Renderer> Default for Row<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Theme, Renderer: crate::core::Renderer>
FromIterator<Element<'a, Message, Theme, Renderer>>
for Row<'a, Message, Theme, Renderer>
{
fn from_iter<
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
>(
iter: T,
) -> Self {
Self::with_children(iter)
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Row<'_, Message, Theme, 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 size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::flex::resolve(
layout::flex::Axis::Horizontal,
renderer,
limits,
self.width,
self.height,
self.padding,
self.spacing,
self.align,
&mut self.children,
&mut tree.children,
)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((child, tree), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
child.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, tree), layout)| {
child
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
for ((child, tree), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(viewport))
{
child.as_widget().draw(
tree, renderer, theme, style, layout, cursor, viewport,
);
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Row<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(row: Row<'a, Message, Theme, Renderer>) -> Self {
Self::new(row)
}
}
/// A [`Row`] that wraps its contents.
///
/// Create a [`Row`] first, and then call [`Row::wrap`] to
/// obtain a [`Row`] that wraps its contents.
///
/// The original alignment of the [`Row`] is preserved per row wrapped.
pub struct Wrapping<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
row: Row<'a, Message, Theme, Renderer>,
vertical_spacing: Option<f32>,
align_x: alignment::Horizontal,
}
impl<Message, Theme, Renderer> Wrapping<'_, Message, Theme, Renderer> {
/// Sets the vertical spacing _between_ lines.
pub fn vertical_spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.vertical_spacing = Some(amount.into().0);
self
}
/// Sets the horizontal alignment of the wrapping [`Row`].
pub fn align_x(
mut self,
align_x: impl Into<alignment::Horizontal>,
) -> Self {
self.align_x = align_x.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Wrapping<'_, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.row.children()
}
fn diff(&self, tree: &mut Tree) {
self.row.diff(tree);
}
fn size(&self) -> Size<Length> {
self.row.size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits
.width(self.row.width)
.height(self.row.height)
.shrink(self.row.padding);
let child_limits = limits.loose();
let spacing = self.row.spacing;
let vertical_spacing = self.vertical_spacing.unwrap_or(spacing);
let max_width = limits.max().width;
let mut children: Vec<layout::Node> = Vec::new();
let mut intrinsic_size = Size::ZERO;
let mut row_start = 0;
let mut row_height = 0.0;
let mut x = 0.0;
let mut y = 0.0;
let align_factor = match self.row.align {
Alignment::Start => 0.0,
Alignment::Center => 2.0,
Alignment::End => 1.0,
};
let align_y = |row_start: std::ops::Range<usize>,
row_height: f32,
children: &mut Vec<layout::Node>| {
if align_factor != 0.0 {
for node in &mut children[row_start] {
let height = node.size().height;
node.translate_mut(Vector::new(
0.0,
(row_height - height) / align_factor,
));
}
}
};
for (i, child) in self.row.children.iter_mut().enumerate() {
let node = child.as_widget_mut().layout(
&mut tree.children[i],
renderer,
&child_limits,
);
let child_size = node.size();
if x != 0.0 && x + child_size.width > max_width {
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
align_y(row_start..i, row_height, &mut children);
y += row_height + vertical_spacing;
x = 0.0;
row_start = i;
row_height = 0.0;
}
row_height = row_height.max(child_size.height);
children.push(node.move_to((
x + self.row.padding.left,
y + self.row.padding.top,
)));
x += child_size.width + spacing;
}
if x != 0.0 {
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
}
intrinsic_size.height = y + row_height;
align_y(row_start..children.len(), row_height, &mut children);
let align_factor = match self.align_x {
alignment::Horizontal::Left => 0.0,
alignment::Horizontal::Center => 2.0,
alignment::Horizontal::Right => 1.0,
};
if align_factor != 0.0 {
let total_width = intrinsic_size.width;
let mut row_start = 0;
for i in 0..children.len() {
let bounds = children[i].bounds();
let row_width = bounds.x + bounds.width;
let next_x = children
.get(i + 1)
.map(|node| node.bounds().x)
.unwrap_or_default();
if next_x == 0.0 {
let translation = Vector::new(
(total_width - row_width) / align_factor,
0.0,
);
for node in &mut children[row_start..=i] {
node.translate_mut(translation);
}
row_start = i + 1;
}
}
}
let size =
limits.resolve(self.row.width, self.row.height, intrinsic_size);
layout::Node::with_children(size.expand(self.row.padding), children)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.row.operate(tree, layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.row.update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.row
.mouse_interaction(tree, layout, cursor, viewport, renderer)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.row
.draw(tree, renderer, theme, style, layout, cursor, viewport);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.row
.overlay(tree, layout, renderer, viewport, translation)
}
}
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self {
Self::new(row)
}
}
+325
View File
@@ -0,0 +1,325 @@
//! Rules divide space horizontally or vertically.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::rule;
//!
//! #[derive(Clone)]
//! enum Message {
//! // ...,
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! rule::horizontal(2).into()
//! }
//! ```
use crate::core;
use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
};
/// Creates a new horizontal [`Rule`] with the given height.
pub fn horizontal<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme>
where
Theme: Catalog,
{
Rule {
thickness: Length::Fixed(height.into().0),
is_vertical: false,
class: Theme::default(),
}
}
/// Creates a new vertical [`Rule`] with the given width.
pub fn vertical<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme>
where
Theme: Catalog,
{
Rule {
thickness: Length::Fixed(width.into().0),
is_vertical: true,
class: Theme::default(),
}
}
/// Display a horizontal or vertical rule for dividing content.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::rule;
///
/// #[derive(Clone)]
/// enum Message {
/// // ...,
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// rule::horizontal(2).into()
/// }
/// ```
pub struct Rule<'a, Theme = crate::Theme>
where
Theme: Catalog,
{
thickness: Length,
is_vertical: bool,
class: Theme::Class<'a>,
}
impl<'a, Theme> Rule<'a, Theme>
where
Theme: Catalog,
{
/// Sets the style of the [`Rule`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Rule`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Rule<'_, Theme>
where
Renderer: core::Renderer,
Theme: Catalog,
{
fn size(&self) -> Size<Length> {
if self.is_vertical {
Size {
width: self.thickness,
height: Length::Fill,
}
} else {
Size {
width: Length::Fill,
height: self.thickness,
}
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let size = <Self as Widget<(), Theme, Renderer>>::size(self);
layout::atomic(limits, size.width, size.height)
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let style = theme.style(&self.class);
let bounds = if self.is_vertical {
let line_x = bounds.x.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: bounds.width,
height: line_height,
}
} else {
let line_y = bounds.y.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: bounds.height,
}
};
renderer.fill_quad(
renderer::Quad {
bounds,
border: border::rounded(style.radius),
snap: style.snap,
..renderer::Quad::default()
},
style.color,
);
}
}
impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a + Catalog,
Renderer: 'a + core::Renderer,
{
fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
Element::new(rule)
}
}
/// The appearance of a rule.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The color of the rule.
pub color: Color,
/// The radius of the line corners.
pub radius: border::Radius,
/// The [`FillMode`] of the rule.
pub fill_mode: FillMode,
/// Whether the rule should be snapped to the pixel grid.
pub snap: bool,
}
/// The fill mode of a rule.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FillMode {
/// Fill the whole length of the container.
Full,
/// Fill a percent of the length of the container. The rule
/// will be centered in that container.
///
/// The range is `[0.0, 100.0]`.
Percent(f32),
/// Uniform offset from each end, length units.
Padded(u16),
/// Different offset on each end of the rule, length units.
/// First = top or left.
AsymmetricPadding(u16, u16),
}
impl FillMode {
/// Return the starting offset and length of the rule.
///
/// * `space` - The space to fill.
///
/// # Returns
///
/// * (`starting_offset`, `length`)
pub fn fill(&self, space: f32) -> (f32, f32) {
match *self {
FillMode::Full => (0.0, space),
FillMode::Percent(percent) => {
if percent >= 100.0 {
(0.0, space)
} else {
let percent_width = (space * percent / 100.0).round();
(((space - percent_width) / 2.0).round(), percent_width)
}
}
FillMode::Padded(padding) => {
if padding == 0 {
(0.0, space)
} else {
let padding = padding as f32;
let mut line_width = space - (padding * 2.0);
if line_width < 0.0 {
line_width = 0.0;
}
(padding, line_width)
}
}
FillMode::AsymmetricPadding(first_pad, second_pad) => {
let first_pad = first_pad as f32;
let second_pad = second_pad as f32;
let mut line_width = space - first_pad - second_pad;
if line_width < 0.0 {
line_width = 0.0;
}
(first_pad, line_width)
}
}
}
}
/// The theme catalog of a [`Rule`].
pub trait Catalog: Sized {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`Rule`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// The default styling of a [`Rule`].
pub fn default(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
color: palette.background.strong.color,
radius: 0.0.into(),
fill_mode: FillMode::Full,
snap: true,
}
}
/// A [`Rule`] styling using the weak background color.
pub fn weak(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
color: palette.background.weak.color,
radius: 0.0.into(),
fill_mode: FillMode::Full,
snap: true,
}
}
File diff suppressed because it is too large Load Diff
+447
View File
@@ -0,0 +1,447 @@
//! Generate messages when content pops in and out of view.
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::time::{Duration, Instant};
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
self, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
Size, Vector, Widget,
};
/// A widget that can generate messages when its content pops in and out of view.
///
/// It can even notify you with anticipation at a given distance!
pub struct Sensor<
'a,
Key,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
content: Element<'a, Message, Theme, Renderer>,
key: Key,
on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_hide: Option<Message>,
anticipate: Pixels,
delay: Duration,
}
impl<'a, Message, Theme, Renderer> Sensor<'a, (), Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
/// Creates a new [`Sensor`] widget with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
key: (),
on_show: None,
on_resize: None,
on_hide: None,
anticipate: Pixels::ZERO,
delay: Duration::ZERO,
}
}
}
impl<'a, Key, Message, Theme, Renderer>
Sensor<'a, Key, Message, Theme, Renderer>
where
Key: self::Key,
Renderer: core::Renderer,
{
/// Sets the message to be produced when the content pops into view.
///
/// The closure will receive the [`Size`] of the content in that moment.
pub fn on_show(mut self, on_show: impl Fn(Size) -> Message + 'a) -> Self {
self.on_show = Some(Box::new(on_show));
self
}
/// Sets the message to be produced when the content changes [`Size`] once its in view.
///
/// The closure will receive the new [`Size`] of the content.
pub fn on_resize(
mut self,
on_resize: impl Fn(Size) -> Message + 'a,
) -> Self {
self.on_resize = Some(Box::new(on_resize));
self
}
/// Sets the message to be produced when the content pops out of view.
pub fn on_hide(mut self, on_hide: Message) -> Self {
self.on_hide = Some(on_hide);
self
}
/// Sets the key of the [`Sensor`] widget, for continuity.
///
/// If the key changes, the [`Sensor`] widget will trigger again.
pub fn key<K>(
self,
key: K,
) -> Sensor<'a, impl self::Key, Message, Theme, Renderer>
where
K: Clone + PartialEq + 'static,
{
Sensor {
content: self.content,
key: OwnedKey(key),
on_show: self.on_show,
on_resize: self.on_resize,
on_hide: self.on_hide,
anticipate: self.anticipate,
delay: self.delay,
}
}
/// Sets the key of the [`Sensor`], for continuity; using a reference.
///
/// If the key changes, the [`Sensor`] will trigger again.
pub fn key_ref<K>(
self,
key: &'a K,
) -> Sensor<'a, &'a K, Message, Theme, Renderer>
where
K: ToOwned + PartialEq<K::Owned> + ?Sized,
K::Owned: 'static,
{
Sensor {
content: self.content,
key,
on_show: self.on_show,
on_resize: self.on_resize,
on_hide: self.on_hide,
anticipate: self.anticipate,
delay: self.delay,
}
}
/// Sets the distance in [`Pixels`] to use in anticipation of the
/// content popping into view.
///
/// This can be quite useful to lazily load items in a long scrollable
/// behind the scenes before the user can notice it!
pub fn anticipate(mut self, distance: impl Into<Pixels>) -> Self {
self.anticipate = distance.into();
self
}
/// Sets the amount of time to wait before firing an [`on_show`] or
/// [`on_hide`] event; after the content is shown or hidden.
///
/// When combined with [`key`], this can be useful to debounce key changes.
///
/// [`on_show`]: Self::on_show
/// [`on_hide`]: Self::on_hide
/// [`key`]: Self::key
pub fn delay(mut self, delay: impl Into<Duration>) -> Self {
self.delay = delay.into();
self
}
}
#[derive(Debug, Clone)]
struct State<Key> {
has_popped_in: bool,
should_notify_at: Option<(bool, Instant)>,
last_size: Option<Size>,
last_key: Key,
}
impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Sensor<'_, Key, Message, Theme, Renderer>
where
Key: self::Key,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Key::Owned>>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
has_popped_in: false,
should_notify_at: None,
last_size: None,
last_key: self.key.to_owned(),
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&[&self.content]);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if let Event::Window(window::Event::RedrawRequested(now)) = &event {
let state = tree.state.downcast_mut::<State<Key::Owned>>();
if state.has_popped_in && !self.key.eq(&state.last_key) {
state.has_popped_in = false;
state.should_notify_at = None;
state.last_key = self.key.to_owned();
}
let bounds = layout.bounds();
let top_left_distance = viewport.distance(bounds.position());
let bottom_right_distance = viewport
.distance(bounds.position() + Vector::from(bounds.size()));
let distance = top_left_distance.min(bottom_right_distance);
if self.on_show.is_none() {
if let Some(on_resize) = &self.on_resize {
let size = bounds.size();
if Some(size) != state.last_size {
state.last_size = Some(size);
shell.publish(on_resize(size));
}
}
} else if state.has_popped_in {
if distance <= self.anticipate.0 {
if let Some(on_resize) = &self.on_resize {
let size = bounds.size();
if Some(size) != state.last_size {
state.last_size = Some(size);
shell.publish(on_resize(size));
}
}
} else if self.on_hide.is_some() {
state.has_popped_in = false;
state.should_notify_at = Some((false, *now + self.delay));
}
} else if distance <= self.anticipate.0 {
let size = bounds.size();
state.has_popped_in = true;
state.should_notify_at = Some((true, *now + self.delay));
state.last_size = Some(size);
}
match &state.should_notify_at {
Some((has_popped_in, at)) if at <= now => {
if *has_popped_in {
if let Some(on_show) = &self.on_show {
shell.publish(on_show(layout.bounds().size()));
}
} else if let Some(on_hide) = self.on_hide.take() {
shell.publish(on_hide);
}
state.should_notify_at = None;
}
Some((_, at)) => {
shell.request_redraw_at(*at);
}
None => {}
}
}
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.content.as_widget().size_hint()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget_mut().layout(
&mut tree.children[0],
renderer,
limits,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: layout::Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn operate(
&mut self,
tree: &mut Tree,
layout: core::Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: core::Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: core::Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: core::Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Key, Message, Theme, Renderer>
From<Sensor<'a, Key, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Key: self::Key + 'a,
Renderer: core::Renderer + 'a,
Theme: 'a,
{
fn from(pop: Sensor<'a, Key, Message, Theme, Renderer>) -> Self {
Element::new(pop)
}
}
/// The key of a widget.
///
/// You should generally not need to care about this trait.
pub trait Key {
/// The owned version of the key.
type Owned: 'static;
/// Returns the owned version of the key.
fn to_owned(&self) -> Self::Owned;
/// Compares the key with the given owned version.
fn eq(&self, other: &Self::Owned) -> bool;
}
impl<T> Key for &T
where
T: ToOwned + PartialEq<T::Owned> + ?Sized,
T::Owned: 'static,
{
type Owned = T::Owned;
fn to_owned(&self) -> <Self as Key>::Owned {
ToOwned::to_owned(*self)
}
fn eq(&self, other: &Self::Owned) -> bool {
*self == other
}
}
struct OwnedKey<T>(T);
impl<T> Key for OwnedKey<T>
where
T: PartialEq + Clone + 'static,
{
type Owned = T;
fn to_owned(&self) -> Self::Owned {
self.0.clone()
}
fn eq(&self, other: &Self::Owned) -> bool {
&self.0 == other
}
}
impl<T> PartialEq<T> for OwnedKey<T>
where
T: PartialEq,
{
fn eq(&self, other: &T) -> bool {
&self.0 == other
}
}
impl Key for () {
type Owned = ();
fn to_owned(&self) -> Self::Owned {}
fn eq(&self, _other: &Self::Owned) -> bool {
true
}
}
+200
View File
@@ -0,0 +1,200 @@
//! A custom shader widget for wgpu applications.
mod program;
pub use program::Program;
use crate::core::event;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size};
use crate::renderer::wgpu::primitive;
use std::marker::PhantomData;
pub use crate::Action;
pub use crate::graphics::Viewport;
pub use primitive::{Pipeline, Primitive, Storage};
/// A widget which can render custom shaders with Iced's `wgpu` backend.
///
/// Must be initialized with a [`Program`], which describes the internal widget state & how
/// its [`Program::Primitive`]s are drawn.
pub struct Shader<Message, P: Program<Message>> {
width: Length,
height: Length,
program: P,
_message: PhantomData<Message>,
}
impl<Message, P: Program<Message>> Shader<Message, P> {
/// Create a new custom [`Shader`].
pub fn new(program: P) -> Self {
Self {
width: Length::Fixed(100.0),
height: Length::Fixed(100.0),
program,
_message: PhantomData,
}
}
/// Set the `width` of the custom [`Shader`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Set the `height` of the custom [`Shader`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Shader<Message, P>
where
P: Program<Message>,
Renderer: primitive::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 size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width, self.height)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let state = tree.state.downcast_mut::<P::State>();
if let Some(action) = self.program.update(state, event, bounds, cursor)
{
let (message, redraw_request, event_status) = action.into_inner();
shell.request_redraw_at(redraw_request);
if let Some(message) = message {
shell.publish(message);
}
if event_status == event::Status::Captured {
shell.capture_event();
}
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let state = tree.state.downcast_ref::<P::State>();
self.program.mouse_interaction(state, bounds, cursor)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let state = tree.state.downcast_ref::<P::State>();
renderer.draw_primitive(
bounds,
self.program.draw(state, cursor_position, bounds),
);
}
}
impl<'a, Message, Theme, Renderer, P> From<Shader<Message, P>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Renderer: primitive::Renderer,
P: Program<Message> + 'a,
{
fn from(
custom: Shader<Message, P>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(custom)
}
}
impl<Message, T> Program<Message> for &T
where
T: Program<Message>,
{
type State = T::State;
type Primitive = T::Primitive;
fn update(
&self,
state: &mut Self::State,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
T::update(self, state, event, bounds, cursor)
}
fn draw(
&self,
state: &Self::State,
cursor: mouse::Cursor,
bounds: Rectangle,
) -> Self::Primitive {
T::draw(self, state, cursor, bounds)
}
fn mouse_interaction(
&self,
state: &Self::State,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
T::mouse_interaction(self, state, bounds, cursor)
}
}
+60
View File
@@ -0,0 +1,60 @@
use crate::core::Rectangle;
use crate::core::mouse;
use crate::renderer::wgpu::Primitive;
use crate::shader::{self, Action};
/// The state and logic of a [`Shader`] widget.
///
/// A [`Program`] can mutate the internal state of a [`Shader`] widget
/// and produce messages for an application.
///
/// [`Shader`]: crate::Shader
pub trait Program<Message> {
/// The internal state of the [`Program`].
type State: Default + 'static;
/// The type of primitive this [`Program`] can draw.
type Primitive: Primitive + 'static;
/// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
/// based on mouse & other events. You can return an [`Action`] to publish a message, request a
/// redraw, or capture the event.
///
/// By default, this method returns `None`.
///
/// [`State`]: Self::State
fn update(
&self,
_state: &mut Self::State,
_event: &shader::Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<Action<Message>> {
None
}
/// Draws the [`Primitive`].
///
/// [`Primitive`]: Self::Primitive
fn draw(
&self,
state: &Self::State,
cursor: mouse::Cursor,
bounds: Rectangle,
) -> Self::Primitive;
/// 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 [`Shader`]'s program.
///
/// [`Shader`]: crate::Shader
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
}
+702
View File
@@ -0,0 +1,702 @@
//! Sliders let users set a value by moving an indicator.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::slider;
//!
//! struct State {
//! value: f32,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Message {
//! ValueChanged(f32),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! slider(0.0..=100.0, state.value, Message::ValueChanged).into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::ValueChanged(value) => {
//! state.value = value;
//! }
//! }
//! }
//! ```
use crate::core::border::{self, Border};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
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::window;
use crate::core::{
self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
Point, Rectangle, Shell, Size, Theme, Widget,
};
use std::ops::RangeInclusive;
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
/// The [`Slider`] range of numeric values is generic and its step size defaults
/// to 1 unit.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::slider;
///
/// struct State {
/// value: f32,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// ValueChanged(f32),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// slider(0.0..=100.0, state.value, Message::ValueChanged).into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::ValueChanged(value) => {
/// state.value = value;
/// }
/// }
/// }
/// ```
pub struct Slider<'a, T, Message, Theme = crate::Theme>
where
Theme: Catalog,
{
range: RangeInclusive<T>,
step: T,
shift_step: Option<T>,
value: T,
default: Option<T>,
on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>,
width: Length,
height: f32,
class: Theme::Class<'a>,
status: Option<Status>,
}
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
where
T: Copy + From<u8> + PartialOrd,
Message: Clone,
Theme: Catalog,
{
/// The default height of a [`Slider`].
pub const DEFAULT_HEIGHT: f32 = 16.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,
default: None,
range,
step: T::from(1),
shift_step: None,
on_change: Box::new(on_change),
on_release: None,
width: Length::Fill,
height: Self::DEFAULT_HEIGHT,
class: Theme::default(),
status: None,
}
}
/// Sets the optional default value for the [`Slider`].
///
/// If set, the [`Slider`] will reset to this value when ctrl-clicked or command-clicked.
pub fn default(mut self, default: impl Into<T>) -> Self {
self.default = Some(default.into());
self
}
/// 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 step size of the [`Slider`].
pub fn step(mut self, step: impl Into<T>) -> Self {
self.step = step.into();
self
}
/// Sets the optional "shift" step for the [`Slider`].
///
/// If set, this value is used as the step while the shift key is pressed.
pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
self.shift_step = Some(shift_step.into());
self
}
/// Sets the style of the [`Slider`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Slider`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Slider<'_, T, Message, Theme>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
Theme: Catalog,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width, self.height)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
let mut update = || {
let current_value = self.value;
let locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
if cursor_position.x <= bounds.x {
Some(*self.range.start())
} else if cursor_position.x >= bounds.x + bounds.width {
Some(*self.range.end())
} else {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let start = (*self.range.start()).into();
let end = (*self.range.end()).into();
let percent = f64::from(cursor_position.x - bounds.x)
/ f64::from(bounds.width);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value.min(end))
}
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
if new_value > (*self.range.end()).into() {
return Some(*self.range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
if new_value < (*self.range.start()).into() {
return Some(*self.range.start());
}
T::from_f64(new_value)
};
let change = |new_value: T| {
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((self.on_change)(new_value));
self.value = new_value;
}
};
match &event {
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) =
cursor.position_over(layout.bounds())
{
if state.keyboard_modifiers.command() {
let _ = self.default.map(change);
state.is_dragging = false;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
}
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left,
))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if state.is_dragging {
if let Some(on_release) = self.on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if state.is_dragging {
let _ = cursor
.land()
.position()
.and_then(locate)
.map(change);
shell.capture_event();
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta })
if state.keyboard_modifiers.control() =>
{
if cursor.is_over(layout.bounds()) {
let delta = match delta {
mouse::ScrollDelta::Lines { x: _, y } => y,
mouse::ScrollDelta::Pixels { x: _, y } => y,
};
if *delta < 0.0 {
let _ = decrement(current_value).map(change);
} else {
let _ = increment(current_value).map(change);
}
shell.capture_event();
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key, ..
}) => {
if cursor.is_over(layout.bounds()) {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
shell.capture_event();
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
shell.capture_event();
}
_ => (),
}
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(
modifiers,
)) => {
state.keyboard_modifiers = *modifiers;
}
_ => {}
}
};
update();
let current_status = if state.is_dragging {
Status::Dragged
} else if cursor.is_over(layout.bounds()) {
Status::Hovered
} else {
Status::Active
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.status = Some(current_status);
} else if self.status.is_some_and(|status| status != current_status) {
shell.request_redraw();
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Active));
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
(radius * 2.0, radius * 2.0, radius.into())
}
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.height, border_radius),
};
let value = self.value.into() as f32;
let (range_start, range_end) = {
let (start, end) = self.range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let offset = if range_start >= range_end {
0.0
} else {
(bounds.width - handle_width) * (value - range_start)
/ (range_end - range_start)
};
let rail_y = bounds.y + bounds.height / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: rail_y - style.rail.width / 2.0,
width: offset + handle_width / 2.0,
height: style.rail.width,
},
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.backgrounds.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset + handle_width / 2.0,
y: rail_y - style.rail.width / 2.0,
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.backgrounds.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset,
y: rail_y - handle_height / 2.0,
width: handle_width,
height: handle_height,
},
border: Border {
radius: handle_border_radius,
width: style.handle.border_width,
color: style.handle.border_color,
},
..renderer::Quad::default()
},
style.handle.background,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
if state.is_dragging {
// FIXME: Fall back to `Pointer` on Windows
// See https://github.com/rust-windowing/winit/issues/1043
if cfg!(target_os = "windows") {
mouse::Interaction::Pointer
} else {
mouse::Interaction::Grabbing
}
} else if cursor.is_over(layout.bounds()) {
if cfg!(target_os = "windows") {
mouse::Interaction::Pointer
} else {
mouse::Interaction::Grab
}
} else {
mouse::Interaction::default()
}
}
}
impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
for Element<'a, Message, Theme, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(
slider: Slider<'a, T, Message, Theme>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(slider)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct State {
is_dragging: bool,
keyboard_modifiers: keyboard::Modifiers,
}
/// The possible status of a [`Slider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Slider`] can be interacted with.
Active,
/// The [`Slider`] is being hovered.
Hovered,
/// The [`Slider`] is being dragged.
Dragged,
}
/// The appearance of a slider.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The colors of the rail of the slider.
pub rail: Rail,
/// The appearance of the [`Handle`] of the slider.
pub handle: Handle,
}
impl Style {
/// Changes the [`HandleShape`] of the [`Style`] to a circle
/// with the given radius.
pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
self.handle.shape = HandleShape::Circle {
radius: radius.into().0,
};
self
}
}
/// The appearance of a slider rail
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rail {
/// The backgrounds of the rail of the slider.
pub backgrounds: (Background, Background),
/// The width of the stroke of a slider rail.
pub width: f32,
/// The border of the rail.
pub border: Border,
}
/// The appearance of the handle of a slider.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
/// The [`Background`] of the handle.
pub background: Background,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
pub border_color: Color,
}
/// The shape of the handle of a slider.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HandleShape {
/// A circular handle.
Circle {
/// The radius of the circle.
radius: f32,
},
/// A rectangular shape.
Rectangle {
/// The width of the rectangle.
width: u16,
/// The border radius of the corners of the rectangle.
border_radius: border::Radius,
},
}
/// The theme catalog of a [`Slider`].
pub trait Catalog: Sized {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
/// A styling function for a [`Slider`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
/// The default style of a [`Slider`].
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let color = match status {
Status::Active => palette.primary.base.color,
Status::Hovered => palette.primary.strong.color,
Status::Dragged => palette.primary.weak.color,
};
Style {
rail: Rail {
backgrounds: (color.into(), palette.background.strong.color.into()),
width: 4.0,
border: Border {
radius: 2.0.into(),
width: 0.0,
color: Color::TRANSPARENT,
},
},
handle: Handle {
shape: HandleShape::Circle { radius: 7.0 },
background: color.into(),
border_color: Color::TRANSPARENT,
border_width: 0.0,
},
}
}
+104
View File
@@ -0,0 +1,104 @@
//! Add some explicit spacing between elements.
use crate::core;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{Element, Layout, Length, Rectangle, Size, Widget};
/// Creates a new [`Space`] widget that fills the available
/// horizontal space.
///
/// This can be useful to separate widgets in a [`Row`](crate::Row).
pub fn horizontal() -> Space {
Space::new().width(Length::Fill)
}
/// Creates a new [`Space`] widget that fills the available
/// vertical space.
///
/// This can be useful to separate widgets in a [`Column`](crate::Column).
pub fn vertical() -> Space {
Space::new().height(Length::Fill)
}
/// 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 some empty [`Space`] with no size.
pub fn new() -> Self {
Space {
width: Length::Shrink,
height: Length::Shrink,
}
}
/// Sets the width of the [`Space`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Space`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
impl Default for Space {
fn default() -> Self {
Space::new()
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Space
where
Renderer: core::Renderer,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width, self.height)
}
fn draw(
&self,
_tree: &Tree,
_renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
}
}
impl<'a, Message, Theme, Renderer> From<Space>
for Element<'a, Message, Theme, Renderer>
where
Renderer: core::Renderer,
Message: 'a,
{
fn from(space: Space) -> Element<'a, Message, Theme, Renderer> {
Element::new(space)
}
}
+405
View File
@@ -0,0 +1,405 @@
//! Display content on top of other content.
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::{
Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector,
Widget,
};
/// A container that displays children on top of each other.
///
/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and
/// will be displayed as the base layer. Every consecutive [`Element`] will be
/// rendered on top; on its own layer.
///
/// You can use [`push_under`](Self::push_under) to push an [`Element`] under
/// the current [`Stack`] without affecting its intrinsic [`Size`].
///
/// Keep in mind that too much layering will normally produce bad UX as well as
/// introduce certain rendering overhead. Use this widget sparingly!
pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
{
width: Length,
height: Length,
children: Vec<Element<'a, Message, Theme, Renderer>>,
clip: bool,
base_layer: usize,
}
impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Stack`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`Stack`] with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self::from_vec(Vec::with_capacity(capacity))
}
/// Creates a [`Stack`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
let iterator = children.into_iter();
Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Stack`] from an already allocated [`Vec`].
///
/// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means
/// it won't automatically adapt to the sizing strategy of its contents.
///
/// If any of the children have a [`Length::Fill`] strategy, you will need to
/// call [`Stack::width`] or [`Stack::height`] accordingly.
pub fn from_vec(
children: Vec<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
width: Length::Shrink,
height: Length::Shrink,
children,
clip: false,
base_layer: 0,
}
}
/// Sets the width of the [`Stack`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Stack`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Adds an element on top of the [`Stack`].
pub fn push(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
if !child_size.is_void() {
if self.children.is_empty() {
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
}
self.children.push(child);
}
self
}
/// Adds an element under the [`Stack`].
pub fn push_under(
mut self,
child: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
self.children.insert(0, child.into());
self.base_layer += 1;
self
}
/// Extends the [`Stack`] with the given children.
pub fn extend(
self,
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
children.into_iter().fold(self, Self::push)
}
/// Sets whether the [`Stack`] should clip overflowing content.
///
/// It has a slight performance overhead during presentation.
///
/// By default, it is set to `false`.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
}
impl<Message, Renderer> Default for Stack<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Stack<'a, Message, Theme, 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 size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
if self.children.len() <= self.base_layer {
return layout::Node::new(limits.resolve(
self.width,
self.height,
Size::ZERO,
));
}
let base = self.children[self.base_layer].as_widget_mut().layout(
&mut tree.children[self.base_layer],
renderer,
&limits,
);
let size = limits.resolve(self.width, self.height, base.size());
let limits = layout::Limits::new(Size::ZERO, size);
let (under, above) = self.children.split_at_mut(self.base_layer);
let (tree_under, tree_above) =
tree.children.split_at_mut(self.base_layer);
let nodes = under
.iter_mut()
.zip(tree_under)
.map(|(layer, tree)| {
layer.as_widget_mut().layout(tree, renderer, &limits)
})
.chain(std::iter::once(base))
.chain(above[1..].iter_mut().zip(&mut tree_above[1..]).map(
|(layer, tree)| {
layer.as_widget_mut().layout(tree, renderer, &limits)
},
))
.collect();
layout::Node::with_children(size, nodes)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget_mut()
.operate(state, layout, renderer, operation);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
mut cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if self.children.is_empty() {
return;
}
let is_over = cursor.is_over(layout.bounds());
let end = self.children.len() - 1;
for (i, ((child, tree), layout)) in self
.children
.iter_mut()
.rev()
.zip(tree.children.iter_mut().rev())
.zip(layout.children().rev())
.enumerate()
{
child.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
if shell.is_event_captured() {
return;
}
if i < end && is_over && !cursor.is_levitating() {
let interaction = child.as_widget().mouse_interaction(
tree, layout, cursor, viewport, renderer,
);
if interaction != mouse::Interaction::None {
cursor = cursor.levitate();
}
}
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.children
.iter()
.rev()
.zip(tree.children.iter().rev())
.zip(layout.children().rev())
.map(|((child, tree), layout)| {
child
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.find(|&interaction| interaction != mouse::Interaction::None)
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
let layers_under = if cursor.is_over(layout.bounds()) {
self.children
.iter()
.rev()
.zip(tree.children.iter().rev())
.zip(layout.children().rev())
.position(|((layer, tree), layout)| {
let interaction = layer.as_widget().mouse_interaction(
tree, layout, cursor, viewport, renderer,
);
interaction != mouse::Interaction::None
})
.map(|i| self.children.len() - i - 1)
.unwrap_or_default()
} else {
0
};
let mut layers = self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.enumerate();
let layers = layers.by_ref();
let mut draw_layer =
|i,
layer: &Element<'a, Message, Theme, Renderer>,
tree,
layout,
cursor| {
if i > 0 {
renderer.with_layer(*viewport, |renderer| {
layer.as_widget().draw(
tree, renderer, theme, style, layout, cursor,
viewport,
);
});
} else {
layer.as_widget().draw(
tree, renderer, theme, style, layout, cursor,
viewport,
);
}
};
for (i, ((layer, tree), layout)) in layers.take(layers_under) {
draw_layer(i, layer, tree, layout, mouse::Cursor::Unavailable);
}
for (i, ((layer, tree), layout)) in layers {
draw_layer(i, layer, tree, layout, cursor);
}
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self {
Self::new(stack)
}
}
+340
View File
@@ -0,0 +1,340 @@
//! Svg widgets display vector graphics in your application.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::svg;
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! svg("tiger.svg").into()
//! }
//! ```
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::svg;
use crate::core::widget::Tree;
use crate::core::window;
use crate::core::{
Clipboard, Color, ContentFit, Element, Event, Layout, Length, Point,
Rectangle, Rotation, Shell, Size, Theme, Vector, Widget,
};
use std::path::PathBuf;
pub use crate::core::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.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::svg;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// svg("tiger.svg").into()
/// }
/// ```
pub struct Svg<'a, Theme = crate::Theme>
where
Theme: Catalog,
{
handle: Handle,
width: Length,
height: Length,
content_fit: ContentFit,
class: Theme::Class<'a>,
rotation: Rotation,
opacity: f32,
status: Option<Status>,
}
impl<'a, Theme> Svg<'a, Theme>
where
Theme: Catalog,
{
/// 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,
class: Theme::default(),
rotation: Rotation::default(),
opacity: 1.0,
status: None,
}
}
/// 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 of the [`Svg`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Svg`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
/// Applies the given [`Rotation`] to the [`Svg`].
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
self.rotation = rotation.into();
self
}
/// Sets the opacity of the [`Svg`].
///
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
/// and `1.0` meaning completely opaque.
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
self.opacity = opacity.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Svg<'_, Theme>
where
Renderer: svg::Renderer,
Theme: Catalog,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
// The raw w/h of the underlying image
let Size { width, height } = renderer.measure_svg(&self.handle);
let image_size = Size::new(width as f32, height as f32);
// The rotated size of the svg
let rotated_size = self.rotation.apply(image_size);
// The size to be available to the widget prior to `Shrink`ing
let raw_size = limits.resolve(self.width, self.height, rotated_size);
// The uncropped size of the image when fit to the bounds above
let full_size = self.content_fit.fit(rotated_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 update(
&mut self,
_state: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let current_status = if cursor.is_over(layout.bounds()) {
Status::Hovered
} else {
Status::Idle
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.status = Some(current_status);
} else if self.status.is_some_and(|status| status != current_status) {
shell.request_redraw();
}
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let Size { width, height } = renderer.measure_svg(&self.handle);
let image_size = Size::new(width as f32, height as f32);
let rotated_size = self.rotation.apply(image_size);
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
let scale = Vector::new(
adjusted_fit.width / rotated_size.width,
adjusted_fit.height / rotated_size.height,
);
let final_size = image_size * scale;
let position = match self.content_fit {
ContentFit::None => Point::new(
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
),
_ => Point::new(
bounds.center_x() - final_size.width / 2.0,
bounds.center_y() - final_size.height / 2.0,
),
};
let drawing_bounds = Rectangle::new(position, final_size);
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Idle));
renderer.draw_svg(
svg::Svg {
handle: self.handle.clone(),
color: style.color,
rotation: self.rotation.radians(),
opacity: self.opacity,
},
drawing_bounds,
bounds,
);
}
}
impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
Theme: Catalog + 'a,
Renderer: svg::Renderer + 'a,
{
fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
Element::new(icon)
}
}
/// The possible status of an [`Svg`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Svg`] is idle.
Idle,
/// The [`Svg`] is being hovered.
Hovered,
}
/// The appearance of an [`Svg`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Style {
/// The [`Color`] filter of an [`Svg`].
///
/// Useful for coloring a symbolic icon.
///
/// `None` keeps the original color.
pub color: Option<Color>,
}
/// The theme catalog of an [`Svg`].
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(|_theme, _status| Style::default())
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
/// A styling function for an [`Svg`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl<Theme> From<Style> for StyleFn<'_, Theme> {
fn from(style: Style) -> Self {
Box::new(move |_theme, _status| style)
}
}
+725
View File
@@ -0,0 +1,725 @@
//! Display tables.
use crate::core;
use crate::core::alignment;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::{
Alignment, Background, Element, Layout, Length, Pixels, Rectangle, Size,
Widget,
};
/// Creates a new [`Table`] with the given columns and rows.
///
/// Columns can be created using the [`column()`] function, while rows can be any
/// iterator over some data type `T`.
pub fn table<'a, 'b, T, Message, Theme, Renderer>(
columns: impl IntoIterator<Item = Column<'a, 'b, T, Message, Theme, Renderer>>,
rows: impl IntoIterator<Item = T>,
) -> Table<'a, Message, Theme, Renderer>
where
T: Clone,
Theme: Catalog,
Renderer: core::Renderer,
{
Table::new(columns, rows)
}
/// Creates a new [`Column`] with the given header and view function.
///
/// The view function will be called for each row in a [`Table`] and it must
/// produce the resulting contents of a cell.
pub fn column<'a, 'b, T, E, Message, Theme, Renderer>(
header: impl Into<Element<'a, Message, Theme, Renderer>>,
view: impl Fn(T) -> E + 'b,
) -> Column<'a, 'b, T, Message, Theme, Renderer>
where
T: 'a,
E: Into<Element<'a, Message, Theme, Renderer>>,
{
Column {
header: header.into(),
view: Box::new(move |data| view(data).into()),
width: Length::Shrink,
align_x: alignment::Horizontal::Left,
align_y: alignment::Vertical::Top,
}
}
/// A grid-like visual representation of data distributed in columns and rows.
pub struct Table<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Theme: Catalog,
{
columns: Vec<Column_>,
cells: Vec<Element<'a, Message, Theme, Renderer>>,
width: Length,
height: Length,
padding_x: f32,
padding_y: f32,
separator_x: f32,
separator_y: f32,
class: Theme::Class<'a>,
}
struct Column_ {
width: Length,
align_x: alignment::Horizontal,
align_y: alignment::Vertical,
}
impl<'a, Message, Theme, Renderer> Table<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
/// Creates a new [`Table`] with the given columns and rows.
///
/// Columns can be created using the [`column()`] function, while rows can be any
/// iterator over some data type `T`.
pub fn new<'b, T>(
columns: impl IntoIterator<
Item = Column<'a, 'b, T, Message, Theme, Renderer>,
>,
rows: impl IntoIterator<Item = T>,
) -> Self
where
T: Clone,
{
let columns = columns.into_iter();
let rows = rows.into_iter();
let mut width = Length::Shrink;
let mut height = Length::Shrink;
let mut cells = Vec::with_capacity(
columns.size_hint().0 * (1 + rows.size_hint().0),
);
let (mut columns, views): (Vec<_>, Vec<_>) = columns
.map(|column| {
width = width.enclose(column.width);
cells.push(column.header);
(
Column_ {
width: column.width,
align_x: column.align_x,
align_y: column.align_y,
},
column.view,
)
})
.collect();
for row in rows {
for view in &views {
let cell = view(row.clone());
let size_hint = cell.as_widget().size_hint();
height = height.enclose(size_hint.height);
cells.push(cell);
}
}
if width == Length::Shrink
&& let Some(first) = columns.first_mut()
{
first.width = Length::Fill;
}
Self {
columns,
cells,
width,
height,
padding_x: 10.0,
padding_y: 5.0,
separator_x: 1.0,
separator_y: 1.0,
class: Theme::default(),
}
}
/// Sets the width of the [`Table`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the padding of the cells of the [`Table`].
pub fn padding(self, padding: impl Into<Pixels>) -> Self {
let padding = padding.into();
self.padding_x(padding).padding_y(padding)
}
/// Sets the horizontal padding of the cells of the [`Table`].
pub fn padding_x(mut self, padding: impl Into<Pixels>) -> Self {
self.padding_x = padding.into().0;
self
}
/// Sets the vertical padding of the cells of the [`Table`].
pub fn padding_y(mut self, padding: impl Into<Pixels>) -> Self {
self.padding_y = padding.into().0;
self
}
/// Sets the thickness of the line separator between the cells of the [`Table`].
pub fn separator(self, separator: impl Into<Pixels>) -> Self {
let separator = separator.into();
self.separator_x(separator).separator_y(separator)
}
/// Sets the thickness of the horizontal line separator between the cells of the [`Table`].
pub fn separator_x(mut self, separator: impl Into<Pixels>) -> Self {
self.separator_x = separator.into().0;
self
}
/// Sets the thickness of the vertical line separator between the cells of the [`Table`].
pub fn separator_y(mut self, separator: impl Into<Pixels>) -> Self {
self.separator_y = separator.into().0;
self
}
}
struct Metrics {
columns: Vec<f32>,
rows: Vec<f32>,
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Table<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<Metrics>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(Metrics {
columns: Vec::new(),
rows: Vec::new(),
})
}
fn children(&self) -> Vec<widget::Tree> {
self.cells
.iter()
.map(|cell| widget::Tree::new(cell.as_widget()))
.collect()
}
fn diff(&self, tree: &mut widget::Tree) {
tree.diff_children(&self.cells);
}
fn layout(
&mut self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let metrics = tree.state.downcast_mut::<Metrics>();
let columns = self.columns.len();
let rows = self.cells.len() / columns;
let limits = limits.width(self.width).height(self.height);
let available = limits.max();
let table_fluid = self.width.fluid();
let mut cells = Vec::with_capacity(self.cells.len());
cells.resize(self.cells.len(), layout::Node::default());
metrics.columns = vec![0.0; self.columns.len()];
metrics.rows = vec![0.0; rows];
let mut column_factors = vec![0; self.columns.len()];
let mut total_row_factors = 0;
let mut total_fluid_height = 0.0;
let mut row_factor = 0;
let spacing_x = self.padding_x * 2.0 + self.separator_x;
let spacing_y = self.padding_y * 2.0 + self.separator_y;
// FIRST PASS
// Lay out non-fluid cells
let mut x = self.padding_x;
let mut y = self.padding_y;
for (i, (cell, state)) in
self.cells.iter_mut().zip(&mut tree.children).enumerate()
{
let row = i / columns;
let column = i % columns;
let width = self.columns[column].width;
let size = cell.as_widget().size();
if column == 0 {
x = self.padding_x;
if row > 0 {
y += metrics.rows[row - 1] + spacing_y;
if row_factor != 0 {
total_fluid_height += metrics.rows[row - 1];
total_row_factors += row_factor;
row_factor = 0;
}
}
}
let width_factor = width.fill_factor();
let height_factor = size.height.fill_factor();
if width_factor != 0 || height_factor != 0 || size.width.is_fill() {
column_factors[column] =
column_factors[column].max(width_factor);
row_factor = row_factor.max(height_factor);
continue;
}
let limits = layout::Limits::new(
Size::ZERO,
Size::new(available.width - x, available.height - y),
)
.width(width);
let layout = cell.as_widget_mut().layout(state, renderer, &limits);
let size = limits.resolve(width, Length::Shrink, layout.size());
metrics.columns[column] = metrics.columns[column].max(size.width);
metrics.rows[row] = metrics.rows[row].max(size.height);
cells[i] = layout;
x += size.width + spacing_x;
}
// SECOND PASS
// Lay out fluid cells, using metrics from the first pass as limits
let left = Size::new(
available.width
- metrics
.columns
.iter()
.enumerate()
.filter(|(i, _)| column_factors[*i] == 0)
.map(|(_, width)| width)
.sum::<f32>(),
available.height - total_fluid_height,
);
let width_unit = (left.width
- spacing_x * self.columns.len().saturating_sub(1) as f32
- self.padding_x * 2.0)
/ column_factors.iter().sum::<u16>() as f32;
let height_unit = (left.height
- spacing_y * rows.saturating_sub(1) as f32
- self.padding_y * 2.0)
/ total_row_factors as f32;
let mut x = self.padding_x;
let mut y = self.padding_y;
for (i, (cell, state)) in
self.cells.iter_mut().zip(&mut tree.children).enumerate()
{
let row = i / columns;
let column = i % columns;
let size = cell.as_widget().size();
let width = self.columns[column].width;
let width_factor = width.fill_factor();
let height_factor = size.height.fill_factor();
if column == 0 {
x = self.padding_x;
if row > 0 {
y += metrics.rows[row - 1] + spacing_y;
}
}
if width_factor == 0
&& size.width.fill_factor() == 0
&& size.height.fill_factor() == 0
{
continue;
}
let max_width = if width_factor == 0 {
if size.width.is_fill() {
metrics.columns[column]
} else {
(available.width - x).max(0.0)
}
} else {
width_unit * width_factor as f32
};
let max_height = if height_factor == 0 {
if size.height.is_fill() {
metrics.rows[row]
} else {
(available.height - y).max(0.0)
}
} else {
height_unit * height_factor as f32
};
let limits = layout::Limits::new(
Size::ZERO,
Size::new(max_width, max_height),
)
.width(width);
let layout = cell.as_widget_mut().layout(state, renderer, &limits);
let size = limits.resolve(
if let Length::Fixed(_) = width {
width
} else {
table_fluid
},
Length::Shrink,
layout.size(),
);
metrics.columns[column] = metrics.columns[column].max(size.width);
metrics.rows[row] = metrics.rows[row].max(size.height);
cells[i] = layout;
x += size.width + spacing_x;
}
// THIRD PASS
// Position each cell
let mut x = self.padding_x;
let mut y = self.padding_y;
for (i, cell) in cells.iter_mut().enumerate() {
let row = i / columns;
let column = i % columns;
if column == 0 {
x = self.padding_x;
if row > 0 {
y += metrics.rows[row - 1] + spacing_y;
}
}
let Column_ {
align_x, align_y, ..
} = &self.columns[column];
cell.move_to_mut((x, y));
cell.align_mut(
Alignment::from(*align_x),
Alignment::from(*align_y),
Size::new(metrics.columns[column], metrics.rows[row]),
);
x += metrics.columns[column] + spacing_x;
}
let intrinsic = limits.resolve(
self.width,
self.height,
Size::new(
x - spacing_x + self.padding_x,
y + metrics
.rows
.last()
.copied()
.map(|height| height + self.padding_y)
.unwrap_or_default(),
),
);
layout::Node::with_children(intrinsic, cells)
}
fn update(
&mut self,
tree: &mut widget::Tree,
event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn core::Clipboard,
shell: &mut core::Shell<'_, Message>,
viewport: &Rectangle,
) {
for ((cell, tree), layout) in self
.cells
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
cell.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
for ((cell, state), layout) in
self.cells.iter().zip(&tree.children).zip(layout.children())
{
cell.as_widget()
.draw(state, renderer, theme, style, layout, cursor, viewport);
}
let bounds = layout.bounds();
let metrics = tree.state.downcast_ref::<Metrics>();
let style = theme.style(&self.class);
if self.separator_x > 0.0 {
let mut x = self.padding_x;
for width in
&metrics.columns[..metrics.columns.len().saturating_sub(1)]
{
x += width + self.padding_x;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + x,
y: bounds.y,
width: self.separator_x,
height: bounds.height,
},
snap: true,
..renderer::Quad::default()
},
style.separator_x,
);
x += self.separator_x + self.padding_x;
}
}
if self.separator_y > 0.0 {
let mut y = self.padding_y;
for height in &metrics.rows[..metrics.rows.len().saturating_sub(1)]
{
y += height + self.padding_y;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: bounds.y + y,
width: bounds.width,
height: self.separator_y,
},
snap: true,
..renderer::Quad::default()
},
style.separator_y,
);
y += self.separator_y + self.padding_y;
}
}
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.cells
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((cell, tree), layout)| {
cell.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
}
fn operate(
&mut self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
for ((cell, state), layout) in self
.cells
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
cell.as_widget_mut()
.operate(state, layout, renderer, operation);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut widget::Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: core::Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
overlay::from_children(
&mut self.cells,
tree,
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Table<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(table: Table<'a, Message, Theme, Renderer>) -> Self {
Element::new(table)
}
}
/// A vertical visualization of some data with a header.
pub struct Column<
'a,
'b,
T,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
header: Element<'a, Message, Theme, Renderer>,
view: Box<dyn Fn(T) -> Element<'a, Message, Theme, Renderer> + 'b>,
width: Length,
align_x: alignment::Horizontal,
align_y: alignment::Vertical,
}
impl<'a, 'b, T, Message, Theme, Renderer>
Column<'a, 'b, T, Message, Theme, Renderer>
{
/// Sets the width of the [`Column`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the alignment for the horizontal axis of the [`Column`].
pub fn align_x(
mut self,
alignment: impl Into<alignment::Horizontal>,
) -> Self {
self.align_x = alignment.into();
self
}
/// Sets the alignment for the vertical axis of the [`Column`].
pub fn align_y(
mut self,
alignment: impl Into<alignment::Vertical>,
) -> Self {
self.align_y = alignment.into();
self
}
}
/// The appearance of a [`Table`].
#[derive(Debug, Clone, Copy)]
pub struct Style {
/// The background color of the horizontal line separator between cells.
pub separator_x: Background,
/// The background color of the vertical line separator between cells.
pub separator_y: Background,
}
/// The theme catalog of a [`Table`].
pub trait Catalog {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>) -> Style;
}
/// A styling function for a [`Table`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl<Theme> From<Style> for StyleFn<'_, Theme> {
fn from(style: Style) -> Self {
Box::new(move |_theme| style)
}
}
impl Catalog for crate::Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
/// The default style of a [`Table`].
pub fn default(theme: &crate::Theme) -> Style {
let palette = theme.extended_palette();
let separator = palette.background.strong.color.into();
Style {
separator_x: separator,
separator_y: separator,
}
}
+30
View File
@@ -0,0 +1,30 @@
//! Draw and interact with text.
mod rich;
pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span};
pub use crate::core::widget::text::*;
pub use rich::Rich;
/// A bunch of text.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::text;
/// use iced::color;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// text("Hello, this is iced!")
/// .size(20)
/// .color(color!(0x0000ff))
/// .into()
/// }
/// ```
pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> =
crate::core::widget::Text<'a, Theme, Renderer>;
+563
View File
@@ -0,0 +1,563 @@
use crate::core::alignment;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::{Paragraph, Span};
use crate::core::widget::text::{
self, Alignment, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
};
use crate::core::widget::tree::{self, Tree};
use crate::core::{
self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
Rectangle, Shell, Size, Vector, Widget,
};
/// A bunch of [`Rich`] text.
pub struct Rich<
'a,
Link,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Link: Clone + 'static,
Theme: Catalog,
Renderer: core::text::Renderer,
{
spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
size: Option<Pixels>,
line_height: LineHeight,
width: Length,
height: Length,
font: Option<Renderer::Font>,
align_x: Alignment,
align_y: alignment::Vertical,
wrapping: Wrapping,
class: Theme::Class<'a>,
hovered_link: Option<usize>,
on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
}
impl<'a, Link, Message, Theme, Renderer>
Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: Catalog,
Renderer: core::text::Renderer,
Renderer::Font: 'a,
{
/// Creates a new empty [`Rich`] text.
pub fn new() -> Self {
Self {
spans: Box::new([]),
size: None,
line_height: LineHeight::default(),
width: Length::Shrink,
height: Length::Shrink,
font: None,
align_x: Alignment::Default,
align_y: alignment::Vertical::Top,
wrapping: Wrapping::default(),
class: Theme::default(),
hovered_link: None,
on_link_click: None,
}
}
/// Creates a new [`Rich`] text with the given text spans.
pub fn with_spans(
spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
) -> Self {
Self {
spans: Box::new(spans),
..Self::new()
}
}
/// Sets the default size of the [`Rich`] text.
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into());
self
}
/// Sets the default [`LineHeight`] of the [`Rich`] text.
pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
self.line_height = line_height.into();
self
}
/// Sets the default font of the [`Rich`] text.
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the width of the [`Rich`] text boundaries.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Rich`] text boundaries.
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Centers the [`Rich`] text, both horizontally and vertically.
pub fn center(self) -> Self {
self.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
}
/// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
self.align_x = alignment.into();
self
}
/// Sets the [`alignment::Vertical`] of the [`Rich`] text.
pub fn align_y(
mut self,
alignment: impl Into<alignment::Vertical>,
) -> Self {
self.align_y = alignment.into();
self
}
/// Sets the [`Wrapping`] strategy of the [`Rich`] text.
pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
self.wrapping = wrapping;
self
}
/// Sets the message that will be produced when a link of the [`Rich`] text
/// is clicked.
///
/// If the spans of the [`Rich`] text contain no links, you may need to call
/// this method with `on_link_click(never)` in order for the compiler to infer
/// the proper `Link` generic type.
pub fn on_link_click(
mut self,
on_link_click: impl Fn(Link) -> Message + 'a,
) -> Self {
self.on_link_click = Some(Box::new(on_link_click));
self
}
/// Sets the default style of the [`Rich`] text.
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the default [`Color`] of the [`Rich`] text.
pub fn color(self, color: impl Into<Color>) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.color_maybe(Some(color))
}
/// Sets the default [`Color`] of the [`Rich`] text, if `Some`.
pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
let color = color.map(Into::into);
self.style(move |_theme| Style { color })
}
/// Sets the default style class of the [`Rich`] text.
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<'a, Link, Message, Theme, Renderer> Default
for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
Renderer: core::text::Renderer,
Renderer::Font: 'a,
{
fn default() -> Self {
Self::new()
}
}
struct State<Link, P: Paragraph> {
spans: Vec<Span<'static, Link, P::Font>>,
span_pressed: Option<usize>,
paragraph: P,
}
impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Rich<'_, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: Catalog,
Renderer: core::text::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Link, Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<Link, _> {
spans: Vec::new(),
span_pressed: None,
paragraph: Renderer::Paragraph::default(),
})
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
tree.state
.downcast_mut::<State<Link, Renderer::Paragraph>>(),
renderer,
limits,
self.width,
self.height,
self.spans.as_ref().as_ref(),
self.line_height,
self.size,
self.font,
self.align_x,
self.align_y,
self.wrapping,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if !layout.bounds().intersects(viewport) {
return;
}
let state = tree
.state
.downcast_ref::<State<Link, Renderer::Paragraph>>();
let style = theme.style(&self.class);
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
let is_hovered_link = self.on_link_click.is_some()
&& Some(index) == self.hovered_link;
if span.highlight.is_some()
|| span.underline
|| span.strikethrough
|| is_hovered_link
{
let translation = layout.position() - Point::ORIGIN;
let regions = state.paragraph.span_bounds(index);
if let Some(highlight) = span.highlight {
for bounds in &regions {
let bounds = Rectangle::new(
bounds.position()
- Vector::new(
span.padding.left,
span.padding.top,
),
bounds.size()
+ Size::new(span.padding.x(), span.padding.y()),
);
renderer.fill_quad(
renderer::Quad {
bounds: bounds + translation,
border: highlight.border,
..Default::default()
},
highlight.background,
);
}
}
if span.underline || span.strikethrough || is_hovered_link {
let size = span
.size
.or(self.size)
.unwrap_or(renderer.default_size());
let line_height = span
.line_height
.unwrap_or(self.line_height)
.to_absolute(size);
let color = span
.color
.or(style.color)
.unwrap_or(defaults.text_color);
let baseline = translation
+ Vector::new(
0.0,
size.0 + (line_height.0 - size.0) / 2.0,
);
if span.underline || is_hovered_link {
for bounds in &regions {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle::new(
bounds.position() + baseline
- Vector::new(0.0, size.0 * 0.08),
Size::new(bounds.width, 1.0),
),
..Default::default()
},
color,
);
}
}
if span.strikethrough {
for bounds in &regions {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle::new(
bounds.position() + baseline
- Vector::new(0.0, size.0 / 2.0),
Size::new(bounds.width, 1.0),
),
..Default::default()
},
color,
);
}
}
}
}
}
text::draw(
renderer,
defaults,
layout.bounds(),
&state.paragraph,
style,
viewport,
);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let Some(on_link_clicked) = &self.on_link_click else {
return;
};
let was_hovered = self.hovered_link.is_some();
if let Some(position) = cursor.position_in(layout.bounds()) {
let state = tree
.state
.downcast_ref::<State<Link, Renderer::Paragraph>>();
self.hovered_link =
state.paragraph.hit_span(position).and_then(|span| {
if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
Some(span)
} else {
None
}
});
} else {
self.hovered_link = None;
}
if was_hovered != self.hovered_link.is_some() {
shell.request_redraw();
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let state = tree
.state
.downcast_mut::<State<Link, Renderer::Paragraph>>();
if self.hovered_link.is_some() {
state.span_pressed = self.hovered_link;
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree
.state
.downcast_mut::<State<Link, Renderer::Paragraph>>();
match state.span_pressed {
Some(span) if Some(span) == self.hovered_link => {
if let Some(link) = self
.spans
.as_ref()
.as_ref()
.get(span)
.and_then(|span| span.link.clone())
{
shell.publish(on_link_clicked(link));
}
}
_ => {}
}
state.span_pressed = None;
}
_ => {}
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if self.hovered_link.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::None
}
}
}
fn layout<Link, Renderer>(
state: &mut State<Link, Renderer::Paragraph>,
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
height: Length,
spans: &[Span<'_, Link, Renderer::Font>],
line_height: LineHeight,
size: Option<Pixels>,
font: Option<Renderer::Font>,
align_x: Alignment,
align_y: alignment::Vertical,
wrapping: Wrapping,
) -> layout::Node
where
Link: Clone,
Renderer: core::text::Renderer,
{
layout::sized(limits, width, height, |limits| {
let bounds = limits.max();
let size = size.unwrap_or_else(|| renderer.default_size());
let font = font.unwrap_or_else(|| renderer.default_font());
let text_with_spans = || core::Text {
content: spans,
bounds,
size,
line_height,
font,
align_x,
align_y,
shaping: Shaping::Advanced,
wrapping,
};
if state.spans != spans {
state.paragraph =
Renderer::Paragraph::with_spans(text_with_spans());
state.spans = spans.iter().cloned().map(Span::to_static).collect();
} else {
match state.paragraph.compare(core::Text {
content: (),
bounds,
size,
line_height,
font,
align_x,
align_y,
shaping: Shaping::Advanced,
wrapping,
}) {
core::text::Difference::None => {}
core::text::Difference::Bounds => {
state.paragraph.resize(bounds);
}
core::text::Difference::Shape => {
state.paragraph =
Renderer::Paragraph::with_spans(text_with_spans());
}
}
}
state.paragraph.min_bounds()
})
}
impl<'a, Link, Message, Theme, Renderer>
FromIterator<Span<'a, Link, Renderer::Font>>
for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
Renderer: core::text::Renderer,
Renderer::Font: 'a,
{
fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
spans: T,
) -> Self {
Self::with_spans(spans.into_iter().collect::<Vec<_>>())
}
}
impl<'a, Link, Message, Theme, Renderer>
From<Rich<'a, Link, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Link: Clone + 'a,
Theme: Catalog + 'a,
Renderer: core::text::Renderer + 'a,
{
fn from(
text: Rich<'a, Link, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(text)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+189
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, PartialEq, Eq)]
pub struct Cursor {
state: State,
}
/// The state of a [`Cursor`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
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)))
}
State::Index(_) => 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)),
State::Index(_) => 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),
}
}
}
+70
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);
}
}
}
}
}
+136
View File
@@ -0,0 +1,136 @@
use unicode_segmentation::UnicodeSegmentation;
/// The value of a [`TextInput`].
///
/// [`TextInput`]: super::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 }
}
/// 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_n(
String::from("•"),
self.graphemes.len(),
)
.collect(),
}
}
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.graphemes.concat())
}
}
+296
View File
@@ -0,0 +1,296 @@
use crate::container;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::theme;
use crate::core::widget::Operation;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Background, Clipboard, Color, Element, Event, Layout, Length, Rectangle,
Shell, Size, Vector, Widget,
};
/// A widget that applies any `Theme` to its contents.
///
/// This widget can be useful to leverage multiple `Theme`
/// types in an application.
pub struct Themer<'a, Message, Theme, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
theme: Option<Theme>,
text_color: Option<fn(&Theme) -> Color>,
background: Option<fn(&Theme) -> Background>,
}
impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
{
/// Creates an empty [`Themer`] that applies the given `Theme`
/// to the provided `content`.
pub fn new(
theme: Option<Theme>,
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
theme,
text_color: None,
background: None,
}
}
/// Sets the default text [`Color`] of the [`Themer`].
pub fn text_color(mut self, f: fn(&Theme) -> Color) -> Self {
self.text_color = Some(f);
self
}
/// Sets the [`Background`] of the [`Themer`].
pub fn background(mut self, f: fn(&Theme) -> Background) -> Self {
self.background = Some(f);
self
}
}
impl<Message, Theme, Renderer, AnyTheme> Widget<Message, AnyTheme, Renderer>
for Themer<'_, Message, Theme, Renderer>
where
Theme: theme::Base,
AnyTheme: theme::Base,
Renderer: crate::core::Renderer,
{
fn tag(&self) -> tree::Tag {
self.content.as_widget().tag()
}
fn state(&self) -> tree::State {
self.content.as_widget().state()
}
fn children(&self) -> Vec<Tree> {
self.content.as_widget().children()
}
fn diff(&self, tree: &mut Tree) {
self.content.as_widget().diff(tree);
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget_mut().layout(tree, renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(tree, layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content
.as_widget()
.mouse_interaction(tree, layout, cursor, viewport, renderer)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &AnyTheme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let default_theme = theme::Base::default(theme.mode());
let theme = self.theme.as_ref().unwrap_or(&default_theme);
if let Some(background) = self.background {
container::draw_background(
renderer,
&container::Style {
background: Some(background(theme)),
..container::Style::default()
},
layout.bounds(),
);
}
let style = if let Some(text_color) = self.text_color {
renderer::Style {
text_color: text_color(theme),
}
} else {
*style
};
self.content
.as_widget()
.draw(tree, renderer, theme, &style, layout, cursor, viewport);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> {
struct Overlay<'a, Message, Theme, Renderer> {
theme: &'a Option<Theme>,
content: overlay::Element<'a, Message, Theme, Renderer>,
}
impl<Message, Theme, Renderer, AnyTheme>
overlay::Overlay<Message, AnyTheme, Renderer>
for Overlay<'_, Message, Theme, Renderer>
where
Theme: theme::Base,
AnyTheme: theme::Base,
Renderer: crate::core::Renderer,
{
fn layout(
&mut self,
renderer: &Renderer,
bounds: Size,
) -> layout::Node {
self.content.as_overlay_mut().layout(renderer, bounds)
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &AnyTheme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let default_theme = theme::Base::default(theme.mode());
let theme = self.theme.as_ref().unwrap_or(&default_theme);
self.content
.as_overlay()
.draw(renderer, theme, style, layout, cursor);
}
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
self.content
.as_overlay_mut()
.update(event, layout, cursor, renderer, clipboard, shell);
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content
.as_overlay_mut()
.operate(layout, renderer, operation);
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
self.content
.as_overlay()
.mouse_interaction(layout, cursor, renderer)
}
fn overlay<'b>(
&'b mut self,
layout: Layout<'b>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>>
{
self.content
.as_overlay_mut()
.overlay(layout, renderer)
.map(|content| Overlay {
theme: self.theme,
content,
})
.map(|overlay| overlay::Element::new(Box::new(overlay)))
}
}
self.content
.as_widget_mut()
.overlay(tree, layout, renderer, viewport, translation)
.map(|content| Overlay {
theme: &self.theme,
content,
})
.map(|overlay| overlay::Element::new(Box::new(overlay)))
}
}
impl<'a, Message, Theme, Renderer, AnyTheme>
From<Themer<'a, Message, Theme, Renderer>>
for Element<'a, Message, AnyTheme, Renderer>
where
Message: 'a,
Theme: theme::Base + 'a,
AnyTheme: theme::Base,
Renderer: 'a + crate::core::Renderer,
{
fn from(
themer: Themer<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, AnyTheme, Renderer> {
Element::new(themer)
}
}
+613
View File
@@ -0,0 +1,613 @@
//! Togglers let users make binary choices by toggling a switch.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::toggler;
//!
//! struct State {
//! is_checked: bool,
//! }
//!
//! enum Message {
//! TogglerToggled(bool),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! toggler(state.is_checked)
//! .label("Toggle me!")
//! .on_toggle(Message::TogglerToggled)
//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::TogglerToggled(is_checked) => {
//! state.is_checked = is_checked;
//! }
//! }
//! }
//! ```
use crate::core::alignment;
use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
Pixels, Rectangle, Shell, Size, Theme, Widget,
};
/// A toggler widget.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::toggler;
///
/// struct State {
/// is_checked: bool,
/// }
///
/// enum Message {
/// TogglerToggled(bool),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// toggler(state.is_checked)
/// .label("Toggle me!")
/// .on_toggle(Message::TogglerToggled)
/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::TogglerToggled(is_checked) => {
/// state.is_checked = is_checked;
/// }
/// }
/// }
/// ```
pub struct Toggler<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: Catalog,
Renderer: text::Renderer,
{
is_toggled: bool,
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
label: Option<text::Fragment<'a>>,
width: Length,
size: f32,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_alignment: text::Alignment,
text_shaping: text::Shaping,
text_wrapping: text::Wrapping,
spacing: f32,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
last_status: Option<Status>,
}
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
/// The default size of a [`Toggler`].
pub const DEFAULT_SIZE: f32 = 16.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(is_toggled: bool) -> Self {
Toggler {
is_toggled,
on_toggle: None,
label: None,
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
text_size: None,
text_line_height: text::LineHeight::default(),
text_alignment: text::Alignment::Default,
text_shaping: text::Shaping::default(),
text_wrapping: text::Wrapping::default(),
spacing: Self::DEFAULT_SIZE / 2.0,
font: None,
class: Theme::default(),
last_status: None,
}
}
/// Sets the label of the [`Toggler`].
pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
self.label = Some(label.into_fragment());
self
}
/// Sets the message that should be produced when a user toggles
/// the [`Toggler`].
///
/// If this method is not called, the [`Toggler`] will be disabled.
pub fn on_toggle(
mut self,
on_toggle: impl Fn(bool) -> Message + 'a,
) -> Self {
self.on_toggle = Some(Box::new(on_toggle));
self
}
/// Sets the message that should be produced when a user toggles
/// the [`Toggler`], if `Some`.
///
/// If `None`, the [`Toggler`] will be disabled.
pub fn on_toggle_maybe(
mut self,
on_toggle: Option<impl Fn(bool) -> Message + 'a>,
) -> Self {
self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
self
}
/// 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());
self
}
/// Sets the text [`text::LineHeight`] of the [`Toggler`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
) -> Self {
self.text_line_height = line_height.into();
self
}
/// Sets the horizontal alignment of the text of the [`Toggler`]
pub fn text_alignment(
mut self,
alignment: impl Into<text::Alignment>,
) -> Self {
self.text_alignment = alignment.into();
self
}
/// Sets the [`text::Shaping`] strategy of the [`Toggler`].
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
self.text_shaping = shaping;
self
}
/// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
self.text_wrapping = wrapping;
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 [`Renderer::Font`] of the text of the [`Toggler`]
///
/// [`Renderer::Font`]: crate::core::text::Renderer
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
/// Sets the style of the [`Toggler`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Toggler`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Toggler<'_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width);
layout::next_to_each_other(
&limits,
if self.label.is_some() {
self.spacing
} else {
0.0
},
|_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
|limits| {
if let Some(label) = self.label.as_deref() {
let state = tree
.state
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
widget::text::layout(
state,
renderer,
limits,
label,
widget::text::Format {
width: self.width,
height: Length::Shrink,
line_height: self.text_line_height,
size: self.text_size,
font: self.font,
align_x: self.text_alignment,
align_y: alignment::Vertical::Top,
shaping: self.text_shaping,
wrapping: self.text_wrapping,
},
)
} else {
layout::Node::new(Size::ZERO)
}
},
)
}
fn update(
&mut self,
_tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let Some(on_toggle) = &self.on_toggle else {
return;
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = cursor.is_over(layout.bounds());
if mouse_over {
shell.publish(on_toggle(!self.is_toggled));
shell.capture_event();
}
}
_ => {}
}
let current_status = if self.on_toggle.is_none() {
Status::Disabled {
is_toggled: self.is_toggled,
}
} else if cursor.is_over(layout.bounds()) {
Status::Hovered {
is_toggled: self.is_toggled,
}
} else {
Status::Active {
is_toggled: self.is_toggled,
}
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_status = Some(current_status);
} else if self
.last_status
.is_some_and(|status| status != current_status)
{
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
if self.on_toggle.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::NotAllowed
}
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let mut children = layout.children();
let toggler_layout = children.next().unwrap();
let style = theme.style(
&self.class,
self.last_status.unwrap_or(Status::Disabled {
is_toggled: self.is_toggled,
}),
);
if self.label.is_some() {
let label_layout = children.next().unwrap();
let state: &widget::text::State<Renderer::Paragraph> =
tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout.bounds(),
state.raw(),
crate::text::Style {
color: style.text_color,
},
viewport,
);
}
let bounds = toggler_layout.bounds();
let border_radius = style
.border_radius
.unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
radius: border_radius,
width: style.background_border_width,
color: style.background_border_color,
},
..renderer::Quad::default()
},
style.background,
);
let padding = (style.padding_ratio * bounds.height).round();
let toggler_foreground_bounds = Rectangle {
x: bounds.x
+ if self.is_toggled {
bounds.width - bounds.height + padding
} else {
padding
},
y: bounds.y + padding,
width: bounds.height - (2.0 * padding),
height: bounds.height - (2.0 * padding),
};
renderer.fill_quad(
renderer::Quad {
bounds: toggler_foreground_bounds,
border: Border {
radius: border_radius,
width: style.foreground_border_width,
color: style.foreground_border_color,
},
..renderer::Quad::default()
},
style.foreground,
);
}
}
impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
toggler: Toggler<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(toggler)
}
}
/// The possible status of a [`Toggler`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Toggler`] can be interacted with.
Active {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
/// The [`Toggler`] is being hovered.
Hovered {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
/// The [`Toggler`] is disabled.
Disabled {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
}
/// The appearance of a toggler.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The background [`Color`] of the toggler.
pub background: Background,
/// The width of the background border of the toggler.
pub background_border_width: f32,
/// The [`Color`] of the background border of the toggler.
pub background_border_color: Color,
/// The foreground [`Color`] of the toggler.
pub foreground: Background,
/// The width of the foreground border of the toggler.
pub foreground_border_width: f32,
/// The [`Color`] of the foreground border of the toggler.
pub foreground_border_color: Color,
/// The text [`Color`] of the toggler.
pub text_color: Option<Color>,
/// The border radius of the toggler.
///
/// If `None`, the toggler will be perfectly round.
pub border_radius: Option<border::Radius>,
/// The ratio of separation between the background and the toggle in relative height.
pub padding_ratio: f32,
}
/// The theme catalog of a [`Toggler`].
pub trait Catalog: Sized {
/// The item class of the [`Catalog`].
type Class<'a>;
/// The default class produced by the [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
/// A styling function for a [`Toggler`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
/// The default style of a [`Toggler`].
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let background = match status {
Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
if is_toggled {
palette.primary.base.color
} else {
palette.background.strong.color
}
}
Status::Disabled { is_toggled } => {
if is_toggled {
palette.background.strong.color
} else {
palette.background.weak.color
}
}
};
let foreground = match status {
Status::Active { is_toggled } => {
if is_toggled {
palette.primary.base.text
} else {
palette.background.base.color
}
}
Status::Hovered { is_toggled } => {
if is_toggled {
Color {
a: 0.5,
..palette.primary.base.text
}
} else {
palette.background.weak.color
}
}
Status::Disabled { .. } => palette.background.weakest.color,
};
Style {
background: background.into(),
foreground: foreground.into(),
foreground_border_width: 0.0,
foreground_border_color: Color::TRANSPARENT,
background_border_width: 0.0,
background_border_color: Color::TRANSPARENT,
text_color: None,
border_radius: None,
padding_ratio: 0.1,
}
}
+584
View File
@@ -0,0 +1,584 @@
//! Tooltips display a hint of information over some element when hovered.
//!
//! By default, the tooltip is displayed immediately, however, this can be adjusted
//! with [`Tooltip::delay`].
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::{container, tooltip};
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(_state: &State) -> Element<'_, Message> {
//! tooltip(
//! "Hover me to display the tooltip!",
//! container("This is the tooltip contents!")
//! .padding(10)
//! .style(container::rounded_box),
//! tooltip::Position::Bottom,
//! ).into()
//! }
//! ```
use crate::container;
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::time::{Duration, Instant};
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{
Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
Shell, Size, Vector,
};
/// An element to display a widget over another.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::{container, tooltip};
///
/// enum Message {
/// // ...
/// }
///
/// fn view(_state: &State) -> Element<'_, Message> {
/// tooltip(
/// "Hover me to display the tooltip!",
/// container("This is the tooltip contents!")
/// .padding(10)
/// .style(container::rounded_box),
/// tooltip::Position::Bottom,
/// ).into()
/// }
/// ```
pub struct Tooltip<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Theme: container::Catalog,
Renderer: text::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
tooltip: Element<'a, Message, Theme, Renderer>,
position: Position,
gap: f32,
padding: f32,
snap_within_viewport: bool,
delay: Duration,
class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: text::Renderer,
{
/// 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, Theme, Renderer>>,
tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
position: Position,
) -> Self {
Tooltip {
content: content.into(),
tooltip: tooltip.into(),
position,
gap: 0.0,
padding: Self::DEFAULT_PADDING,
snap_within_viewport: true,
delay: Duration::ZERO,
class: Theme::default(),
}
}
/// 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 the delay before the [`Tooltip`] is shown.
///
/// Set to [`Duration::ZERO`] to be shown immediately.
pub fn delay(mut self, delay: Duration) -> Self {
self.delay = delay;
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`].
#[must_use]
pub fn style(
mut self,
style: impl Fn(&Theme) -> container::Style + 'a,
) -> Self
where
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`Tooltip`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Tooltip<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: text::Renderer,
{
fn children(&self) -> Vec<widget::Tree> {
vec![
widget::Tree::new(&self.content),
widget::Tree::new(&self.tooltip),
]
}
fn diff(&self, tree: &mut widget::Tree) {
tree.diff_children(&[
self.content.as_widget(),
self.tooltip.as_widget(),
]);
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State::default())
}
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<State>()
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.content.as_widget().size_hint()
}
fn layout(
&mut self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content.as_widget_mut().layout(
&mut tree.children[0],
renderer,
limits,
)
}
fn update(
&mut self,
tree: &mut widget::Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if let Event::Mouse(_)
| Event::Window(window::Event::RedrawRequested(_)) = event
{
let state = tree.state.downcast_mut::<State>();
let now = Instant::now();
let cursor_position = cursor.position_over(layout.bounds());
match (*state, cursor_position) {
(State::Idle, Some(cursor_position)) => {
if self.delay == Duration::ZERO {
*state = State::Open { cursor_position };
shell.invalidate_layout();
} else {
*state = State::Hovered { at: now };
}
shell.request_redraw_at(now + self.delay);
}
(State::Hovered { .. }, None) => {
*state = State::Idle;
}
(State::Hovered { at, .. }, _) if at.elapsed() < self.delay => {
shell.request_redraw_at(now + self.delay - at.elapsed());
}
(State::Hovered { .. }, Some(cursor_position)) => {
*state = State::Open { cursor_position };
shell.invalidate_layout();
}
(
State::Open {
cursor_position: last_position,
},
Some(cursor_position),
) if self.position == Position::FollowCursor
&& last_position != cursor_position =>
{
*state = State::Open { cursor_position };
shell.request_redraw();
}
(State::Open { .. }, None) => {
*state = State::Idle;
shell.invalidate_layout();
if !matches!(
event,
Event::Window(window::Event::RedrawRequested(_)),
) {
shell.request_redraw();
}
}
(State::Open { .. }, Some(_)) | (State::Idle, None) => (),
}
}
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
inherited_style,
layout,
cursor,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut widget::Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let state = tree.state.downcast_ref::<State>();
let mut children = tree.children.iter_mut();
let content = self.content.as_widget_mut().overlay(
children.next().unwrap(),
layout,
renderer,
viewport,
translation,
);
let tooltip = if let State::Open { cursor_position } = *state {
Some(overlay::Element::new(Box::new(Overlay {
position: layout.position() + translation,
tooltip: &mut self.tooltip,
tree: children.next().unwrap(),
cursor_position,
content_bounds: layout.bounds(),
snap_within_viewport: self.snap_within_viewport,
positioning: self.position,
gap: self.gap,
padding: self.padding,
class: &self.class,
})))
} else {
None
};
if content.is_some() || tooltip.is_some() {
Some(
overlay::Group::with_children(
content.into_iter().chain(tooltip).collect(),
)
.overlay(),
)
} else {
None
}
}
fn operate(
&mut self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
});
}
}
impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Theme: container::Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
tooltip: Tooltip<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(tooltip)
}
}
/// The position of the tooltip. Defaults to following the cursor.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
/// The tooltip will appear on the top of the widget.
#[default]
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,
/// The tooltip will follow the cursor.
FollowCursor,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
enum State {
#[default]
Idle,
Hovered {
at: Instant,
},
Open {
cursor_position: Point,
},
}
struct Overlay<'a, 'b, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: text::Renderer,
{
position: Point,
tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
tree: &'b mut widget::Tree,
cursor_position: Point,
content_bounds: Rectangle,
snap_within_viewport: bool,
positioning: Position,
gap: f32,
padding: f32,
class: &'b Theme::Class<'a>,
}
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'_, '_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: text::Renderer,
{
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
let viewport = Rectangle::with_size(bounds);
let tooltip_layout = self.tooltip.as_widget_mut().layout(
self.tree,
renderer,
&layout::Limits::new(
Size::ZERO,
if self.snap_within_viewport {
viewport.size()
} else {
Size::INFINITE
},
)
.shrink(Padding::new(self.padding)),
);
let text_bounds = tooltip_layout.bounds();
let x_center = self.position.x
+ (self.content_bounds.width - text_bounds.width) / 2.0;
let y_center = self.position.y
+ (self.content_bounds.height - text_bounds.height) / 2.0;
let mut tooltip_bounds = {
let offset = match self.positioning {
Position::Top => Vector::new(
x_center,
self.position.y
- text_bounds.height
- self.gap
- self.padding,
),
Position::Bottom => Vector::new(
x_center,
self.position.y
+ self.content_bounds.height
+ self.gap
+ self.padding,
),
Position::Left => Vector::new(
self.position.x
- text_bounds.width
- self.gap
- self.padding,
y_center,
),
Position::Right => Vector::new(
self.position.x
+ self.content_bounds.width
+ self.gap
+ self.padding,
y_center,
),
Position::FollowCursor => {
let translation =
self.position - self.content_bounds.position();
Vector::new(
self.cursor_position.x,
self.cursor_position.y - text_bounds.height,
) + translation
}
};
Rectangle {
x: offset.x - self.padding,
y: offset.y - self.padding,
width: text_bounds.width + self.padding * 2.0,
height: text_bounds.height + self.padding * 2.0,
}
};
if self.snap_within_viewport {
if tooltip_bounds.x < viewport.x {
tooltip_bounds.x = viewport.x;
} else if viewport.x + viewport.width
< tooltip_bounds.x + tooltip_bounds.width
{
tooltip_bounds.x =
viewport.x + viewport.width - tooltip_bounds.width;
}
if tooltip_bounds.y < viewport.y {
tooltip_bounds.y = viewport.y;
} else if viewport.y + viewport.height
< tooltip_bounds.y + tooltip_bounds.height
{
tooltip_bounds.y =
viewport.y + viewport.height - tooltip_bounds.height;
}
}
layout::Node::with_children(
tooltip_bounds.size(),
vec![
tooltip_layout
.translate(Vector::new(self.padding, self.padding)),
],
)
.translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
) {
let style = theme.style(self.class);
container::draw_background(renderer, &style, layout.bounds());
let defaults = renderer::Style {
text_color: style.text_color.unwrap_or(inherited_style.text_color),
};
self.tooltip.as_widget().draw(
self.tree,
renderer,
theme,
&defaults,
layout.children().next().unwrap(),
cursor_position,
&Rectangle::with_size(Size::INFINITE),
);
}
}
+567
View File
@@ -0,0 +1,567 @@
//! Sliders let users set a value by moving an indicator.
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! #
//! use iced::widget::slider;
//!
//! struct State {
//! value: f32,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Message {
//! ValueChanged(f32),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! slider(0.0..=100.0, state.value, Message::ValueChanged).into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::ValueChanged(value) => {
//! state.value = value;
//! }
//! }
//! }
//! ```
use std::ops::RangeInclusive;
pub use crate::slider::{
Catalog, Handle, HandleShape, Status, Style, StyleFn, default,
};
use crate::core::border::Border;
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
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::window;
use crate::core::{
self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell,
Size, Widget,
};
/// An vertical bar and a handle that selects a single value from a range of
/// values.
///
/// A [`VerticalSlider`] will try to fill the vertical space of its container.
///
/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults
/// to 1 unit.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// #
/// use iced::widget::vertical_slider;
///
/// struct State {
/// value: f32,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// ValueChanged(f32),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into()
/// }
///
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::ValueChanged(value) => {
/// state.value = value;
/// }
/// }
/// }
/// ```
pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
where
Theme: Catalog,
{
range: RangeInclusive<T>,
step: T,
shift_step: Option<T>,
value: T,
default: Option<T>,
on_change: Box<dyn Fn(T) -> Message + 'a>,
on_release: Option<Message>,
width: f32,
height: Length,
class: Theme::Class<'a>,
status: Option<Status>,
}
impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
Theme: Catalog,
{
/// The default width of a [`VerticalSlider`].
pub const DEFAULT_WIDTH: f32 = 16.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,
default: None,
range,
step: T::from(1),
shift_step: None,
on_change: Box::new(on_change),
on_release: None,
width: Self::DEFAULT_WIDTH,
height: Length::Fill,
class: Theme::default(),
status: None,
}
}
/// Sets the optional default value for the [`VerticalSlider`].
///
/// If set, the [`VerticalSlider`] will reset to this value when ctrl-clicked or command-clicked.
pub fn default(mut self, default: impl Into<T>) -> Self {
self.default = Some(default.into());
self
}
/// 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 step size of the [`VerticalSlider`].
pub fn step(mut self, step: T) -> Self {
self.step = step;
self
}
/// Sets the optional "shift" step for the [`VerticalSlider`].
///
/// If set, this value is used as the step while the shift key is pressed.
pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
self.shift_step = Some(shift_step.into());
self
}
/// Sets the style of the [`VerticalSlider`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the style class of the [`VerticalSlider`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VerticalSlider<'_, T, Message, Theme>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
Theme: Catalog,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
Size {
width: Length::Shrink,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(limits, self.width, self.height)
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
let is_dragging = state.is_dragging;
let current_value = self.value;
let locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
if cursor_position.y >= bounds.y + bounds.height {
Some(*self.range.start())
} else if cursor_position.y <= bounds.y {
Some(*self.range.end())
} else {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let start = (*self.range.start()).into();
let end = (*self.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;
T::from_f64(value.min(end))
}
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
if new_value > (*self.range.end()).into() {
return Some(*self.range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
if new_value < (*self.range.start()).into() {
return Some(*self.range.start());
}
T::from_f64(new_value)
};
let change = |new_value: T| {
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((self.on_change)(new_value));
self.value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) =
cursor.position_over(layout.bounds())
{
if state.keyboard_modifiers.control()
|| state.keyboard_modifiers.command()
{
let _ = self.default.map(change);
state.is_dragging = false;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
}
shell.capture_event();
}
}
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) = self.on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ =
cursor.land().position().and_then(locate).map(change);
shell.capture_event();
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta })
if state.keyboard_modifiers.control() =>
{
if cursor.is_over(layout.bounds()) {
let delta = match *delta {
mouse::ScrollDelta::Lines { x: _, y } => y,
mouse::ScrollDelta::Pixels { x: _, y } => y,
};
if delta < 0.0 {
let _ = decrement(current_value).map(change);
} else {
let _ = increment(current_value).map(change);
}
shell.capture_event();
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if cursor.is_over(layout.bounds()) {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
shell.capture_event();
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
shell.capture_event();
}
_ => (),
}
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = *modifiers;
}
_ => {}
}
let current_status = if state.is_dragging {
Status::Dragged
} else if cursor.is_over(layout.bounds()) {
Status::Hovered
} else {
Status::Active
};
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.status = Some(current_status);
} else if self.status.is_some_and(|status| status != current_status) {
shell.request_redraw();
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Active));
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
(radius * 2.0, radius * 2.0, radius.into())
}
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.width, border_radius),
};
let value = self.value.into() as f32;
let (range_start, range_end) = {
let (start, end) = self.range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let offset = if range_start >= range_end {
0.0
} else {
(bounds.height - handle_width) * (value - range_end)
/ (range_start - range_end)
};
let rail_x = bounds.x + bounds.width / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - style.rail.width / 2.0,
y: bounds.y,
width: style.rail.width,
height: offset + handle_width / 2.0,
},
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.backgrounds.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - style.rail.width / 2.0,
y: bounds.y + offset + handle_width / 2.0,
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.backgrounds.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - handle_height / 2.0,
y: bounds.y + offset,
width: handle_height,
height: handle_width,
},
border: Border {
radius: handle_border_radius,
width: style.handle.border_width,
color: style.handle.border_color,
},
..renderer::Quad::default()
},
style.handle.background,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
if state.is_dragging {
// FIXME: Fall back to `Pointer` on Windows
// See https://github.com/rust-windowing/winit/issues/1043
if cfg!(target_os = "windows") {
mouse::Interaction::Pointer
} else {
mouse::Interaction::Grabbing
}
} else if cursor.is_over(layout.bounds()) {
if cfg!(target_os = "windows") {
mouse::Interaction::Pointer
} else {
mouse::Interaction::Grab
}
} else {
mouse::Interaction::default()
}
}
}
impl<'a, T, Message, Theme, Renderer>
From<VerticalSlider<'a, T, Message, Theme>>
for Element<'a, Message, Theme, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(
slider: VerticalSlider<'a, T, Message, Theme>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(slider)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct State {
is_dragging: bool,
keyboard_modifiers: keyboard::Modifiers,
}