Integrate diagnostic decorations and current-source Text input boundaries
This commit is contained in:
+85
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! 
|
||||
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;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// 
|
||||
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
@@ -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)
|
||||
}
|
||||
}
|
||||
+1013
File diff suppressed because it is too large
Load Diff
+660
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
+2107
File diff suppressed because it is too large
Load Diff
+418
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
Vendored
+153
@@ -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;
|
||||
+1722
File diff suppressed because it is too large
Load Diff
+437
@@ -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
@@ -0,0 +1,2 @@
|
||||
//! Display interactive elements on top of other widgets.
|
||||
pub mod menu;
|
||||
+661
@@ -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(),
|
||||
}
|
||||
}
|
||||
+1343
File diff suppressed because it is too large
Load Diff
+259
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/// A four cardinal direction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
/// ↑
|
||||
Up,
|
||||
/// ↓
|
||||
Down,
|
||||
/// ←
|
||||
Left,
|
||||
/// →
|
||||
Right,
|
||||
}
|
||||
@@ -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
@@ -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, ®ion_a, count_a, regions,
|
||||
);
|
||||
b.compute_regions(
|
||||
spacing, min_size, ®ion_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, ®ion_a, count_a, splits);
|
||||
b.compute_splits(spacing, min_size, ®ion_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+271
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+605
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
+2437
File diff suppressed because it is too large
Load Diff
+447
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+340
@@ -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
@@ -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
@@ -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
@@ -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 ®ions {
|
||||
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 ®ions {
|
||||
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 ®ions {
|
||||
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)
|
||||
}
|
||||
}
|
||||
+1534
File diff suppressed because it is too large
Load Diff
+1815
File diff suppressed because it is too large
Load Diff
+189
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user