Merge branch 'master' into beacon

This commit is contained in:
Héctor Ramón Jiménez 2025-03-04 19:11:37 +01:00
commit 8bd5de72ea
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
371 changed files with 33138 additions and 12950 deletions

View file

@ -22,8 +22,10 @@ lazy = ["ouroboros"]
image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
qr_code = ["canvas", "qrcode"]
qr_code = ["canvas", "dep:qrcode"]
wgpu = ["iced_renderer/wgpu"]
markdown = ["dep:pulldown-cmark", "dep:url"]
highlighter = ["dep:iced_highlighter"]
advanced = []
[dependencies]
@ -31,6 +33,7 @@ iced_renderer.workspace = true
iced_runtime.workspace = true
num-traits.workspace = true
log.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
@ -40,3 +43,12 @@ ouroboros.optional = true
qrcode.workspace = true
qrcode.optional = true
pulldown-cmark.workspace = true
pulldown-cmark.optional = true
iced_highlighter.workspace = true
iced_highlighter.optional = true
url.workspace = true
url.optional = true

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

85
widget/src/action.rs Normal file
View file

@ -0,0 +1,85 @@
use crate::core::event;
use crate::core::time::Instant;
use crate::core::window;
/// A runtime action that can be performed by some widgets.
#[derive(Debug, Clone)]
pub struct Action<Message> {
message_to_publish: Option<Message>,
redraw_request: window::RedrawRequest,
event_status: event::Status,
}
impl<Message> Action<Message> {
fn new() -> Self {
Self {
message_to_publish: None,
redraw_request: window::RedrawRequest::Wait,
event_status: event::Status::Ignored,
}
}
/// Creates a new "capturing" [`Action`]. A capturing [`Action`]
/// will make other widgets consider it final and prevent further
/// processing.
///
/// Prevents "event bubbling".
pub fn capture() -> Self {
Self {
event_status: event::Status::Captured,
..Self::new()
}
}
/// Creates a new [`Action`] that publishes the given `Message` for
/// the application to handle.
///
/// Publishing a `Message` always produces a redraw.
pub fn publish(message: Message) -> Self {
Self {
message_to_publish: Some(message),
..Self::new()
}
}
/// Creates a new [`Action`] that requests a redraw to happen as
/// soon as possible; without publishing any `Message`.
pub fn request_redraw() -> Self {
Self {
redraw_request: window::RedrawRequest::NextFrame,
..Self::new()
}
}
/// Creates a new [`Action`] that requests a redraw to happen at
/// the given [`Instant`]; without publishing any `Message`.
///
/// This can be useful to efficiently animate content, like a
/// blinking caret on a text input.
pub fn request_redraw_at(at: Instant) -> Self {
Self {
redraw_request: window::RedrawRequest::At(at),
..Self::new()
}
}
/// Marks the [`Action`] as "capturing". See [`Self::capture`].
pub fn and_capture(mut self) -> Self {
self.event_status = event::Status::Captured;
self
}
/// Converts the [`Action`] into its internal parts.
///
/// This method is meant to be used by runtimes, libraries, or internal
/// widget implementations.
pub fn into_inner(
self,
) -> (Option<Message>, window::RedrawRequest, event::Status) {
(
self.message_to_publish,
self.redraw_request,
self.event_status,
)
}
}

View file

@ -1,48 +1,71 @@
//! Allow your users to perform actions by pressing a button.
use crate::core::event::{self, Event};
//! 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::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
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
/// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
/// #
/// # 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,
/// }
///
/// let button = Button::new("Press me!").on_press(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:
///
/// ```
/// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
/// #
/// ```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 disabled_button<'a>() -> Button<'a, Message> {
/// Button::new("I'm disabled!")
/// }
///
/// fn enabled_button<'a>() -> Button<'a, Message> {
/// disabled_button().on_press(Message::ButtonPressed)
/// fn view(state: &State) -> Element<'_, Message> {
/// button("I am disabled!").into()
/// }
/// ```
#[allow(missing_debug_implementations)]
@ -52,12 +75,27 @@ where
Theme: Catalog,
{
content: Element<'a, Message, Theme, Renderer>,
on_press: Option<Message>,
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>
@ -80,6 +118,7 @@ where
padding: DEFAULT_PADDING,
clip: false,
class: Theme::default(),
status: None,
}
}
@ -105,7 +144,23 @@ where
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(on_press);
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
}
@ -114,7 +169,7 @@ where
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press;
self.on_press = on_press.map(OnPress::Direct);
self
}
@ -205,7 +260,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
@ -217,28 +272,30 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
if let event::Status::Captured = self.content.as_widget_mut().on_event(
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event.clone(),
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
viewport,
) {
return event::Status::Captured;
);
if shell.is_event_captured() {
return;
}
match event {
@ -252,13 +309,13 @@ where
state.is_pressed = true;
return event::Status::Captured;
shell.capture_event();
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = self.on_press.clone() {
if let Some(on_press) = &self.on_press {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
@ -267,10 +324,10 @@ where
let bounds = layout.bounds();
if cursor.is_over(bounds) {
shell.publish(on_press);
shell.publish(on_press.get());
}
return event::Status::Captured;
shell.capture_event();
}
}
}
@ -282,7 +339,25 @@ where
_ => {}
}
event::Status::Ignored
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(
@ -297,23 +372,8 @@ where
) {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let is_mouse_over = cursor.is_over(bounds);
let status = if self.on_press.is_none() {
Status::Disabled
} else if is_mouse_over {
let state = tree.state.downcast_ref::<State>();
if state.is_pressed {
Status::Pressed
} else {
Status::Hovered
}
} else {
Status::Active
};
let style = theme.style(&self.class, status);
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
if style.background.is_some()
|| style.border.width > 0.0
@ -417,15 +477,18 @@ pub enum Status {
}
/// 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 buton.
/// The [`Border`] of the button.
pub border: Border,
/// The [`Shadow`] of the butoon.
/// The [`Shadow`] of the button.
pub shadow: Shadow,
}
@ -451,6 +514,54 @@ impl Default for Style {
}
/// 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>;
@ -480,12 +591,12 @@ impl Catalog for Theme {
/// 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.strong);
let base = styled(palette.primary.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(Background::Color(palette.primary.base.color)),
background: Some(Background::Color(palette.primary.strong.color)),
..base
},
Status::Disabled => disabled(base),
@ -522,6 +633,21 @@ pub fn success(theme: &Theme, status: Status) -> Style {
}
}
/// 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();
@ -560,7 +686,7 @@ fn styled(pair: palette::Pair) -> Style {
Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border::rounded(2),
border: border::rounded(2),
..Style::default()
}
}

View file

@ -1,22 +1,71 @@
//! Draw 2D graphics for your users.
pub mod event;
//! 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 event::Event;
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, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin,
Path, Stroke, Style, Text,
Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style,
Text, fill, gradient, path, stroke,
};
use crate::core;
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,
};
@ -39,15 +88,16 @@ pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
/// A widget capable of drawing 2D graphics.
///
/// ## Drawing a simple circle
/// If you want to get a quick overview, here's how we can draw a simple circle:
///
/// # Example: Drawing a Simple Circle
/// ```no_run
/// # use iced_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program};
/// # use iced_widget::core::{Color, Rectangle};
/// # use iced_widget::core::mouse;
/// # use iced_widget::{Renderer, Theme};
/// # 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 {
@ -55,26 +105,36 @@ pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
/// }
///
/// // Then, we implement the `Program` trait
/// impl Program<()> for Circle {
/// 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<Geometry> {
/// fn draw(
/// &self,
/// _state: &(),
/// renderer: &Renderer,
/// _theme: &Theme,
/// bounds: Rectangle,
/// _cursor: mouse::Cursor
/// ) -> Vec<canvas::Geometry> {
/// // We prepare a new `Frame`
/// let mut frame = Frame::new(renderer, bounds.size());
/// let mut frame = canvas::Frame::new(renderer, bounds.size());
///
/// // We create a `Path` representing a simple circle
/// let circle = Path::circle(frame.center(), self.radius);
/// let circle = canvas::Path::circle(frame.center(), self.radius);
///
/// // And fill it with some color
/// frame.fill(&circle, Color::BLACK);
///
/// // Finally, we produce the geometry
/// // Then, we produce the geometry
/// vec![frame.into_geometry()]
/// }
/// }
///
/// // Finally, we simply use our `Circle` to create the `Canvas`!
/// let canvas = Canvas::new(Circle { radius: 50.0 });
/// 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>
@ -88,6 +148,7 @@ where
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>
@ -106,6 +167,7 @@ where
message_: PhantomData,
theme_: PhantomData,
renderer_: PhantomData,
last_mouse_interaction: None,
}
}
@ -153,42 +215,54 @@ where
layout::atomic(limits, self.width, self.height)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: core::Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
viewport: &Rectangle,
) {
let bounds = layout.bounds();
let canvas_event = match event {
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
core::Event::Keyboard(keyboard_event) => {
Some(Event::Keyboard(keyboard_event))
}
_ => None,
};
let state = tree.state.downcast_mut::<P::State>();
let is_redraw_request = matches!(
event,
Event::Window(window::Event::RedrawRequested(_now)),
);
if let Some(canvas_event) = canvas_event {
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();
let (event_status, message) =
self.program.update(state, canvas_event, bounds, cursor);
shell.request_redraw_at(redraw_request);
if let Some(message) = message {
shell.publish(message);
}
return event_status;
if event_status == event::Status::Captured {
shell.capture_event();
}
}
event::Status::Ignored
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(

View file

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

View file

@ -1,6 +1,6 @@
use crate::canvas::event::{self, Event};
use crate::Action;
use crate::canvas::mouse;
use crate::canvas::Geometry;
use crate::canvas::{Event, Geometry};
use crate::core::Rectangle;
use crate::graphics::geometry;
@ -22,8 +22,9 @@ where
/// When a [`Program`] is used in a [`Canvas`], the runtime will call this
/// method for each [`Event`].
///
/// This method can optionally return a `Message` to notify an application
/// of any meaningful interactions.
/// 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.
///
@ -31,11 +32,11 @@ where
fn update(
&self,
_state: &mut Self::State,
_event: Event,
_event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
) -> Option<Action<Message>> {
None
}
/// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
@ -81,10 +82,10 @@ where
fn update(
&self,
state: &mut Self::State,
event: Event,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
) -> Option<Action<Message>> {
T::update(self, state, event, bounds, cursor)
}

View file

@ -1,6 +1,36 @@
//! Show toggle controls using checkboxes.
//! 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("Toggle me!", state.is_checked)
//! .on_toggle(Message::CheckboxToggled)
//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
//! match message {
//! Message::CheckboxToggled(is_checked) => {
//! state.is_checked = is_checked;
//! }
//! }
//! }
//! ```
//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@ -9,27 +39,43 @@ 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, Layout, Length, Pixels,
Rectangle, Shell, Size, Theme, Widget,
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
Pixels, Rectangle, Shell, Size, Theme, Widget,
};
/// A box that can be checked.
///
/// # Example
///
/// ```no_run
/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>;
/// # 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>;
/// #
/// pub enum Message {
/// use iced::widget::checkbox;
///
/// struct State {
/// is_checked: bool,
/// }
///
/// enum Message {
/// CheckboxToggled(bool),
/// }
///
/// let is_checked = true;
/// fn view(state: &State) -> Element<'_, Message> {
/// checkbox("Toggle me!", state.is_checked)
/// .on_toggle(Message::CheckboxToggled)
/// .into()
/// }
///
/// Checkbox::new("Toggle me!", is_checked).on_toggle(Message::CheckboxToggled);
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::CheckboxToggled(is_checked) => {
/// state.is_checked = is_checked;
/// }
/// }
/// }
/// ```
///
/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
#[allow(missing_debug_implementations)]
pub struct Checkbox<
@ -50,9 +96,11 @@ pub struct Checkbox<
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>
@ -81,7 +129,8 @@ where
spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic,
text_shaping: text::Shaping::default(),
text_wrapping: text::Wrapping::default(),
font: None,
icon: Icon {
font: Renderer::ICON_FONT,
@ -91,6 +140,7 @@ where
shaping: text::Shaping::Basic,
},
class: Theme::default(),
last_status: None,
}
}
@ -158,6 +208,12 @@ where
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
@ -191,8 +247,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Checkbox<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Checkbox<'_, Message, Theme, Renderer>
where
Renderer: text::Renderer,
Theme: Catalog,
@ -240,22 +296,23 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
self.text_wrapping,
)
},
)
}
fn on_event(
fn update(
&mut self,
_tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -264,14 +321,35 @@ where
if mouse_over {
if let Some(on_toggle) = &self.on_toggle {
shell.publish((on_toggle)(!self.is_checked));
return event::Status::Captured;
shell.capture_event();
}
}
}
_ => {}
}
event::Status::Ignored
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(
@ -296,24 +374,17 @@ where
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let is_mouse_over = cursor.is_over(layout.bounds());
let is_disabled = self.on_toggle.is_none();
let is_checked = self.is_checked;
let mut children = layout.children();
let status = if is_disabled {
Status::Disabled { is_checked }
} else if is_mouse_over {
Status::Hovered { is_checked }
} else {
Status::Active { is_checked }
};
let style = theme.style(&self.class, status);
let style = theme.style(
&self.class,
self.last_status.unwrap_or(Status::Disabled {
is_checked: self.is_checked,
}),
);
{
let layout = children.next().unwrap();
@ -348,6 +419,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: *shaping,
wrapping: text::Wrapping::default(),
},
bounds.center(),
style.icon_color,
@ -358,12 +430,14 @@ where
{
let label_layout = children.next().unwrap();
let state: &widget::text::State<Renderer::Paragraph> =
tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout,
tree.state.downcast_ref(),
state.0.raw(),
crate::text::Style {
color: style.text_color,
},
@ -371,6 +445,16 @@ where
);
}
}
fn operate(
&self,
_state: &mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
operation.text(None, layout.bounds(), &self.label);
}
}
impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
@ -423,13 +507,13 @@ pub enum Status {
}
/// The style of a checkbox.
#[derive(Debug, Clone, Copy)]
#[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 hte checkbox.
/// The [`Border`] of the checkbox.
pub border: Border,
/// The text [`Color`] of the checkbox.
pub text_color: Option<Color>,
@ -471,18 +555,21 @@ pub fn primary(theme: &Theme, status: Status) -> Style {
match status {
Status::Active { is_checked } => styled(
palette.primary.strong.text,
palette.background.strongest.color,
palette.background.base,
palette.primary.strong,
palette.primary.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.primary.strong.text,
palette.background.strongest.color,
palette.background.weak,
palette.primary.base,
palette.primary.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.primary.strong.text,
palette.background.weak.color,
palette.background.weak,
palette.background.strong,
is_checked,
@ -497,18 +584,21 @@ pub fn secondary(theme: &Theme, status: Status) -> Style {
match status {
Status::Active { is_checked } => styled(
palette.background.base.text,
palette.background.strongest.color,
palette.background.base,
palette.background.strong,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.background.base.text,
palette.background.strongest.color,
palette.background.weak,
palette.background.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.background.strong.color,
palette.background.weak.color,
palette.background.weak,
palette.background.weak,
is_checked,
@ -523,18 +613,21 @@ pub fn success(theme: &Theme, status: Status) -> Style {
match status {
Status::Active { is_checked } => styled(
palette.success.base.text,
palette.background.weak.color,
palette.background.base,
palette.success.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.success.base.text,
palette.background.strongest.color,
palette.background.weak,
palette.success.base,
palette.success.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.success.base.text,
palette.background.weak.color,
palette.background.weak,
palette.success.weak,
is_checked,
@ -542,25 +635,28 @@ pub fn success(theme: &Theme, status: Status) -> Style {
}
}
/// A danger checkbox; denoting a negaive toggle.
/// 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.danger.base.text,
palette.background.strongest.color,
palette.background.base,
palette.danger.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.danger.base.text,
palette.background.strongest.color,
palette.background.weak,
palette.danger.base,
palette.danger.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.danger.base.text,
palette.background.weak.color,
palette.background.weak,
palette.danger.weak,
is_checked,
@ -570,6 +666,7 @@ pub fn danger(theme: &Theme, status: Status) -> Style {
fn styled(
icon_color: Color,
border_color: Color,
base: palette::Pair,
accent: palette::Pair,
is_checked: bool,
@ -584,7 +681,11 @@ fn styled(
border: Border {
radius: 2.0.into(),
width: 1.0,
color: accent.color,
color: if is_checked {
accent.color
} else {
border_color
},
},
text_color: None,
}

View file

@ -1,16 +1,37 @@
//! Distribute content vertically.
use crate::core::event::{self, Event};
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::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
{
@ -19,7 +40,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
width: Length,
height: Length,
max_width: f32,
align_items: Alignment,
align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
@ -63,7 +84,7 @@ where
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
align: Alignment::Start,
clip: false,
children,
}
@ -104,8 +125,8 @@ where
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
pub fn align_items(mut self, align: Alignment) -> Self {
self.align_items = align;
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
self.align = Alignment::from(align.into());
self
}
@ -152,7 +173,7 @@ where
}
}
impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer>
impl<Message, Renderer> Default for Column<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
@ -161,8 +182,21 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
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,
{
@ -197,7 +231,7 @@ where
self.height,
self.padding,
self.spacing,
self.align_items,
self.align,
&self.children,
&mut tree.children,
)
@ -208,7 +242,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@ -223,34 +257,28 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.children
) {
for ((child, state), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
{
child.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(
@ -285,24 +313,21 @@ where
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(viewport))
{
child.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor,
if self.clip {
&clipped_viewport
} else {
viewport
},
state, renderer, theme, style, layout, cursor, viewport,
);
}
}

View file

@ -1,5 +1,59 @@
//! Display a dropdown list of searchable and selectable options.
use crate::core::event::{self, Event};
//! Combo boxes display a dropdown list of searchable and 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::combo_box;
//!
//! struct State {
//! fruits: combo_box::State<Fruit>,
//! favorite: Option<Fruit>,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Fruit {
//! Apple,
//! Orange,
//! Strawberry,
//! Tomato,
//! }
//!
//! #[derive(Debug, Clone)]
//! enum Message {
//! FruitSelected(Fruit),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! combo_box(
//! &state.fruits,
//! "Select your favorite fruit...",
//! state.favorite.as_ref(),
//! Message::FruitSelected
//! )
//! .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::keyboard;
use crate::core::keyboard::key;
use crate::core::layout::{self, Layout};
@ -10,7 +64,8 @@ use crate::core::text;
use crate::core::time::Instant;
use crate::core::widget::{self, Widget};
use crate::core::{
Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector,
Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme,
Vector,
};
use crate::overlay::menu;
use crate::text::LineHeight;
@ -21,9 +76,60 @@ use std::fmt::Display;
/// A widget for searching and selecting a single value from a list of options.
///
/// This widget is composed by a [`TextInput`] that can be filled with the text
/// to search for corresponding values from the list of options that are displayed
/// as a Menu.
/// # 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::combo_box;
///
/// struct State {
/// fruits: combo_box::State<Fruit>,
/// favorite: Option<Fruit>,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Fruit {
/// Apple,
/// Orange,
/// Strawberry,
/// Tomato,
/// }
///
/// #[derive(Debug, Clone)]
/// enum Message {
/// FruitSelected(Fruit),
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// combo_box(
/// &state.fruits,
/// "Select your favorite fruit...",
/// state.favorite.as_ref(),
/// Message::FruitSelected
/// )
/// .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",
/// })
/// }
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct ComboBox<
'a,
@ -41,6 +147,7 @@ pub struct ComboBox<
selection: text_input::Value,
on_selected: Box<dyn Fn(T) -> Message>,
on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
on_open: Option<Message>,
on_close: Option<Message>,
on_input: Option<Box<dyn Fn(String) -> Message>>,
menu_class: <Theme as menu::Catalog>::Class<'a>,
@ -77,6 +184,7 @@ where
on_selected: Box::new(on_selected),
on_option_hovered: None,
on_input: None,
on_open: None,
on_close: None,
menu_class: <Theme as Catalog>::default_menu(),
padding: text_input::DEFAULT_PADDING,
@ -104,6 +212,13 @@ where
self
}
/// Sets the message that will be produced when the [`ComboBox`] is
/// opened.
pub fn on_open(mut self, message: Message) -> Self {
self.on_open = Some(message);
self
}
/// Sets the message that will be produced when the outside area
/// of the [`ComboBox`] is pressed.
pub fn on_close(mut self, message: Message) -> Self {
@ -208,12 +323,14 @@ where
/// The local state of a [`ComboBox`].
#[derive(Debug, Clone)]
pub struct State<T>(RefCell<Inner<T>>);
pub struct State<T> {
options: Vec<T>,
inner: RefCell<Inner<T>>,
}
#[derive(Debug, Clone)]
struct Inner<T> {
value: String,
options: Vec<T>,
option_matchers: Vec<String>,
filtered_options: Filtered<T>,
}
@ -247,39 +364,58 @@ where
.collect(),
);
Self(RefCell::new(Inner {
value,
Self {
options,
option_matchers,
filtered_options,
}))
inner: RefCell::new(Inner {
value,
option_matchers,
filtered_options,
}),
}
}
/// Returns the options of the [`State`].
///
/// These are the options provided when the [`State`]
/// was constructed with [`State::new`].
pub fn options(&self) -> &[T] {
&self.options
}
fn value(&self) -> String {
let inner = self.0.borrow();
let inner = self.inner.borrow();
inner.value.clone()
}
fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
let inner = self.0.borrow();
let inner = self.inner.borrow();
f(&inner)
}
fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
let mut inner = self.0.borrow_mut();
let mut inner = self.inner.borrow_mut();
f(&mut inner);
}
fn sync_filtered_options(&self, options: &mut Filtered<T>) {
let inner = self.0.borrow();
let inner = self.inner.borrow();
inner.filtered_options.sync(options);
}
}
impl<T> Default for State<T>
where
T: Display + Clone,
{
fn default() -> Self {
Self::new(Vec::new())
}
}
impl<T> Filtered<T>
where
T: Clone,
@ -322,8 +458,8 @@ enum TextInputEvent {
TextChanged(String),
}
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ComboBox<'a, T, Message, Theme, Renderer>
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ComboBox<'_, T, Message, Theme, Renderer>
where
T: Display + Clone + 'static,
Message: Clone,
@ -373,17 +509,17 @@ where
vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
}
fn on_event(
fn update(
&mut self,
tree: &mut widget::Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let menu = tree.state.downcast_mut::<Menu<T>>();
let started_focused = {
@ -402,9 +538,9 @@ where
let mut local_shell = Shell::new(&mut local_messages);
// Provide it to the widget
let mut event_status = self.text_input.on_event(
self.text_input.update(
&mut tree.children[0],
event.clone(),
event,
layout,
cursor,
renderer,
@ -413,13 +549,19 @@ where
viewport,
);
if local_shell.is_event_captured() {
shell.capture_event();
}
shell.request_redraw_at(local_shell.redraw_request());
shell.request_input_method(local_shell.input_method());
// Then finally react to them here
for message in local_messages {
let TextInputEvent::TextChanged(new_value) = message;
if let Some(on_input) = &self.on_input {
shell.publish((on_input)(new_value.clone()));
published_message_to_shell = true;
}
// Couple the filtered options with the `ComboBox`
@ -431,7 +573,7 @@ where
state.filtered_options.update(
search(
&state.options,
&self.state.options,
&state.option_matchers,
&state.value,
)
@ -440,6 +582,7 @@ where
);
});
shell.invalidate_layout();
shell.request_redraw();
}
let is_focused = {
@ -472,8 +615,8 @@ where
..
}) = event
{
let shift_modifer = modifiers.shift();
match (named_key, shift_modifer) {
let shift_modifier = modifiers.shift();
match (named_key, shift_modifier) {
(key::Named::Enter, _) => {
if let Some(index) = &menu.hovered_option {
if let Some(option) =
@ -483,9 +626,9 @@ where
}
}
event_status = event::Status::Captured;
shell.capture_event();
shell.request_redraw();
}
(key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
if let Some(index) = &mut menu.hovered_option {
if *index == 0 {
@ -520,7 +663,8 @@ where
}
}
event_status = event::Status::Captured;
shell.capture_event();
shell.request_redraw();
}
(key::Named::ArrowDown, _)
| (key::Named::Tab, false)
@ -567,7 +711,8 @@ where
}
}
event_status = event::Status::Captured;
shell.capture_event();
shell.request_redraw();
}
_ => {}
}
@ -580,7 +725,7 @@ where
if let Some(selection) = menu.new_selection.take() {
// Clear the value and reset the options and menu
state.value = String::new();
state.filtered_options.update(state.options.clone());
state.filtered_options.update(self.state.options.clone());
menu.menu = menu::State::default();
// Notify the selection
@ -588,18 +733,21 @@ where
published_message_to_shell = true;
// Unfocus the input
let _ = self.text_input.on_event(
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
self.text_input.update(
&mut tree.children[0],
Event::Mouse(mouse::Event::ButtonPressed(
&Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)),
layout,
mouse::Cursor::Unavailable,
renderer,
clipboard,
&mut Shell::new(&mut vec![]),
&mut local_shell,
viewport,
);
shell.request_input_method(local_shell.input_method());
}
});
@ -611,18 +759,20 @@ where
text_input_state.is_focused()
};
if started_focused && !is_focused && !published_message_to_shell {
if let Some(message) = self.on_close.take() {
shell.publish(message);
if started_focused != is_focused {
// Focus changed, invalidate widget tree to force a fresh `view`
shell.invalidate_widgets();
if !published_message_to_shell {
if is_focused {
if let Some(on_open) = self.on_open.take() {
shell.publish(on_open);
}
} else if let Some(on_close) = self.on_close.take() {
shell.publish(on_close);
}
}
}
// Focus changed, invalidate widget tree to force a fresh `view`
if started_focused != is_focused {
shell.invalidate_widgets();
}
event_status
}
fn mouse_interaction(

View file

@ -1,23 +1,62 @@
//! Decorate content and apply alignment.
//! 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::event::{self, Event};
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, Border, Clipboard, Color, Element, Layout, Length,
self, Background, Clipboard, Color, Element, Event, Layout, Length,
Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector,
Widget,
Widget, color,
};
use crate::runtime::Command;
use crate::runtime::task::{self, Task};
/// An element decorating some content.
/// A widget that aligns its contents inside of its boundaries.
///
/// It is normally used for alignment purposes.
/// # 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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Container<
'a,
@ -69,8 +108,8 @@ where
}
/// Sets the [`Id`] of the [`Container`].
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.id = Some(id.into());
self
}
@ -92,46 +131,6 @@ where
self
}
/// Sets the [`Container`] to fill the available space in the horizontal axis.
///
/// This can be useful to quickly position content when chained with
/// alignment functions—like [`center_x`].
///
/// Calling this method is equivalent to calling [`width`] with a
/// [`Length::Fill`].
///
/// [`center_x`]: Self::center_x
/// [`width`]: Self::width
pub fn fill_x(self) -> Self {
self.width(Length::Fill)
}
/// Sets the [`Container`] to fill the available space in the vetical axis.
///
/// This can be useful to quickly position content when chained with
/// alignment functions—like [`center_y`].
///
/// Calling this method is equivalent to calling [`height`] with a
/// [`Length::Fill`].
///
/// [`center_y`]: Self::center_x
/// [`height`]: Self::height
pub fn fill_y(self) -> Self {
self.height(Length::Fill)
}
/// Sets the [`Container`] to fill all the available space.
///
/// Calling this method is equivalent to chaining [`fill_x`] and
/// [`fill_y`].
///
/// [`center`]: Self::center
/// [`fill_x`]: Self::fill_x
/// [`fill_y`]: Self::fill_y
pub fn fill(self) -> Self {
self.width(Length::Fill).height(Length::Fill)
}
/// 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;
@ -144,18 +143,6 @@ where
self
}
/// Sets the content alignment for the horizontal axis of the [`Container`].
pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
self.horizontal_alignment = alignment;
self
}
/// Sets the content alignment for the vertical axis of the [`Container`].
pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
self.vertical_alignment = alignment;
self
}
/// 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)
@ -179,6 +166,44 @@ where
self.center_x(length).center_y(length)
}
/// Aligns the contents of the [`Container`] to the left.
pub fn align_left(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Left)
}
/// Aligns the contents of the [`Container`] to the right.
pub fn align_right(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Right)
}
/// Aligns the contents of the [`Container`] to the top.
pub fn align_top(self, height: impl Into<Length>) -> Self {
self.height(height).align_y(alignment::Vertical::Top)
}
/// Aligns the contents of the [`Container`] 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 {
@ -197,7 +222,6 @@ where
}
/// Sets the style class of the [`Container`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
@ -205,8 +229,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Container<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Container<'_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
@ -258,7 +282,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
operation.container(
self.id.as_ref().map(|id| &id.0),
@ -274,18 +298,18 @@ where
);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.content.as_widget_mut().on_event(
) {
self.content.as_widget_mut().update(
tree,
event,
layout.children().next().unwrap(),
@ -294,7 +318,7 @@ where
clipboard,
shell,
viewport,
)
);
}
fn mouse_interaction(
@ -457,9 +481,17 @@ impl From<Id> for widget::Id {
}
}
/// Produces a [`Command`] that queries the visible screen bounds of the
impl From<&'static str> for Id {
fn from(value: &'static str) -> Self {
Id::new(value)
}
}
/// Produces a [`Task`] that queries the visible screen bounds of the
/// [`Container`] with the given [`Id`].
pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
pub fn visible_bounds(id: impl Into<Id>) -> Task<Option<Rectangle>> {
let id = id.into();
struct VisibleBounds {
target: widget::Id,
depth: usize,
@ -470,10 +502,11 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
impl Operation<Option<Rectangle>> for VisibleBounds {
fn scrollable(
&mut self,
_state: &mut dyn widget::operation::Scrollable,
_id: Option<&widget::Id>,
bounds: Rectangle,
_content_bounds: Rectangle,
translation: Vector,
_state: &mut dyn widget::operation::Scrollable,
) {
match self.scrollables.last() {
Some((last_translation, last_viewport, _depth)) => {
@ -538,7 +571,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
}
}
Command::widget(VisibleBounds {
task::widget(VisibleBounds {
target: id.into(),
depth: 0,
scrollables: Vec::new(),
@ -547,7 +580,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
}
/// The appearance of a container.
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Style {
/// The text [`Color`] of the container.
pub text_color: Option<Color>,
@ -560,46 +593,54 @@ pub struct Style {
}
impl Style {
/// Updates the border of the [`Style`] with the given [`Color`] and `width`.
pub fn with_border(
self,
color: impl Into<Color>,
width: impl Into<Pixels>,
) -> Self {
/// Updates the text color of the [`Style`].
pub fn color(self, color: impl Into<Color>) -> Self {
Self {
border: Border {
color: color.into(),
width: width.into().0,
..Border::default()
},
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 with_background(self, background: impl Into<Background>) -> Self {
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().with_background(color)
Self::default().background(color)
}
}
impl From<Gradient> for Style {
fn from(gradient: Gradient) -> Self {
Self::default().with_background(gradient)
Self::default().background(gradient)
}
}
impl From<gradient::Linear> for Style {
fn from(gradient: gradient::Linear) -> Self {
Self::default().with_background(gradient)
Self::default().background(gradient)
}
}
@ -618,6 +659,12 @@ pub trait Catalog {
/// 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>;
@ -635,13 +682,18 @@ 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()),
border: Border::rounded(2),
border: border::rounded(2),
..Style::default()
}
}
@ -651,12 +703,57 @@ pub fn bordered_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
background: Some(palette.background.weak.color.into()),
background: Some(palette.background.weakest.color.into()),
border: Border {
width: 1.0,
radius: 0.0.into(),
radius: 5.0.into(),
color: palette.background.strong.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 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()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,21 @@
//! Display images in your user interface.
//! 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;
@ -22,16 +39,23 @@ pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
/// A frame that displays an image while keeping aspect ratio.
///
/// # Example
///
/// ```no_run
/// # use iced_widget::image::{self, Image};
/// #
/// let image = Image::<image::Handle>::new("resources/ferris.png");
/// ```
/// # 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">
#[derive(Debug)]
pub struct Image<Handle> {
pub struct Image<Handle = image::Handle> {
handle: Handle,
width: Length,
height: Length,
@ -39,11 +63,12 @@ pub struct Image<Handle> {
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
scale: f32,
}
impl<Handle> Image<Handle> {
/// Creates a new [`Image`] with the given path.
pub fn new<T: Into<Handle>>(handle: T) -> Self {
pub fn new(handle: impl Into<Handle>) -> Self {
Image {
handle: handle.into(),
width: Length::Shrink,
@ -52,6 +77,7 @@ impl<Handle> Image<Handle> {
filter_method: FilterMethod::default(),
rotation: Rotation::default(),
opacity: 1.0,
scale: 1.0,
}
}
@ -95,6 +121,15 @@ impl<Handle> Image<Handle> {
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
}
}
/// Computes the layout of an [`Image`].
@ -143,11 +178,13 @@ where
pub fn draw<Renderer, Handle>(
renderer: &mut Renderer,
layout: Layout<'_>,
viewport: &Rectangle,
handle: &Handle,
content_fit: ContentFit,
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
scale: f32,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
@ -159,12 +196,12 @@ pub fn draw<Renderer, Handle>(
let bounds = layout.bounds();
let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
let scale = Vector::new(
let fit_scale = Vector::new(
adjusted_fit.width / rotated_size.width,
adjusted_fit.height / rotated_size.height,
);
let final_size = image_size * scale;
let final_size = image_size * fit_scale * scale;
let position = match content_fit {
ContentFit::None => Point::new(
@ -181,17 +218,22 @@ pub fn draw<Renderer, Handle>(
let render = |renderer: &mut Renderer| {
renderer.draw_image(
handle.clone(),
filter_method,
image::Image {
handle: handle.clone(),
filter_method,
rotation: rotation.radians(),
opacity,
snap: true,
},
drawing_bounds,
rotation.radians(),
opacity,
);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
{
renderer.with_layer(bounds, render);
if let Some(bounds) = bounds.intersection(viewport) {
renderer.with_layer(bounds, render);
}
} else {
render(renderer);
}
@ -235,16 +277,18 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
viewport: &Rectangle,
) {
draw(
renderer,
layout,
viewport,
&self.handle,
self.content_fit,
self.filter_method,
self.rotation,
self.opacity,
self.scale,
);
}
}

View file

@ -1,13 +1,12 @@
//! Zoom and pan on an image.
use crate::core::event::{self, Event};
use crate::core::image;
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, Element, Layout, Length, Pixels, Point, Radians, Rectangle,
Shell, Size, Vector, Widget,
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.
@ -20,30 +19,38 @@ pub struct Viewer<Handle> {
max_scale: f32,
scale_step: f32,
handle: Handle,
filter_method: image::FilterMethod,
filter_method: FilterMethod,
content_fit: ContentFit,
}
impl<Handle> Viewer<Handle> {
/// Creates a new [`Viewer`] with the given [`State`].
pub fn new(handle: Handle) -> Self {
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Viewer {
handle,
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: image::FilterMethod::default(),
filter_method: FilterMethod::default(),
content_fit: ContentFit::default(),
}
}
/// Sets the [`image::FilterMethod`] of the [`Viewer`].
/// 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;
@ -115,58 +122,52 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let Size { width, height } = renderer.measure_image(&self.handle);
// The raw w/h of the underlying image
let image_size = renderer.measure_image(&self.handle);
let image_size =
Size::new(image_size.width as f32, image_size.height as f32);
let mut size = limits.resolve(
self.width,
self.height,
Size::new(width as f32, 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);
let expansion_size = if height > width {
self.width
} else {
self.height
// 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,
},
};
// Only calculate viewport sizes if the images are constrained to a limited space.
// If they are Fill|Portion let them expand within their allotted space.
match expansion_size {
Length::Shrink | Length::Fixed(_) => {
let aspect_ratio = width as f32 / height as f32;
let viewport_aspect_ratio = size.width / size.height;
if viewport_aspect_ratio > aspect_ratio {
size.width = width as f32 * size.height / height as f32;
} else {
size.height = height as f32 * size.width / width as f32;
}
}
Length::Fill | Length::FillPortion(_) => {}
}
layout::Node::new(size)
layout::Node::new(final_size)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let bounds = layout.bounds();
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
let Some(cursor_position) = cursor.position_over(bounds) else {
return event::Status::Ignored;
return;
};
match delta {
match *delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
let state = tree.state.downcast_mut::<State>();
@ -182,11 +183,12 @@ where
})
.clamp(self.min_scale, self.max_scale);
let image_size = image_size(
let scaled_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);
let factor = state.scale / previous_scale - 1.0;
@ -198,12 +200,12 @@ where
+ state.current_offset * factor;
state.current_offset = Vector::new(
if image_size.width > bounds.width {
if scaled_size.width > bounds.width {
state.current_offset.x + adjustment.x
} else {
0.0
},
if image_size.height > bounds.height {
if scaled_size.height > bounds.height {
state.current_offset.y + adjustment.y
} else {
0.0
@ -213,11 +215,12 @@ where
}
}
event::Status::Captured
shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let Some(cursor_position) = cursor.position_over(bounds) else {
return event::Status::Ignored;
return;
};
let state = tree.state.downcast_mut::<State>();
@ -225,49 +228,48 @@ where
state.cursor_grabbed_at = Some(cursor_position);
state.starting_offset = state.current_offset;
event::Status::Captured
shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree.state.downcast_mut::<State>();
if state.cursor_grabbed_at.is_some() {
state.cursor_grabbed_at = None;
event::Status::Captured
} else {
event::Status::Ignored
shell.request_redraw();
shell.capture_event();
}
}
Event::Mouse(mouse::Event::CursorMoved { position }) => {
let state = tree.state.downcast_mut::<State>();
if let Some(origin) = state.cursor_grabbed_at {
let image_size = image_size(
let scaled_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);
let hidden_width = (image_size.width - bounds.width / 2.0)
let hidden_width = (scaled_size.width - bounds.width / 2.0)
.max(0.0)
.round();
let hidden_height = (image_size.height
let hidden_height = (scaled_size.height
- bounds.height / 2.0)
.max(0.0)
.round();
let delta = position - origin;
let delta = *position - origin;
let x = if bounds.width < image_size.width {
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 < image_size.height {
let y = if bounds.height < scaled_size.height {
(state.starting_offset.y - delta.y)
.clamp(-hidden_height, hidden_height)
} else {
@ -275,13 +277,11 @@ where
};
state.current_offset = Vector::new(x, y);
event::Status::Captured
} else {
event::Status::Ignored
shell.request_redraw();
shell.capture_event();
}
}
_ => event::Status::Ignored,
_ => {}
}
}
@ -319,33 +319,46 @@ where
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let image_size =
image_size(renderer, &self.handle, state, bounds.size());
let final_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);
let translation = {
let image_top_left = Vector::new(
bounds.width / 2.0 - image_size.width / 2.0,
bounds.height / 2.0 - image_size.height / 2.0,
);
let diff_w = bounds.width - final_size.width;
let diff_h = bounds.height - final_size.height;
image_top_left - state.offset(bounds, image_size)
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)
};
renderer.with_layer(bounds, |renderer| {
let drawing_bounds = Rectangle::new(bounds.position(), final_size);
let render = |renderer: &mut Renderer| {
renderer.with_translation(translation, |renderer| {
renderer.draw_image(
self.handle.clone(),
self.filter_method,
Rectangle {
x: bounds.x,
y: bounds.y,
..Rectangle::with_size(image_size)
Image {
handle: self.handle.clone(),
filter_method: self.filter_method,
rotation: Radians(0.0),
opacity: 1.0,
snap: true,
},
Radians(0.0),
1.0,
drawing_bounds,
);
});
});
};
renderer.with_layer(bounds, render);
}
}
@ -411,32 +424,23 @@ where
/// Returns the bounds of the underlying image, given the bounds of
/// the [`Viewer`]. Scaling will be applied and original aspect ratio
/// will be respected.
pub fn image_size<Renderer>(
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);
let image_size = Size::new(width as f32, height as f32);
let (width, height) = {
let dimensions = (width as f32, height as f32);
let adjusted_fit = content_fit.fit(image_size, bounds);
let width_ratio = bounds.width / dimensions.0;
let height_ratio = bounds.height / dimensions.1;
let ratio = width_ratio.min(height_ratio);
let scale = state.scale;
if ratio < 1.0 {
(dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
} else {
(dimensions.0 * scale, dimensions.1 * scale)
}
};
Size::new(width, height)
Size::new(
adjusted_fit.width * state.scale,
adjusted_fit.height * state.scale,
)
}

View file

@ -1,4 +1,4 @@
//! Use widgets that can provide hints to ensure continuity.
//! Keyed widgets can provide hints to ensure continuity.
//!
//! # What is continuity?
//! Continuity is the feeling of persistence of state.
@ -41,13 +41,35 @@ pub mod column;
pub use column::Column;
/// Creates a [`Column`] with the given children.
/// 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::Column::new()
$crate::keyed::Column::new()
);
($($x:expr),+ $(,)?) => (
$crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+])
($(($key:expr, $x:expr)),+ $(,)?) => (
$crate::keyed::Column::with_children(vec![$(($key, $crate::core::Element::from($x))),+])
);
}

View file

@ -1,17 +1,34 @@
//! Distribute content vertically.
use crate::core::event::{self, Event};
//! 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::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
Shell, Size, Vector, Widget,
Alignment, Clipboard, Element, Event, Layout, Length, Padding, Pixels,
Rectangle, Shell, Size, Vector, Widget,
};
/// A container that distributes its contents vertically.
/// 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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Column<
'a,
@ -168,7 +185,7 @@ where
}
}
impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
impl<Key, Message, Renderer> Default for Column<'_, Key, Message, Renderer>
where
Key: Copy + PartialEq,
Renderer: crate::core::Renderer,
@ -185,8 +202,8 @@ where
keys: Vec<Key>,
}
impl<'a, Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Column<'a, Key, Message, Theme, Renderer>
impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Column<'_, Key, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
Key: Copy + PartialEq + 'static,
@ -265,7 +282,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@ -280,34 +297,28 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.children
) {
for ((child, state), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
{
child.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(

View file

@ -4,21 +4,21 @@ pub(crate) mod helpers;
pub mod component;
pub mod responsive;
#[allow(deprecated)]
pub use component::Component;
pub use responsive::Responsive;
mod cache;
use crate::core::event::{self, Event};
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::Element;
use crate::core::{
self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector,
self, Clipboard, Event, Length, Point, Rectangle, Shell, Size, Vector,
};
use crate::runtime::overlay::Nested;
@ -29,6 +29,7 @@ use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
/// A widget that only rebuilds its contents when necessary.
#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
dependency: Dependency,
@ -182,7 +183,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
self.with_element(|element| {
element.as_widget().operate(
@ -194,19 +195,19 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
self.with_element_mut(|element| {
element.as_widget_mut().on_event(
element.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
@ -215,8 +216,8 @@ where
clipboard,
shell,
viewport,
)
})
);
});
}
fn mouse_interaction(
@ -267,30 +268,41 @@ where
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'_, Message, Theme, Renderer>> {
let overlay = Overlay(Some(
InnerBuilder {
cell: self.element.borrow().as_ref().unwrap().clone(),
element: self
.element
.borrow()
.as_ref()
.unwrap()
.borrow_mut()
.take()
.unwrap(),
tree: &mut tree.children[0],
overlay_builder: |element, tree| {
element
.as_widget_mut()
.overlay(tree, layout, renderer, translation)
.map(|overlay| RefCell::new(Nested::new(overlay)))
},
}
.build(),
));
) -> 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],
overlay_builder: |element, tree| {
element
.as_widget_mut()
.overlay(tree, layout, renderer, translation)
.map(|overlay| RefCell::new(Nested::new(overlay)))
},
}
.build();
Some(overlay::Element::new(Box::new(overlay)))
#[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
}
}
}
@ -309,16 +321,14 @@ struct Overlay<'a, Message, Theme, Renderer>(
Option<Inner<'a, Message, Theme, Renderer>>,
);
impl<'a, Message, Theme, Renderer> Drop
for Overlay<'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<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> {
impl<Message, Theme, Renderer> Overlay<'_, Message, Theme, Renderer> {
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T,
@ -338,8 +348,8 @@ impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> {
}
}
impl<'a, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
@ -374,19 +384,18 @@ where
.unwrap_or_default()
}
fn on_event(
fn update(
&mut self,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_overlay_mut_maybe(|overlay| {
overlay.on_event(event, layout, cursor, renderer, clipboard, shell)
})
.unwrap_or(event::Status::Ignored)
) {
let _ = self.with_overlay_mut_maybe(|overlay| {
overlay.update(event, layout, cursor, renderer, clipboard, shell);
});
}
fn is_over(

View file

@ -1,5 +1,6 @@
use crate::core::overlay;
#![allow(dead_code)]
use crate::core::Element;
use crate::core::overlay;
use ouroboros::self_referencing;

View file

@ -1,5 +1,5 @@
//! Build and reuse custom widgets using The Elm Architecture.
use crate::core::event;
#![allow(deprecated)]
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
@ -30,6 +30,25 @@ use std::rc::Rc;
///
/// 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;
@ -58,8 +77,9 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
/// By default, it does nothing.
fn operate(
&self,
_bounds: Rectangle,
_state: &mut Self::State,
_operation: &mut dyn widget::Operation<Message>,
_operation: &mut dyn widget::Operation,
) {
}
@ -121,8 +141,8 @@ struct State<'a, Message: 'a, Theme: 'a, Renderer: 'a, Event: 'a, S: 'a> {
element: Option<Element<'this, Event, Theme, Renderer>>,
}
impl<'a, Message, Theme, Renderer, Event, S>
Instance<'a, Message, Theme, Renderer, Event, S>
impl<Message, Theme, Renderer, Event, S>
Instance<'_, Message, Theme, Renderer, Event, S>
where
S: Default + 'static,
Renderer: renderer::Renderer,
@ -172,11 +192,13 @@ where
fn rebuild_element_with_operation(
&self,
operation: &mut dyn widget::Operation<Message>,
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()
@ -231,8 +253,8 @@ where
}
}
impl<'a, Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer>
for Instance<'a, Message, Theme, Renderer, Event, S>
impl<Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer>
for Instance<'_, Message, Theme, Renderer, Event, S>
where
S: 'static + Default,
Renderer: core::Renderer,
@ -247,7 +269,10 @@ where
state: tree::State::new(S::default()),
children: vec![Tree::empty()],
})));
*self.tree.borrow_mut() = state.clone();
self.diff_self();
tree::State::new(state)
}
@ -291,23 +316,23 @@ where
})
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: core::Event,
event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
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>>>>();
let event_status = self.with_element_mut(|element| {
element.as_widget_mut().on_event(
self.with_element_mut(|element| {
element.as_widget_mut().update(
&mut t.borrow_mut().as_mut().unwrap().children[0],
event,
layout,
@ -316,15 +341,17 @@ where
clipboard,
&mut local_shell,
viewport,
)
);
});
local_shell.revalidate_layout(|| shell.invalidate_layout());
if let Some(redraw_request) = local_shell.redraw_request() {
shell.request_redraw(redraw_request);
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();
@ -349,8 +376,6 @@ where
shell.invalidate_layout();
}
event_status
}
fn operate(
@ -358,62 +383,9 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
self.rebuild_element_with_operation(operation);
struct MapOperation<'a, B> {
operation: &'a mut dyn widget::Operation<B>,
}
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
fn container(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>,
),
) {
self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation });
});
}
fn focusable(
&mut self,
state: &mut dyn widget::operation::Focusable,
id: Option<&widget::Id>,
) {
self.operation.focusable(state, id);
}
fn text_input(
&mut self,
state: &mut dyn widget::operation::TextInput,
id: Option<&widget::Id>,
) {
self.operation.text_input(state, id);
}
fn scrollable(
&mut self,
state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
}
fn custom(
&mut self,
state: &mut dyn std::any::Any,
id: Option<&widget::Id>,
) {
self.operation.custom(state, id);
}
}
self.rebuild_element_with_operation(layout, operation);
let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| {
@ -421,7 +393,7 @@ where
&mut tree.borrow_mut().as_mut().unwrap().children[0],
layout,
renderer,
&mut MapOperation { operation },
operation,
);
});
}
@ -479,44 +451,48 @@ where
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.rebuild_element_if_necessary();
let tree = tree
.state
.downcast_mut::<Rc<RefCell<Option<Tree>>>>()
.borrow_mut()
.take()
.unwrap();
let state = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
let tree = state.borrow_mut().take().unwrap();
let overlay = Overlay(Some(
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,
translation,
)
.map(|overlay| {
RefCell::new(Nested::new(overlay))
})
},
)
},
}
.build(),
));
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,
translation,
)
.map(|overlay| RefCell::new(Nested::new(overlay)))
},
)
},
}
.build();
Some(overlay::Element::new(Box::new(OverlayInstance {
overlay: Some(overlay),
})))
#[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
}
}
}
@ -524,8 +500,8 @@ struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>(
Option<Inner<'a, 'b, Message, Theme, Renderer, Event, S>>,
);
impl<'a, 'b, Message, Theme, Renderer, Event, S> Drop
for Overlay<'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) {
@ -549,8 +525,8 @@ struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> {
overlay: Option<Overlay<'a, 'b, Message, Theme, Renderer, Event, S>>,
}
impl<'a, 'b, Message, Theme, Renderer, Event, S>
OverlayInstance<'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,
@ -583,9 +559,9 @@ impl<'a, 'b, Message, Theme, Renderer, Event, S>
}
}
impl<'a, 'b, Message, Theme, Renderer, Event, S>
impl<Message, Theme, Renderer, Event, S>
overlay::Overlay<Message, Theme, Renderer>
for OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S>
for OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S>
where
Renderer: core::Renderer,
S: 'static + Default,
@ -621,36 +597,36 @@ where
.unwrap_or_default()
}
fn on_event(
fn update(
&mut self,
event: core::Event,
event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
) {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
let event_status = self
.with_overlay_mut_maybe(|overlay| {
overlay.on_event(
event,
layout,
cursor,
renderer,
clipboard,
&mut local_shell,
)
})
.unwrap_or(event::Status::Ignored);
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());
if let Some(redraw_request) = local_shell.redraw_request() {
shell.request_redraw(redraw_request);
}
shell.request_redraw_at(local_shell.redraw_request());
shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut inner =
@ -687,8 +663,6 @@ where
shell.invalidate_layout();
}
event_status
}
fn is_over(

View file

@ -1,9 +1,11 @@
use crate::core::{self, Element, Size};
use crate::lazy::component::{self, Component};
use crate::lazy::{Lazy, Responsive};
use crate::lazy::component;
use std::hash::Hash;
#[allow(deprecated)]
pub use crate::lazy::{Component, Lazy, Responsive};
/// 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")]
@ -21,6 +23,12 @@ where
/// 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>

View file

@ -1,4 +1,3 @@
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
@ -6,8 +5,8 @@ use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
Widget,
self, Clipboard, Element, Event, Length, Point, Rectangle, Shell, Size,
Vector, Widget,
};
use crate::horizontal_space;
use crate::runtime::overlay::Nested;
@ -21,6 +20,7 @@ use std::ops::Deref;
///
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Responsive<
'a,
@ -82,15 +82,21 @@ where
new_size: Size,
view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>,
) {
if self.size == new_size {
return;
if self.size != new_size {
self.element = view(new_size);
self.size = new_size;
self.layout = None;
tree.diff(&self.element);
} else {
let is_tree_empty =
tree.tag == tree::Tag::stateless() && tree.children.is_empty();
if is_tree_empty {
self.layout = None;
tree.diff(&self.element);
}
}
self.element = view(new_size);
self.size = new_size;
self.layout = None;
tree.diff(&self.element);
}
fn resolve<R, T>(
@ -125,8 +131,8 @@ struct State {
tree: RefCell<Tree>,
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Responsive<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Responsive<'_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
@ -161,7 +167,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
@ -179,30 +185,30 @@ where
);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
let mut local_messages = vec![];
let mut local_shell = Shell::new(&mut local_messages);
let status = content.resolve(
content.resolve(
&mut state.tree.borrow_mut(),
renderer,
layout,
&self.view,
|tree, renderer, layout, element| {
element.as_widget_mut().on_event(
element.as_widget_mut().update(
tree,
event,
layout,
@ -211,7 +217,7 @@ where
clipboard,
&mut local_shell,
viewport,
)
);
},
);
@ -220,8 +226,6 @@ where
}
shell.merge(local_shell, std::convert::identity);
status
}
fn draw(
@ -319,7 +323,11 @@ where
}
.build();
Some(overlay::Element::new(Box::new(overlay)))
if overlay.with_overlay(|(overlay, _layout)| overlay.is_some()) {
Some(overlay::Element::new(Box::new(overlay)))
} else {
None
}
}
}
@ -350,9 +358,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> {
),
}
impl<'a, 'b, Message, Theme, Renderer>
Overlay<'a, 'b, Message, Theme, Renderer>
{
impl<Message, Theme, Renderer> Overlay<'_, '_, Message, Theme, Renderer> {
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T,
@ -372,9 +378,8 @@ impl<'a, 'b, Message, Theme, Renderer>
}
}
impl<'a, 'b, Message, Theme, Renderer>
overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, 'b, Message, Theme, Renderer>
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'_, '_, Message, Theme, Renderer>
where
Renderer: core::Renderer,
{
@ -409,36 +414,28 @@ where
.unwrap_or_default()
}
fn on_event(
fn update(
&mut self,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
) {
let mut is_layout_invalid = false;
let event_status = self
.with_overlay_mut_maybe(|overlay| {
let event_status = overlay.on_event(
event, layout, cursor, renderer, clipboard, shell,
);
let _ = self.with_overlay_mut_maybe(|overlay| {
overlay.update(event, layout, cursor, renderer, clipboard, shell);
is_layout_invalid = shell.is_layout_invalid();
event_status
})
.unwrap_or(event::Status::Ignored);
is_layout_invalid = shell.is_layout_invalid();
});
if is_layout_invalid {
self.with_overlay_mut(|(_overlay, layout)| {
**layout = None;
});
}
event_status
}
fn is_over(
@ -452,4 +449,15 @@ where
})
.unwrap_or_default()
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
let _ = self.with_overlay_mut_maybe(|overlay| {
overlay.operate(layout, renderer, operation);
});
}
}

View file

@ -8,9 +8,10 @@ pub use iced_renderer::graphics;
pub use iced_runtime as runtime;
pub use iced_runtime::core;
mod action;
mod column;
mod mouse_area;
mod row;
mod pin;
mod space;
mod stack;
mod themer;
@ -23,8 +24,10 @@ pub mod keyed;
pub mod overlay;
pub mod pane_grid;
pub mod pick_list;
pub mod pop;
pub mod progress_bar;
pub mod radio;
pub mod row;
pub mod rule;
pub mod scrollable;
pub mod slider;
@ -42,9 +45,6 @@ pub use helpers::*;
#[cfg(feature = "lazy")]
mod lazy;
#[cfg(feature = "lazy")]
pub use crate::lazy::{Component, Lazy, Responsive};
#[cfg(feature = "lazy")]
pub use crate::lazy::helpers::*;
@ -65,6 +65,10 @@ 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 pop::Pop;
#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
@ -130,5 +134,9 @@ pub mod 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;

1337
widget/src/markdown.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,13 @@
//! A container for capturing mouse events.
use iced_renderer::core::Point;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{tree, Operation, Tree};
use crate::core::widget::{Operation, Tree, tree};
use crate::core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size,
Vector, Widget,
};
/// Emit messages on mouse events.
@ -24,12 +21,14 @@ pub struct MouseArea<
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>>,
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
@ -49,6 +48,22 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
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 {
@ -77,6 +92,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
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 {
@ -86,11 +111,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
/// The message to emit when the mouse moves in the area.
#[must_use]
pub fn on_move<F>(mut self, build_message: F) -> Self
where
F: Fn(Point) -> Message + 'static,
{
self.on_move = Some(Box::new(build_message));
pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
self.on_move = Some(Box::new(on_move));
self
}
@ -113,6 +135,9 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
#[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> {
@ -124,10 +149,12 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
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,
@ -136,8 +163,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for MouseArea<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for MouseArea<'_, Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
Message: Clone,
@ -178,7 +205,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
self.content.as_widget().operate(
&mut tree.children[0],
@ -188,31 +215,33 @@ where
);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
if let event::Status::Captured = self.content.as_widget_mut().on_event(
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event.clone(),
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
) {
return event::Status::Captured;
);
if shell.is_event_captured() {
return;
}
update(self, tree, event, layout, cursor, shell)
update(self, tree, event, layout, cursor, shell);
}
fn mouse_interaction(
@ -297,18 +326,22 @@ where
fn update<Message: Clone, Theme, Renderer>(
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) -> event::Status {
if let Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) = event
{
let state: &mut State = tree.state.downcast_mut();
) {
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(),
@ -331,71 +364,71 @@ fn update<Message: Clone, Theme, Renderer>(
}
if !cursor.is_over(layout.bounds()) {
return event::Status::Ignored;
return;
}
if let Some(message) = widget.on_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) = event
{
shell.publish(message.clone());
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();
}
return event::Status::Captured;
if let Some(position) = cursor_position {
if 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();
}
}
}
}
if let Some(message) = widget.on_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) = event
{
shell.publish(message.clone());
return event::Status::Captured;
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());
}
}
}
if let Some(message) = widget.on_right_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) =
event
{
shell.publish(message.clone());
return event::Status::Captured;
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();
}
}
}
if let Some(message) = widget.on_right_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Right,
)) = event
{
shell.publish(message.clone());
return event::Status::Captured;
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
if let Some(message) = widget.on_right_release.as_ref() {
shell.publish(message.clone());
}
}
}
if let Some(message) = widget.on_middle_press.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Middle,
)) = event
{
shell.publish(message.clone());
return event::Status::Captured;
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();
}
}
}
if let Some(message) = widget.on_middle_release.as_ref() {
if let Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Middle,
)) = event
{
shell.publish(message.clone());
return event::Status::Captured;
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();
}
}
_ => {}
}
event::Status::Ignored
}

View file

@ -1,15 +1,16 @@
//! Build and show dropdown menus.
use crate::core::alignment;
use crate::core::event::{self, Event};
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;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
Background, Border, Clipboard, Color, Length, Padding, Pixels, Point,
Background, Clipboard, Color, Event, Length, Padding, Pixels, Point,
Rectangle, Size, Theme, Vector,
};
use crate::core::{Element, Shell, Widget};
@ -200,21 +201,18 @@ where
class,
} = menu;
let list = Scrollable::with_direction(
List {
options,
hovered_option,
on_selected,
on_option_hovered,
font,
text_size,
text_line_height,
text_shaping,
padding,
class,
},
scrollable::Direction::default(),
);
let list = Scrollable::new(List {
options,
hovered_option,
on_selected,
on_option_hovered,
font,
text_size,
text_line_height,
text_shaping,
padding,
class,
});
state.tree.diff(&list as &dyn Widget<_, _, _>);
@ -229,9 +227,8 @@ where
}
}
impl<'a, 'b, Message, Theme, Renderer>
crate::core::Overlay<Message, Theme, Renderer>
for Overlay<'a, 'b, Message, Theme, Renderer>
impl<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer>
for Overlay<'_, '_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
@ -264,21 +261,21 @@ where
})
}
fn on_event(
fn update(
&mut self,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
) {
let bounds = layout.bounds();
self.list.on_event(
self.list.update(
self.state, event, layout, cursor, renderer, clipboard, shell,
&bounds,
)
);
}
fn mouse_interaction(
@ -336,13 +333,25 @@ where
class: &'a <Theme as Catalog>::Class<'b>,
}
impl<'a, 'b, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for List<'a, 'b, T, Message, Theme, Renderer>
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,
@ -376,24 +385,24 @@ where
layout::Node::new(size)
}
fn on_event(
fn update(
&mut self,
_state: &mut Tree,
event: Event,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if cursor.is_over(layout.bounds()) {
if let Some(index) = *self.hovered_option {
if let Some(option) = self.options.get(index) {
shell.publish((self.on_selected)(option.clone()));
return event::Status::Captured;
shell.capture_event();
}
}
}
@ -413,14 +422,18 @@ where
let new_hovered_option =
(cursor_position.y / option_height) as usize;
if let Some(on_option_hovered) = self.on_option_hovered {
if *self.hovered_option != Some(new_hovered_option) {
if let Some(option) =
self.options.get(new_hovered_option)
if *self.hovered_option != Some(new_hovered_option) {
if 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();
}
}
@ -445,7 +458,7 @@ where
if let Some(index) = *self.hovered_option {
if let Some(option) = self.options.get(index) {
shell.publish((self.on_selected)(option.clone()));
return event::Status::Captured;
shell.capture_event();
}
}
}
@ -453,7 +466,15 @@ where
_ => {}
}
event::Status::Ignored
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(
@ -517,7 +538,7 @@ where
width: bounds.width - style.border.width * 2.0,
..bounds
},
border: Border::rounded(style.border.radius),
border: border::rounded(style.border.radius),
..renderer::Quad::default()
},
style.selected_background,
@ -534,6 +555,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
@ -563,7 +585,7 @@ where
}
/// The appearance of a [`Menu`].
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the menu.
pub background: Background,

View file

@ -1,15 +1,62 @@
//! Let your users split regions of your application and organize layout dynamically.
//! Pane grids let your users split regions of your application and organize layout dynamically.
//!
//! ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
//!
//! This distribution of space is common in tiling window managers (like
//! [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
//! [`tmux`](https://github.com/tmux/tmux)).
//!
//! A [`PaneGrid`] supports:
//!
//! * Vertical and horizontal splits
//! * Tracking of the last active pane
//! * Mouse-based resizing
//! * Drag and drop to reorganize panes
//! * Hotkey support
//! * Configurable modifier keys
//! * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
//!
//! # Example
//! ```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::{pane_grid, text};
//!
//! struct State {
//! panes: pane_grid::State<Pane>,
//! }
//!
//! enum Pane {
//! SomePane,
//! AnotherKindOfPane,
//! }
//!
//! enum Message {
//! PaneDragged(pane_grid::DragEvent),
//! PaneResized(pane_grid::ResizeEvent),
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! pane_grid(&state.panes, |pane, state, is_maximized| {
//! pane_grid::Content::new(match state {
//! Pane::SomePane => text("This is some pane"),
//! Pane::AnotherKindOfPane => text("This is another kind of pane"),
//! })
//! })
//! .on_drag(Message::PaneDragged)
//! .on_resize(10, Message::PaneResized)
//! .into()
//! }
//! ```
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.12/examples/pane_grid
//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.13/examples/pane_grid
mod axis;
mod configuration;
mod content;
mod controls;
mod direction;
mod draggable;
mod node;
@ -22,6 +69,7 @@ pub mod state;
pub use axis::Axis;
pub use configuration::Configuration;
pub use content::Content;
pub use controls::Controls;
pub use direction::Direction;
pub use draggable::Draggable;
pub use node::Node;
@ -31,7 +79,6 @@ pub use state::State;
pub use title_bar::TitleBar;
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay::{self, Group};
@ -39,8 +86,9 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
self, Background, Border, Clipboard, Color, Element, Layout, Length,
self, Background, Border, Clipboard, Color, Element, Event, Layout, Length,
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
@ -66,14 +114,18 @@ const THICKNESS_RATIO: f32 = 25.0;
/// * Configurable modifier keys
/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
///
/// ## Example
///
/// # Example
/// ```no_run
/// # use iced_widget::{pane_grid, text};
/// # 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>;
/// #
/// # type PaneGrid<'a, Message> = iced_widget::PaneGrid<'a, Message>;
/// #
/// enum PaneState {
/// use iced::widget::{pane_grid, text};
///
/// struct State {
/// panes: pane_grid::State<Pane>,
/// }
///
/// enum Pane {
/// SomePane,
/// AnotherKindOfPane,
/// }
@ -83,17 +135,17 @@ const THICKNESS_RATIO: f32 = 25.0;
/// PaneResized(pane_grid::ResizeEvent),
/// }
///
/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane);
///
/// let pane_grid =
/// PaneGrid::new(&state, |pane, state, is_maximized| {
/// fn view(state: &State) -> Element<'_, Message> {
/// pane_grid(&state.panes, |pane, state, is_maximized| {
/// pane_grid::Content::new(match state {
/// PaneState::SomePane => text("This is some pane"),
/// PaneState::AnotherKindOfPane => text("This is another kind of pane"),
/// Pane::SomePane => text("This is some pane"),
/// Pane::AnotherKindOfPane => text("This is another kind of pane"),
/// })
/// })
/// .on_drag(Message::PaneDragged)
/// .on_resize(10, Message::PaneResized);
/// .on_resize(10, Message::PaneResized)
/// .into()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct PaneGrid<
@ -105,7 +157,9 @@ pub struct PaneGrid<
Theme: Catalog,
Renderer: core::Renderer,
{
contents: Contents<'a, Content<'a, Message, Theme, Renderer>>,
internal: &'a state::Internal,
panes: Vec<Pane>,
contents: Vec<Content<'a, Message, Theme, Renderer>>,
width: Length,
height: Length,
spacing: f32,
@ -113,6 +167,7 @@ pub struct PaneGrid<
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
class: <Theme as Catalog>::Class<'a>,
last_mouse_interaction: Option<mouse::Interaction>,
}
impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer>
@ -128,29 +183,19 @@ where
state: &'a State<T>,
view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>,
) -> Self {
let contents = if let Some((pane, pane_state)) =
state.maximized.and_then(|pane| {
state.panes.get(&pane).map(|pane_state| (pane, pane_state))
}) {
Contents::Maximized(
pane,
view(pane, pane_state, true),
Node::Pane(pane),
)
} else {
Contents::All(
state
.panes
.iter()
.map(|(pane, pane_state)| {
(*pane, view(*pane, pane_state, false))
})
.collect(),
&state.internal,
)
};
let panes = state.panes.keys().copied().collect();
let contents = state
.panes
.iter()
.map(|(pane, pane_state)| match state.maximized() {
Some(p) if *pane == p => view(*pane, pane_state, true),
_ => view(*pane, pane_state, false),
})
.collect();
Self {
internal: &state.internal,
panes,
contents,
width: Length::Fill,
height: Length::Fill,
@ -159,6 +204,7 @@ where
on_drag: None,
on_resize: None,
class: <Theme as Catalog>::default(),
last_mouse_interaction: None,
}
}
@ -196,7 +242,9 @@ where
where
F: 'a + Fn(DragEvent) -> Message,
{
self.on_drag = Some(Box::new(f));
if self.internal.maximized().is_none() {
self.on_drag = Some(Box::new(f));
}
self
}
@ -213,7 +261,9 @@ where
where
F: 'a + Fn(ResizeEvent) -> Message,
{
self.on_resize = Some((leeway.into().0, Box::new(f)));
if self.internal.maximized().is_none() {
self.on_resize = Some((leeway.into().0, Box::new(f)));
}
self
}
@ -239,46 +289,114 @@ where
}
fn drag_enabled(&self) -> bool {
(!self.contents.is_maximized())
self.internal
.maximized()
.is_none()
.then(|| self.on_drag.is_some())
.unwrap_or_default()
}
fn grid_interaction(
&self,
action: &state::Action,
layout: Layout<'_>,
cursor: mouse::Cursor,
) -> Option<mouse::Interaction> {
if action.picked_pane().is_some() {
return Some(mouse::Interaction::Grabbing);
}
let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
let node = self.internal.layout();
let resize_axis =
action.picked_split().map(|(_, axis)| axis).or_else(|| {
resize_leeway.and_then(|leeway| {
let cursor_position = cursor.position()?;
let bounds = layout.bounds();
let splits =
node.split_regions(self.spacing, bounds.size());
let relative_cursor = Point::new(
cursor_position.x - bounds.x,
cursor_position.y - bounds.y,
);
hovered_split(
splits.iter(),
self.spacing + leeway,
relative_cursor,
)
.map(|(_, axis, _)| axis)
})
});
if let Some(resize_axis) = resize_axis {
return Some(match resize_axis {
Axis::Horizontal => mouse::Interaction::ResizingVertically,
Axis::Vertical => mouse::Interaction::ResizingHorizontally,
});
}
None
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for PaneGrid<'a, Message, Theme, Renderer>
#[derive(Default)]
struct Memory {
action: state::Action,
order: Vec<Pane>,
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for PaneGrid<'_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<state::Action>()
tree::Tag::of::<Memory>()
}
fn state(&self) -> tree::State {
tree::State::new(state::Action::Idle)
tree::State::new(Memory::default())
}
fn children(&self) -> Vec<Tree> {
self.contents
.iter()
.map(|(_, content)| content.state())
.collect()
self.contents.iter().map(Content::state).collect()
}
fn diff(&self, tree: &mut Tree) {
match &self.contents {
Contents::All(contents, _) => tree.diff_children_custom(
contents,
|state, (_, content)| content.diff(state),
|(_, content)| content.state(),
),
Contents::Maximized(_, content, _) => tree.diff_children_custom(
&[content],
|state, content| content.diff(state),
|content| content.state(),
),
}
let Memory { order, .. } = tree.state.downcast_ref();
// `Pane` always increments and is iterated by Ord so new
// states are always added at the end. We can simply remove
// states which no longer exist and `diff_children` will
// diff the remaining values in the correct order and
// add new states at the end
let mut i = 0;
let mut j = 0;
tree.children.retain(|_| {
let retain = self.panes.get(i) == order.get(j);
if retain {
i += 1;
}
j += 1;
retain
});
tree.diff_children_custom(
&self.contents,
|state, content| content.diff(state),
Content::state,
);
let Memory { order, .. } = tree.state.downcast_mut();
order.clone_from(&self.panes);
}
fn size(&self) -> Size<Length> {
@ -295,14 +413,23 @@ where
limits: &layout::Limits,
) -> layout::Node {
let size = limits.resolve(self.width, self.height, Size::ZERO);
let node = self.contents.layout();
let regions = node.pane_regions(self.spacing, size);
let regions = self.internal.layout().pane_regions(self.spacing, size);
let children = self
.contents
.panes
.iter()
.copied()
.zip(&self.contents)
.zip(tree.children.iter_mut())
.filter_map(|((pane, content), tree)| {
if self
.internal
.maximized()
.is_some_and(|maximized| maximized != pane)
{
return Some(layout::Node::new(Size::ZERO));
}
let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height);
@ -324,34 +451,39 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.contents
self.panes
.iter()
.copied()
.zip(&self.contents)
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|(((_pane, content), state), layout)| {
.filter(|(((pane, _), _), _)| {
self.internal
.maximized()
.is_none_or(|maximized| *pane == maximized)
})
.for_each(|(((_, content), state), layout)| {
content.operate(state, layout, renderer, operation);
});
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
let mut event_status = event::Status::Ignored;
let action = tree.state.downcast_mut::<state::Action>();
let node = self.contents.layout();
) {
let Memory { action, .. } = tree.state.downcast_mut();
let node = self.internal.layout();
let on_drag = if self.drag_enabled() {
&self.on_drag
@ -359,13 +491,36 @@ where
&None
};
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
for (((pane, content), tree), layout) in self
.panes
.iter()
.copied()
.zip(&mut self.contents)
.zip(&mut tree.children)
.zip(layout.children())
.filter(|(((pane, _), _), _)| {
self.internal
.maximized()
.is_none_or(|maximized| *pane == maximized)
})
{
let is_picked = picked_pane == Some(pane);
content.update(
tree, event, layout, cursor, renderer, clipboard, shell,
viewport, is_picked,
);
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let bounds = layout.bounds();
if let Some(cursor_position) = cursor.position_over(bounds) {
event_status = event::Status::Captured;
shell.capture_event();
match &self.on_resize {
Some((leeway, _)) => {
@ -396,7 +551,10 @@ where
layout,
cursor_position,
shell,
self.contents.iter(),
self.panes
.iter()
.copied()
.zip(&self.contents),
&self.on_click,
on_drag,
);
@ -408,7 +566,7 @@ where
layout,
cursor_position,
shell,
self.contents.iter(),
self.panes.iter().copied().zip(&self.contents),
&self.on_click,
on_drag,
);
@ -434,8 +592,10 @@ where
}
} else {
let dropped_region = self
.contents
.panes
.iter()
.copied()
.zip(&self.contents)
.zip(layout.children())
.find_map(|(target, layout)| {
layout_region(
@ -461,13 +621,13 @@ where
};
shell.publish(on_drag(event));
} else {
shell.publish(on_drag(DragEvent::Canceled {
pane,
}));
}
}
}
event_status = event::Status::Captured;
} else if action.picked_split().is_some() {
event_status = event::Status::Captured;
}
*action = state::Action::Idle;
@ -509,37 +669,48 @@ where
ratio,
}));
event_status = event::Status::Captured;
shell.capture_event();
}
}
} else if action.picked_pane().is_some() {
shell.request_redraw();
}
}
}
_ => {}
}
let picked_pane = action.picked_pane().map(|(pane, _)| pane);
if shell.redraw_request() != window::RedrawRequest::NextFrame {
let interaction = self
.grid_interaction(action, layout, cursor)
.or_else(|| {
self.panes
.iter()
.zip(&self.contents)
.zip(layout.children())
.filter(|((pane, _content), _layout)| {
self.internal
.maximized()
.is_none_or(|maximized| **pane == maximized)
})
.find_map(|((_pane, content), layout)| {
content.grid_interaction(
layout,
cursor,
on_drag.is_some(),
)
})
})
.unwrap_or(mouse::Interaction::None);
self.contents
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|(((pane, content), tree), layout)| {
let is_picked = picked_pane == Some(pane);
content.on_event(
tree,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
is_picked,
)
})
.fold(event_status, event::Status::merge)
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
self.last_mouse_interaction = Some(interaction);
} else if self.last_mouse_interaction.is_some_and(
|last_mouse_interaction| last_mouse_interaction != interaction,
) {
shell.request_redraw();
}
}
}
fn mouse_interaction(
@ -550,50 +721,26 @@ where
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let action = tree.state.downcast_ref::<state::Action>();
let Memory { action, .. } = tree.state.downcast_ref();
if action.picked_pane().is_some() {
return mouse::Interaction::Grabbing;
if let Some(grid_interaction) =
self.grid_interaction(action, layout, cursor)
{
return grid_interaction;
}
let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
let node = self.contents.layout();
let resize_axis =
action.picked_split().map(|(_, axis)| axis).or_else(|| {
resize_leeway.and_then(|leeway| {
let cursor_position = cursor.position()?;
let bounds = layout.bounds();
let splits =
node.split_regions(self.spacing, bounds.size());
let relative_cursor = Point::new(
cursor_position.x - bounds.x,
cursor_position.y - bounds.y,
);
hovered_split(
splits.iter(),
self.spacing + leeway,
relative_cursor,
)
.map(|(_, axis, _)| axis)
})
});
if let Some(resize_axis) = resize_axis {
return match resize_axis {
Axis::Horizontal => mouse::Interaction::ResizingVertically,
Axis::Vertical => mouse::Interaction::ResizingHorizontally,
};
}
self.contents
self.panes
.iter()
.copied()
.zip(&self.contents)
.zip(&tree.children)
.zip(layout.children())
.map(|(((_pane, content), tree), layout)| {
.filter(|(((pane, _), _), _)| {
self.internal
.maximized()
.is_none_or(|maximized| *pane == maximized)
})
.map(|(((_, content), tree), layout)| {
content.mouse_interaction(
tree,
layout,
@ -617,16 +764,10 @@ where
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let action = tree.state.downcast_ref::<state::Action>();
let node = self.contents.layout();
let Memory { action, .. } = tree.state.downcast_ref();
let node = self.internal.layout();
let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
let contents = self
.contents
.iter()
.zip(&tree.children)
.map(|((pane, content), tree)| (pane, (content, tree)));
let picked_pane = action.picked_pane().filter(|(_, origin)| {
cursor
.position()
@ -695,8 +836,18 @@ where
let style = Catalog::style(theme, &self.class);
for ((id, (content, tree)), pane_layout) in
contents.zip(layout.children())
for (((id, content), tree), pane_layout) in self
.panes
.iter()
.copied()
.zip(&self.contents)
.zip(&tree.children)
.zip(layout.children())
.filter(|(((pane, _), _), _)| {
self.internal
.maximized()
.is_none_or(|maximized| maximized == *pane)
})
{
match picked_pane {
Some((dragging, origin)) if id == dragging => {
@ -829,13 +980,23 @@ where
layout: Layout<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'_, Message, Theme, Renderer>> {
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let children = self
.contents
.iter_mut()
.panes
.iter()
.copied()
.zip(&mut self.contents)
.zip(&mut tree.children)
.zip(layout.children())
.filter_map(|(((_, content), state), layout)| {
.filter_map(|(((pane, content), state), layout)| {
if self
.internal
.maximized()
.is_some_and(|maximized| maximized != pane)
{
return None;
}
content.overlay(state, layout, renderer, translation)
})
.collect::<Vec<_>>();
@ -1084,52 +1245,6 @@ fn hovered_split<'a>(
})
}
/// The visible contents of the [`PaneGrid`]
#[derive(Debug)]
pub enum Contents<'a, T> {
/// All panes are visible
All(Vec<(Pane, T)>, &'a state::Internal),
/// A maximized pane is visible
Maximized(Pane, T, Node),
}
impl<'a, T> Contents<'a, T> {
/// Returns the layout [`Node`] of the [`Contents`]
pub fn layout(&self) -> &Node {
match self {
Contents::All(_, state) => state.layout(),
Contents::Maximized(_, _, layout) => layout,
}
}
/// Returns an iterator over the values of the [`Contents`]
pub fn iter(&self) -> Box<dyn Iterator<Item = (Pane, &T)> + '_> {
match self {
Contents::All(contents, _) => Box::new(
contents.iter().map(|(pane, content)| (*pane, content)),
),
Contents::Maximized(pane, content, _) => {
Box::new(std::iter::once((*pane, content)))
}
}
}
fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (Pane, &mut T)> + '_> {
match self {
Contents::All(contents, _) => Box::new(
contents.iter_mut().map(|(pane, content)| (*pane, content)),
),
Contents::Maximized(pane, content, _) => {
Box::new(std::iter::once((*pane, content)))
}
}
}
fn is_maximized(&self) -> bool {
matches!(self, Self::Maximized(..))
}
}
/// The appearance of a [`PaneGrid`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {

View file

@ -1,12 +1,12 @@
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{self, Tree};
use crate::core::{
self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size,
Vector,
};
use crate::pane_grid::{Draggable, TitleBar};
@ -73,7 +73,7 @@ where
}
}
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Content<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
@ -214,7 +214,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
let body_layout = if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
@ -239,10 +239,10 @@ where
);
}
pub(crate) fn on_event(
pub(crate) fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@ -250,15 +250,13 @@ where
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
is_picked: bool,
) -> event::Status {
let mut event_status = event::Status::Ignored;
) {
let body_layout = if let Some(title_bar) = &mut self.title_bar {
let mut children = layout.children();
event_status = title_bar.on_event(
title_bar.update(
&mut tree.children[1],
event.clone(),
event,
children.next().unwrap(),
cursor,
renderer,
@ -272,10 +270,8 @@ where
layout
};
let body_status = if is_picked {
event::Status::Ignored
} else {
self.body.as_widget_mut().on_event(
if !is_picked {
self.body.as_widget_mut().update(
&mut tree.children[0],
event,
body_layout,
@ -284,10 +280,33 @@ where
clipboard,
shell,
viewport,
)
};
);
}
}
event_status.merge(body_status)
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(
@ -382,8 +401,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Draggable
for &Content<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Draggable
for &Content<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,

View file

@ -0,0 +1,59 @@
use crate::container;
use crate::core::{self, Element};
/// The controls of a [`Pane`].
///
/// [`Pane`]: super::Pane
#[allow(missing_debug_implementations)]
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)
}
}

View file

@ -6,7 +6,8 @@ use crate::pane_grid::{
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
};
use rustc_hash::FxHashMap;
use std::borrow::Cow;
use std::collections::BTreeMap;
/// The state of a [`PaneGrid`].
///
@ -25,17 +26,12 @@ pub struct State<T> {
/// The panes of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub panes: FxHashMap<Pane, T>,
pub panes: BTreeMap<Pane, T>,
/// The internal state of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub internal: Internal,
/// The maximized [`Pane`] of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub(super) maximized: Option<Pane>,
}
impl<T> State<T> {
@ -52,16 +48,12 @@ impl<T> State<T> {
/// Creates a new [`State`] with the given [`Configuration`].
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
let mut panes = FxHashMap::default();
let mut panes = BTreeMap::default();
let internal =
Internal::from_configuration(&mut panes, config.into(), 0);
State {
panes,
internal,
maximized: None,
}
State { panes, internal }
}
/// Returns the total amount of panes in the [`State`].
@ -214,7 +206,7 @@ impl<T> State<T> {
}
let _ = self.panes.insert(new_pane, state);
let _ = self.maximized.take();
let _ = self.internal.maximized.take();
Some((new_pane, new_split))
}
@ -228,8 +220,11 @@ impl<T> State<T> {
) {
if let Some((state, _)) = self.close(pane) {
if 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, new_pane);
self.swap(target, pane);
}
}
}
@ -259,13 +254,27 @@ impl<T> State<T> {
&mut self,
axis: Axis,
pane: Pane,
swap: bool,
inverse: bool,
) {
if let Some((state, _)) = self.close(pane) {
let _ = self.split_node(axis, None, state, swap);
if 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
@ -303,8 +312,8 @@ impl<T> State<T> {
/// Closes the given [`Pane`] and returns its internal state and its closest
/// sibling, if it exists.
pub fn close(&mut self, pane: Pane) -> Option<(T, Pane)> {
if self.maximized == Some(pane) {
let _ = self.maximized.take();
if self.internal.maximized == Some(pane) {
let _ = self.internal.maximized.take();
}
if let Some(sibling) = self.internal.layout.remove(pane) {
@ -319,7 +328,7 @@ impl<T> State<T> {
///
/// [`PaneGrid`]: super::PaneGrid
pub fn maximize(&mut self, pane: Pane) {
self.maximized = Some(pane);
self.internal.maximized = Some(pane);
}
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
@ -327,14 +336,14 @@ impl<T> State<T> {
///
/// [`PaneGrid`]: super::PaneGrid
pub fn restore(&mut self) {
let _ = self.maximized.take();
let _ = self.internal.maximized.take();
}
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
pub fn maximized(&self) -> Option<Pane> {
self.maximized
self.internal.maximized
}
}
@ -345,6 +354,7 @@ impl<T> State<T> {
pub struct Internal {
layout: Node,
last_id: usize,
maximized: Option<Pane>,
}
impl Internal {
@ -353,7 +363,7 @@ impl Internal {
///
/// [`PaneGrid`]: super::PaneGrid
pub fn from_configuration<T>(
panes: &mut FxHashMap<Pane, T>,
panes: &mut BTreeMap<Pane, T>,
content: Configuration<T>,
next_id: usize,
) -> Self {
@ -390,18 +400,34 @@ impl Internal {
}
};
Self { layout, last_id }
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)]
#[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.
///
@ -440,10 +466,3 @@ impl Action {
}
}
}
impl Internal {
/// The layout [`Node`] of the [`Internal`] state
pub fn layout(&self) -> &Node {
&self.layout
}
}

View file

@ -1,14 +1,14 @@
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{self, Tree};
use crate::core::{
self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
Vector,
self, Clipboard, Element, Event, Layout, Padding, Point, Rectangle, Shell,
Size, Vector,
};
use crate::pane_grid::controls::Controls;
/// The title bar of a [`Pane`].
///
@ -24,7 +24,7 @@ pub struct TitleBar<
Renderer: core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
controls: Option<Element<'a, Message, Theme, Renderer>>,
controls: Option<Controls<'a, Message, Theme, Renderer>>,
padding: Padding,
always_show_controls: bool,
class: Theme::Class<'a>,
@ -51,7 +51,7 @@ where
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
controls: impl Into<Element<'a, Message, Theme, Renderer>>,
controls: impl Into<Controls<'a, Message, Theme, Renderer>>,
) -> Self {
self.controls = Some(controls.into());
self
@ -98,16 +98,28 @@ where
}
}
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> TitleBar<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: core::Renderer,
{
pub(super) fn state(&self) -> Tree {
let children = if let Some(controls) = self.controls.as_ref() {
vec![Tree::new(&self.content), Tree::new(controls)]
} else {
vec![Tree::new(&self.content), Tree::empty()]
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 {
@ -117,9 +129,13 @@ where
}
pub(super) fn diff(&self, tree: &mut Tree) {
if tree.children.len() == 2 {
if tree.children.len() == 3 {
if let Some(controls) = self.controls.as_ref() {
tree.children[1].diff(controls);
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);
@ -164,18 +180,42 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
show_title = false;
}
if let Some(compact) = controls.compact.as_ref() {
let compact_layout = children.next().unwrap();
controls.as_widget().draw(
&tree.children[1],
renderer,
theme,
&inherited_style,
controls_layout,
cursor,
viewport,
);
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,
);
}
}
}
@ -207,13 +247,20 @@ where
let mut children = padded.children();
let title_layout = children.next().unwrap();
if self.controls.is_some() {
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
{
!controls_layout.bounds().contains(cursor_position)
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)
@ -244,25 +291,73 @@ where
let title_size = title_layout.size();
let node = if let Some(controls) = &self.controls {
let controls_layout = controls.as_widget().layout(
let controls_layout = controls.full.as_widget().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let controls_size = controls_layout.size();
let space_before_controls = max_size.width - controls_size.width;
if title_layout.bounds().width + controls_layout.bounds().width
> max_size.width
{
if let Some(compact) = controls.compact.as_ref() {
let compact_layout = compact.as_widget().layout(
&mut tree.children[2],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
let height = title_size.height.max(controls_size.height);
let compact_size = compact_layout.size();
let space_before_controls =
max_size.width - compact_size.width;
layout::Node::with_children(
Size::new(max_size.width, height),
vec![
title_layout,
controls_layout
.move_to(Point::new(space_before_controls, 0.0)),
],
)
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),
@ -278,7 +373,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
let mut children = layout.children();
let padded = children.next().unwrap();
@ -293,15 +388,33 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
show_title = false;
}
if let Some(compact) = controls.compact.as_ref() {
let compact_layout = children.next().unwrap();
controls.as_widget().operate(
&mut tree.children[1],
controls_layout,
renderer,
operation,
);
compact.as_widget().operate(
&mut tree.children[2],
compact_layout,
renderer,
operation,
);
} else {
show_title = false;
controls.full.as_widget().operate(
&mut tree.children[1],
controls_layout,
renderer,
operation,
);
}
} else {
controls.full.as_widget().operate(
&mut tree.children[1],
controls_layout,
renderer,
operation,
);
}
};
if show_title {
@ -314,17 +427,17 @@ where
}
}
pub(crate) fn on_event(
pub(crate) fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let mut children = layout.children();
let padded = children.next().unwrap();
@ -332,30 +445,55 @@ where
let title_layout = children.next().unwrap();
let mut show_title = true;
let control_status = if let Some(controls) = &mut self.controls {
if let Some(controls) = &mut self.controls {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
show_title = false;
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,
);
}
}
controls.as_widget_mut().on_event(
&mut tree.children[1],
event.clone(),
controls_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
} else {
event::Status::Ignored
};
let title_status = if show_title {
self.content.as_widget_mut().on_event(
if show_title {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
title_layout,
@ -364,12 +502,8 @@ where
clipboard,
shell,
viewport,
)
} else {
event::Status::Ignored
};
control_status.merge(title_status)
);
}
}
pub(crate) fn mouse_interaction(
@ -396,18 +530,33 @@ where
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
let controls_interaction = controls.as_widget().mouse_interaction(
&tree.children[1],
controls_layout,
cursor,
viewport,
renderer,
);
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
{
controls_interaction
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)
}
@ -444,12 +593,36 @@ where
controls.as_mut().and_then(|controls| {
let controls_layout = children.next()?;
controls.as_widget_mut().overlay(
controls_state,
controls_layout,
renderer,
translation,
)
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,
translation,
)
} else {
controls.full.as_widget_mut().overlay(
controls_state,
controls_layout,
renderer,
translation,
)
}
} else {
controls.full.as_widget_mut().overlay(
controls_state,
controls_layout,
renderer,
translation,
)
}
})
})
}

View file

@ -1,17 +1,79 @@
//! Display a dropdown list of selectable values.
//! 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::event::{self, Event};
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::text::{self, Paragraph as _, Text};
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, Layout, Length, Padding,
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
Background, Border, Clipboard, Color, Element, Event, Layout, Length,
Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
use crate::overlay::menu::{self, Menu};
@ -19,6 +81,67 @@ 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",
/// })
/// }
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct PickList<
'a,
@ -50,6 +173,7 @@ pub struct PickList<
handle: Handle<Renderer::Font>,
class: <Theme as Catalog>::Class<'a>,
menu_class: <Theme as menu::Catalog>::Class<'a>,
last_status: Option<Status>,
}
impl<'a, T, L, V, Message, Theme, Renderer>
@ -80,11 +204,12 @@ where
padding: crate::button::DEFAULT_PADDING,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic,
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,
}
}
@ -161,6 +286,19 @@ where
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]
@ -171,6 +309,17 @@ where
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>
@ -225,6 +374,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
@ -277,23 +427,22 @@ where
layout::Node::new(size)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state =
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
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.
@ -303,7 +452,7 @@ where
shell.publish(on_close.clone());
}
event::Status::Captured
shell.capture_event();
} else if cursor.is_over(layout.bounds()) {
let selected = self.selected.as_ref().map(Borrow::borrow);
@ -318,17 +467,12 @@ where
shell.publish(on_open.clone());
}
event::Status::Captured
} else {
event::Status::Ignored
shell.capture_event();
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state =
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
if state.keyboard_modifiers.command()
&& cursor.is_over(layout.bounds())
&& !state.is_open
@ -345,13 +489,13 @@ where
let options = self.options.borrow();
let selected = self.selected.as_ref().map(Borrow::borrow);
let next_option = if y < 0.0 {
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 {
} else if *y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
@ -365,20 +509,34 @@ where
shell.publish((self.on_select)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
shell.capture_event();
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state =
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
state.keyboard_modifiers = *modifiers;
}
_ => event::Status::Ignored,
_ => {}
};
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();
}
}
@ -407,7 +565,7 @@ where
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let font = self.font.unwrap_or_else(|| renderer.default_font());
@ -415,18 +573,12 @@ where
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
let is_selected = selected.is_some();
let status = if state.is_open {
Status::Opened
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
};
let style = Catalog::style(theme, &self.class, status);
let style = Catalog::style(
theme,
&self.class,
self.last_status.unwrap_or(Status::Active),
);
renderer.fill_quad(
renderer::Quad {
@ -490,6 +642,7 @@ where
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
wrapping: text::Wrapping::default(),
},
Point::new(
bounds.x + bounds.width - self.padding.right,
@ -519,9 +672,10 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
if selected.is_some() {
style.text_color
} else {
style.placeholder_color
@ -598,8 +752,8 @@ struct State<P: text::Paragraph> {
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
options: Vec<P>,
placeholder: P,
options: Vec<paragraph::Plain<P>>,
placeholder: paragraph::Plain<P>,
}
impl<P: text::Paragraph> State<P> {
@ -611,7 +765,7 @@ impl<P: text::Paragraph> State<P> {
is_open: bool::default(),
hovered_option: Option::default(),
options: Vec::new(),
placeholder: P::default(),
placeholder: paragraph::Plain::default(),
}
}
}
@ -674,11 +828,14 @@ pub enum Status {
/// The [`PickList`] is being hovered.
Hovered,
/// The [`PickList`] is open.
Opened,
Opened {
/// Whether the [`PickList`] is hovered, while open.
is_hovered: bool,
},
}
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The text [`Color`] of the pick list.
pub text_color: Color,
@ -748,7 +905,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
match status {
Status::Active => active,
Status::Hovered | Status::Opened => Style {
Status::Hovered | Status::Opened { .. } => Style {
border: Border {
color: palette.primary.strong.color,
..active.border

270
widget/src/pin.rs Normal file
View file

@ -0,0 +1,270 @@
//! 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()
/// }
/// ```
#[allow(missing_debug_implementations)]
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(
&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()
.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(
&self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content.as_widget().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<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
tree,
layout.children().next().unwrap(),
renderer,
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)
}
}

319
widget/src/pop.rs Normal file
View file

@ -0,0 +1,319 @@
//! 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::text;
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!
#[allow(missing_debug_implementations)]
pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
content: Element<'a, Message, Theme, Renderer>,
key: Option<text::Fragment<'a>>,
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> Pop<'a, Message, Theme, Renderer>
where
Renderer: core::Renderer,
Message: Clone,
{
/// Creates a new [`Pop`] widget with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
content: content.into(),
key: None,
on_show: None,
on_resize: None,
on_hide: None,
anticipate: Pixels::ZERO,
delay: Duration::ZERO,
}
}
/// 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 [`Pop`] widget, for continuity.
///
/// If the key changes, the [`Pop`] widget will trigger again.
pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self {
self.key = Some(key.into_fragment());
self
}
/// 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, Default)]
struct State {
has_popped_in: bool,
should_notify_at: Option<(bool, Instant)>,
last_size: Option<Size>,
last_key: Option<String>,
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Pop<'_, Message, Theme, Renderer>
where
Message: Clone,
Renderer: core::Renderer,
{
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(&[&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>();
if state.has_popped_in
&& state.last_key.as_deref() != self.key.as_deref()
{
state.has_popped_in = false;
state.should_notify_at = None;
state.last_key =
self.key.as_ref().cloned().map(text::Fragment::into_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 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 self.on_show.is_some() && 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 {
shell.publish(on_hide.clone());
}
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(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content
.as_widget()
.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(
&self,
tree: &mut Tree,
layout: core::Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
self.content.as_widget().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<'_>,
renderer: &Renderer,
translation: core::Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
translation,
)
}
}
impl<'a, Message, Theme, Renderer> From<Pop<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: core::Renderer + 'a,
Theme: 'a,
Message: Clone + 'a,
{
fn from(pop: Pop<'a, Message, Theme, Renderer>) -> Self {
Element::new(pop)
}
}

View file

@ -1,10 +1,31 @@
//! Provide progress feedback to your users.
//! 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, Border, Element, Layout, Length, Rectangle, Size, Theme,
self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
Widget,
};
@ -14,14 +35,23 @@ use std::ops::RangeInclusive;
///
/// # Example
/// ```no_run
/// # type ProgressBar<'a> = iced_widget::ProgressBar<'a>;
/// # 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>;
/// #
/// let value = 50.0;
/// use iced::widget::progress_bar;
///
/// ProgressBar::new(0.0..=100.0, value);
/// struct State {
/// progress: f32,
/// }
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// progress_bar(0.0..=100.0, state.progress).into()
/// }
/// ```
///
/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
#[allow(missing_debug_implementations)]
pub struct ProgressBar<'a, Theme = crate::Theme>
where
@ -29,8 +59,9 @@ where
{
range: RangeInclusive<f32>,
value: f32,
width: Length,
height: Option<Length>,
length: Length,
girth: Length,
is_vertical: bool,
class: Theme::Class<'a>,
}
@ -38,8 +69,8 @@ impl<'a, Theme> ProgressBar<'a, Theme>
where
Theme: Catalog,
{
/// The default height of a [`ProgressBar`].
pub const DEFAULT_HEIGHT: f32 = 30.0;
/// The default girth of a [`ProgressBar`].
pub const DEFAULT_GIRTH: f32 = 30.0;
/// Creates a new [`ProgressBar`].
///
@ -50,21 +81,30 @@ where
ProgressBar {
value: value.clamp(*range.start(), *range.end()),
range,
width: Length::Fill,
height: None,
length: Length::Fill,
girth: Length::from(Self::DEFAULT_GIRTH),
is_vertical: false,
class: Theme::default(),
}
}
/// Sets the width of the [`ProgressBar`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
pub fn length(mut self, length: impl Into<Length>) -> Self {
self.length = length.into();
self
}
/// Sets the height of the [`ProgressBar`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = Some(height.into());
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
}
@ -85,18 +125,34 @@ where
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<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ProgressBar<'a, Theme>
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.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
width: self.width(),
height: self.height(),
}
}
@ -106,11 +162,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::atomic(
limits,
self.width,
self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
)
layout::atomic(limits, self.width(), self.height())
}
fn draw(
@ -126,11 +178,16 @@ where
let bounds = layout.bounds();
let (range_start, range_end) = self.range.clone().into_inner();
let active_progress_width = if range_start >= range_end {
let length = if self.is_vertical {
bounds.height
} else {
bounds.width
};
let active_progress_length = if range_start >= range_end {
0.0
} else {
bounds.width * (self.value - range_start)
/ (range_end - range_start)
length * (self.value - range_start) / (range_end - range_start)
};
let style = theme.style(&self.class);
@ -144,14 +201,27 @@ where
style.background,
);
if active_progress_width > 0.0 {
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: Rectangle {
width: active_progress_width,
..bounds
bounds,
border: Border {
color: Color::TRANSPARENT,
..style.border
},
border: Border::rounded(style.border.radius),
..renderer::Quad::default()
},
style.bar,
@ -175,7 +245,7 @@ where
}
/// The appearance of a progress bar.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the progress bar.
pub background: Background,
@ -218,10 +288,7 @@ impl Catalog for Theme {
pub fn primary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(
palette.background.strong.color,
palette.primary.strong.color,
)
styled(palette.background.strong.color, palette.primary.base.color)
}
/// The secondary style of a [`ProgressBar`].
@ -241,6 +308,13 @@ pub fn success(theme: &Theme) -> Style {
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();
@ -255,6 +329,6 @@ fn styled(
Style {
background: background.into(),
bar: bar.into(),
border: Border::rounded(2),
border: border::rounded(2),
}
}

View file

@ -1,30 +1,72 @@
//! Encode and display information in a QR code.
//! 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, Point, Rectangle, Size, Theme, Vector,
Widget,
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
Vector, Widget,
};
use crate::Renderer;
use std::cell::RefCell;
use thiserror::Error;
const DEFAULT_CELL_SIZE: u16 = 4;
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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct QRCode<'a, Theme = crate::Theme>
where
Theme: Catalog,
{
data: &'a Data,
cell_size: u16,
cell_size: f32,
class: Theme::Class<'a>,
}
@ -42,8 +84,16 @@ where
}
/// Sets the size of the squares of the grid cell of the [`QRCode`].
pub fn cell_size(mut self, cell_size: u16) -> Self {
self.cell_size = cell_size;
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
}
@ -66,7 +116,7 @@ where
}
}
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme>
impl<Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'_, Theme>
where
Theme: Catalog,
{
@ -91,8 +141,8 @@ where
_renderer: &Renderer,
_limits: &layout::Limits,
) -> layout::Node {
let side_length = (self.data.width + 2 * QUIET_ZONE) as f32
* f32::from(self.cell_size);
let side_length =
(self.data.width + 2 * QUIET_ZONE) as f32 * self.cell_size;
layout::Node::new(Size::new(side_length, side_length))
}

View file

@ -1,6 +1,63 @@
//! Create choices using radio buttons.
//! 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::event::{self, Event};
use crate::core::border::{self, Border};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@ -8,8 +65,9 @@ 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, Layout, Length, Pixels,
Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
Rectangle, Shell, Size, Theme, Widget,
};
@ -17,54 +75,59 @@ use crate::core::{
///
/// # Example
/// ```no_run
/// # type Radio<'a, Message> =
/// # iced_widget::Radio<'a, Message, iced_widget::Theme, iced_widget::renderer::Renderer>;
/// # 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;
/// use iced::widget::{column, radio};
///
/// struct State {
/// selection: Option<Choice>,
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// enum Message {
/// RadioSelected(Choice),
/// }
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// pub enum Choice {
/// enum Choice {
/// A,
/// B,
/// C,
/// All,
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Message {
/// RadioSelected(Choice),
/// 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()
/// }
///
/// let selected_choice = Some(Choice::A);
///
/// let a = Radio::new(
/// "A",
/// Choice::A,
/// selected_choice,
/// Message::RadioSelected,
/// );
///
/// let b = Radio::new(
/// "B",
/// Choice::B,
/// selected_choice,
/// Message::RadioSelected,
/// );
///
/// let c = Radio::new(
/// "C",
/// Choice::C,
/// selected_choice,
/// Message::RadioSelected,
/// );
///
/// let all = Radio::new(
/// "All of the above",
/// Choice::All,
/// selected_choice,
/// Message::RadioSelected
/// );
///
/// let content = column![a, b, c, all];
/// ```
#[allow(missing_debug_implementations)]
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
@ -81,8 +144,10 @@ where
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>
@ -104,7 +169,7 @@ where
/// * 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`.
/// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>(
label: impl Into<String>,
value: V,
@ -121,12 +186,14 @@ where
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING, //15
spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic,
text_shaping: text::Shaping::default(),
text_wrapping: text::Wrapping::default(),
font: None,
class: Theme::default(),
last_status: None,
}
}
@ -169,6 +236,12 @@ where
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());
@ -194,8 +267,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Radio<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Radio<'_, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
@ -244,35 +317,53 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
self.text_wrapping,
)
},
)
}
fn on_event(
fn update(
&mut self,
_state: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
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());
return event::Status::Captured;
shell.capture_event();
}
}
_ => {}
}
event::Status::Ignored
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(
@ -297,21 +388,17 @@ where
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let is_mouse_over = cursor.is_over(layout.bounds());
let is_selected = self.is_selected;
let mut children = layout.children();
let status = if is_mouse_over {
Status::Hovered { is_selected }
} else {
Status::Active { is_selected }
};
let style = theme.style(&self.class, status);
let style = theme.style(
&self.class,
self.last_status.unwrap_or(Status::Active {
is_selected: self.is_selected,
}),
);
{
let layout = children.next().unwrap();
@ -342,7 +429,7 @@ where
width: bounds.width - dot_size,
height: bounds.height - dot_size,
},
border: Border::rounded(dot_size / 2.0),
border: border::rounded(dot_size / 2.0),
..renderer::Quad::default()
},
style.dot_color,
@ -352,12 +439,14 @@ where
{
let label_layout = children.next().unwrap();
let state: &widget::text::State<Renderer::Paragraph> =
tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout,
tree.state.downcast_ref(),
state.0.raw(),
crate::text::Style {
color: style.text_color,
},
@ -397,7 +486,7 @@ pub enum Status {
}
/// The appearance of a radio button.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The [`Background`] of the radio button.
pub background: Background,

View file

@ -1,23 +1,44 @@
//! Distribute content horizontally.
use crate::core::event::{self, Event};
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::{
Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell,
Size, Vector, Widget,
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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
height: Length,
align_items: Alignment,
align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
@ -60,7 +81,7 @@ where
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
align_items: Alignment::Start,
align: Alignment::Start,
clip: false,
children,
}
@ -95,8 +116,8 @@ where
}
/// Sets the vertical alignment of the contents of the [`Row`] .
pub fn align_items(mut self, align: Alignment) -> Self {
self.align_items = align;
pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
self.align = Alignment::from(align.into());
self
}
@ -141,9 +162,16 @@ where
) -> 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 }
}
}
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer>
impl<Message, Renderer> Default for Row<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
@ -152,8 +180,21 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
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,
{
@ -186,7 +227,7 @@ where
self.height,
self.padding,
self.spacing,
self.align_items,
self.align,
&self.children,
&mut tree.children,
)
@ -197,7 +238,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@ -212,34 +253,28 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.children
) {
for ((child, state), layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
{
child.as_widget_mut().update(
state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
}
fn mouse_interaction(
@ -274,24 +309,21 @@ where
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
for ((child, state), layout) in self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().intersects(viewport))
{
child.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor,
if self.clip {
&clipped_viewport
} else {
viewport
},
state, renderer, theme, style, layout, cursor, viewport,
);
}
}
@ -325,3 +357,196 @@ where
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.
#[allow(missing_debug_implementations)]
pub struct Wrapping<
'a,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> {
row: Row<'a, Message, Theme, Renderer>,
}
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(
&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 spacing = self.row.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 = |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().enumerate() {
let node = child.as_widget().layout(
&mut tree.children[i],
renderer,
&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(row_start..i, row_height, &mut children);
y += row_height + 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(row_start..children.len(), row_height, &mut children);
let size =
limits.resolve(self.row.width, self.row.height, intrinsic_size);
layout::Node::with_children(size.expand(self.row.padding), children)
}
fn operate(
&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<'_>,
renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.row.overlay(tree, layout, renderer, 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)
}
}

View file

@ -1,6 +1,23 @@
//! Display a horizontal or vertical rule for dividing content.
//! 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::horizontal_rule;
//!
//! #[derive(Clone)]
//! enum Message {
//! // ...,
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! horizontal_rule(2).into()
//! }
//! ```
use crate::core;
use crate::core::border::{self, Border};
use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@ -10,6 +27,23 @@ use crate::core::{
};
/// 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::horizontal_rule;
///
/// #[derive(Clone)]
/// enum Message {
/// // ...,
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// horizontal_rule(2).into()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Rule<'a, Theme = crate::Theme>
where
@ -64,8 +98,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Rule<'a, Theme>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Rule<'_, Theme>
where
Renderer: core::Renderer,
Theme: Catalog,
@ -132,7 +166,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border::rounded(style.radius),
border: border::rounded(style.radius),
..renderer::Quad::default()
},
style.color,
@ -153,7 +187,7 @@ where
}
/// The appearance of a rule.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The color of the rule.
pub color: Color,
@ -166,7 +200,7 @@ pub struct Style {
}
/// The fill mode of a rule.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FillMode {
/// Fill the whole length of the container.
Full,

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,21 @@
//! A custom shader widget for wgpu applications.
mod event;
mod program;
pub use event::Event;
pub use program::Program;
use crate::core;
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::window;
use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size};
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 crate::renderer::wgpu::wgpu;
pub use primitive::{Primitive, Storage};
/// A widget which can render custom shaders with Iced's `wgpu` backend.
@ -88,50 +85,35 @@ where
layout::atomic(limits, self.width, self.height)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: crate::core::Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let bounds = layout.bounds();
let custom_shader_event = match event {
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
core::Event::Keyboard(keyboard_event) => {
Some(Event::Keyboard(keyboard_event))
}
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
core::Event::Window(_, window::Event::RedrawRequested(instant)) => {
Some(Event::RedrawRequested(instant))
}
_ => None,
};
let state = tree.state.downcast_mut::<P::State>();
if let Some(custom_shader_event) = custom_shader_event {
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();
let (event_status, message) = self.program.update(
state,
custom_shader_event,
bounds,
cursor,
shell,
);
shell.request_redraw_at(redraw_request);
if let Some(message) = message {
shell.publish(message);
}
return event_status;
if event_status == event::Status::Captured {
shell.capture_event();
}
}
event::Status::Ignored
}
fn mouse_interaction(
@ -192,12 +174,11 @@ where
fn update(
&self,
state: &mut Self::State,
event: Event,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) -> (event::Status, Option<Message>) {
T::update(self, state, event, bounds, cursor, shell)
) -> Option<Action<Message>> {
T::update(self, state, event, bounds, cursor)
}
fn draw(

View file

@ -1,25 +0,0 @@
//! Handle events of a custom shader widget.
use crate::core::keyboard;
use crate::core::mouse;
use crate::core::time::Instant;
use crate::core::touch;
pub use crate::core::event::Status;
/// A [`Shader`] event.
///
/// [`Shader`]: crate::Shader
#[derive(Debug, Clone, PartialEq)]
pub enum Event {
/// A mouse event.
Mouse(mouse::Event),
/// A touch event.
Touch(touch::Event),
/// A keyboard event.
Keyboard(keyboard::Event),
/// A window requested a redraw.
RedrawRequested(Instant),
}

View file

@ -1,8 +1,7 @@
use crate::core::event;
use crate::core::Rectangle;
use crate::core::mouse;
use crate::core::{Rectangle, Shell};
use crate::renderer::wgpu::Primitive;
use crate::shader;
use crate::shader::{self, Action};
/// The state and logic of a [`Shader`] widget.
///
@ -18,21 +17,20 @@ pub trait Program<Message> {
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 use the [`Shell`] to publish messages, request a
/// redraw for the window, etc.
/// 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 does and returns nothing.
/// By default, this method returns `None`.
///
/// [`State`]: Self::State
fn update(
&self,
_state: &mut Self::State,
_event: shader::Event,
_event: &shader::Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
_shell: &mut Shell<'_, Message>,
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
) -> Option<Action<Message>> {
None
}
/// Draws the [`Primitive`].

View file

@ -1,6 +1,34 @@
//! Display an interactive selector of a single value from a range of values.
use crate::core::border;
use crate::core::event::{self, Event};
//! 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;
@ -8,9 +36,10 @@ 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, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Rectangle, Shell, Size, Theme, Widget,
self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels,
Point, Rectangle, Shell, Size, Theme, Widget,
};
use std::ops::RangeInclusive;
@ -25,19 +54,32 @@ use std::ops::RangeInclusive;
///
/// # Example
/// ```no_run
/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>;
/// # 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>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
/// SliderChanged(f32),
/// use iced::widget::slider;
///
/// struct State {
/// value: f32,
/// }
///
/// let value = 50.0;
/// #[derive(Debug, Clone)]
/// enum Message {
/// ValueChanged(f32),
/// }
///
/// Slider::new(0.0..=100.0, value, Message::SliderChanged);
/// 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;
/// }
/// }
/// }
/// ```
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
pub struct Slider<'a, T, Message, Theme = crate::Theme>
where
@ -53,6 +95,7 @@ where
width: Length,
height: f32,
class: Theme::Class<'a>,
status: Option<Status>,
}
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
@ -70,8 +113,8 @@ where
/// * 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`.
/// 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,
@ -99,6 +142,7 @@ where
width: Length::Fill,
height: Self::DEFAULT_HEIGHT,
class: Theme::default(),
status: None,
}
}
@ -166,8 +210,8 @@ where
}
}
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Slider<'a, T, Message, Theme>
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Slider<'_, T, Message, Theme>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
@ -198,29 +242,53 @@ where
layout::atomic(limits, self.width, self.height)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
let is_dragging = state.is_dragging;
let current_value = self.value;
let mut update = || {
let current_value = self.value;
let locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
let new_value = 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 locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
let new_value = 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))
};
new_value
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
@ -228,150 +296,158 @@ where
}
.into();
let start = (*self.range.start()).into();
let end = (*self.range.end()).into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
let percent = f64::from(cursor_position.x - bounds.x)
/ f64::from(bounds.width);
if new_value > (*self.range.end()).into() {
return Some(*self.range.end());
}
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value)
T::from_f64(new_value)
};
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 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);
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());
}
if new_value > (*self.range.end()).into() {
return Some(*self.range.end());
}
T::from_f64(new_value)
};
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));
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
self.value = new_value;
}
};
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
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;
}
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);
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;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
shell.capture_event();
}
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = self.on_release.clone() {
shell.publish(on_release);
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if state.is_dragging {
let _ = cursor.position().and_then(locate).map(change);
shell.capture_event();
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ = cursor.position().and_then(locate).map(change);
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,
};
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if cursor.position_over(layout.bounds()).is_some() {
match key {
Key::Named(key::Named::ArrowUp) => {
if *delta < 0.0 {
let _ = decrement(current_value).map(change);
} else {
let _ = increment(current_value).map(change);
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
}
_ => (),
shell.capture_event();
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {}
}
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);
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
}
_ => (),
}
event::Status::Ignored
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,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
let style = theme.style(
&self.class,
if state.is_dragging {
Status::Dragged
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
},
);
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Active));
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
@ -408,10 +484,10 @@ where
width: offset + handle_width / 2.0,
height: style.rail.width,
},
border: Border::rounded(style.rail.border_radius),
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.colors.0,
style.rail.backgrounds.0,
);
renderer.fill_quad(
@ -422,10 +498,10 @@ where
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
border: Border::rounded(style.rail.border_radius),
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.colors.1,
style.rail.backgrounds.1,
);
renderer.fill_quad(
@ -443,7 +519,7 @@ where
},
..renderer::Quad::default()
},
style.handle.color,
style.handle.background,
);
}
@ -502,7 +578,7 @@ pub enum Status {
}
/// The appearance of a slider.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The colors of the rail of the slider.
pub rail: Rail,
@ -522,23 +598,23 @@ impl Style {
}
/// The appearance of a slider rail
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rail {
/// The colors of the rail of the slider.
pub colors: (Color, Color),
/// 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 radius of the corners of the rail.
pub border_radius: border::Radius,
/// The border of the rail.
pub border: Border,
}
/// The appearance of the handle of a slider.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
/// The [`Color`] of the handle.
pub color: Color,
/// The [`Background`] of the handle.
pub background: Background,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
@ -546,7 +622,7 @@ pub struct Handle {
}
/// The shape of the handle of a slider.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HandleShape {
/// A circular handle.
Circle {
@ -594,20 +670,24 @@ pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let color = match status {
Status::Active => palette.primary.strong.color,
Status::Hovered => palette.primary.base.color,
Status::Dragged => palette.primary.strong.color,
Status::Active => palette.primary.base.color,
Status::Hovered => palette.primary.strong.color,
Status::Dragged => palette.primary.weak.color,
};
Style {
rail: Rail {
colors: (color, palette.secondary.base.color),
backgrounds: (color.into(), palette.background.strong.color.into()),
width: 4.0,
border_radius: 2.0.into(),
border: Border {
radius: 2.0.into(),
width: 0.0,
color: Color::TRANSPARENT,
},
},
handle: Handle {
shape: HandleShape::Circle { radius: 7.0 },
color,
background: color.into(),
border_color: Color::TRANSPARENT,
border_width: 0.0,
},

View file

@ -1,12 +1,12 @@
//! Display content on top of other content.
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector,
Widget,
};
/// A container that displays children on top of each other.
@ -116,7 +116,7 @@ where
}
}
impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer>
impl<Message, Renderer> Default for Stack<'_, Message, Renderer>
where
Renderer: crate::core::Renderer,
{
@ -189,7 +189,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@ -204,36 +204,47 @@ where
});
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
mut cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.children
) {
let is_over = cursor.is_over(layout.bounds());
let end = self.children.len() - 1;
for (i, ((child, state), layout)) in self
.children
.iter_mut()
.rev()
.zip(tree.children.iter_mut().rev())
.zip(layout.children().rev())
.map(|((child, state), layout)| {
child.as_widget_mut().on_event(
state,
event.clone(),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
})
.find(|&status| status == event::Status::Captured)
.unwrap_or(event::Status::Ignored)
.enumerate()
{
child.as_widget_mut().update(
state, 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(
state, layout, cursor, viewport, renderer,
);
if interaction != mouse::Interaction::None {
cursor = cursor.levitate();
}
}
}
}
fn mouse_interaction(
@ -269,15 +280,53 @@ where
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
for (i, ((layer, state), layout)) in self
let layers_below = if cursor.is_over(layout.bounds()) {
self.children
.iter()
.rev()
.zip(tree.children.iter().rev())
.zip(layout.children().rev())
.position(|((layer, state), layout)| {
let interaction = layer.as_widget().mouse_interaction(
state, 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()
{
if i > 0 {
renderer.with_layer(clipped_viewport, |renderer| {
.enumerate();
let layers = layers.by_ref();
let mut draw_layer =
|i,
layer: &Element<'a, Message, Theme, Renderer>,
state,
layout,
cursor| {
if i > 0 {
renderer.with_layer(clipped_viewport, |renderer| {
layer.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor,
&clipped_viewport,
);
});
} else {
layer.as_widget().draw(
state,
renderer,
@ -287,18 +336,15 @@ where
cursor,
&clipped_viewport,
);
});
} else {
layer.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor,
&clipped_viewport,
);
}
}
};
for (i, ((layer, state), layout)) in layers.take(layers_below) {
draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable);
}
for (i, ((layer, state), layout)) in layers {
draw_layer(i, layer, state, layout, cursor);
}
}
}

View file

@ -1,4 +1,20 @@
//! Display vector graphics in your application.
//! 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;
@ -19,6 +35,22 @@ pub use crate::core::svg::Handle;
///
/// [`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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Svg<'a, Theme = crate::Theme>
where
@ -116,8 +148,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Svg<'a, Theme>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Svg<'_, Theme>
where
Renderer: svg::Renderer,
Theme: Catalog,
@ -211,11 +243,13 @@ where
let render = |renderer: &mut Renderer| {
renderer.draw_svg(
self.handle.clone(),
style.color,
svg::Svg {
handle: self.handle.clone(),
color: style.color,
rotation: self.rotation.radians(),
opacity: self.opacity,
},
drawing_bounds,
self.rotation.radians(),
self.opacity,
);
};
@ -289,7 +323,7 @@ impl Catalog for Theme {
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
impl<Theme> From<Style> for StyleFn<'_, Theme> {
fn from(style: Style) -> Self {
Box::new(move |_theme, _status| style)
}

View file

@ -1,6 +1,30 @@
//! Draw and interact with text.
pub use crate::core::widget::text::*;
mod rich;
/// A paragraph.
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>;

566
widget/src/text/rich.rs Normal file
View file

@ -0,0 +1,566 @@
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, 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.
#[allow(missing_debug_implementations)]
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::Horizontal,
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::Horizontal::Left,
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::Horizontal>,
) -> 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.
pub fn on_link_click(
mut self,
on_link_clicked: impl Fn(Link) -> Message + 'a,
) -> Self {
self.on_link_click = Some(Box::new(on_link_clicked));
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(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
tree.state
.downcast_mut::<State<Link, Renderer::Paragraph>>(),
renderer,
limits,
self.width,
self.height,
self.spans.as_ref().as_ref(),
self.line_height,
self.size,
self.font,
self.align_x,
self.align_y,
self.wrapping,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
defaults: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if !layout.bounds().intersects(viewport) {
return;
}
let state = tree
.state
.downcast_ref::<State<Link, Renderer::Paragraph>>();
let style = theme.style(&self.class);
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
let is_hovered_link = self.on_link_click.is_some()
&& Some(index) == self.hovered_link;
if span.highlight.is_some()
|| span.underline
|| span.strikethrough
|| is_hovered_link
{
let translation = layout.position() - Point::ORIGIN;
let regions = state.paragraph.span_bounds(index);
if let Some(highlight) = span.highlight {
for bounds in &regions {
let bounds = Rectangle::new(
bounds.position()
- Vector::new(
span.padding.left,
span.padding.top,
),
bounds.size()
+ Size::new(
span.padding.horizontal(),
span.padding.vertical(),
),
);
renderer.fill_quad(
renderer::Quad {
bounds: bounds + translation,
border: highlight.border,
..Default::default()
},
highlight.background,
);
}
}
if span.underline || span.strikethrough || is_hovered_link {
let size = span
.size
.or(self.size)
.unwrap_or(renderer.default_size());
let line_height = span
.line_height
.unwrap_or(self.line_height)
.to_absolute(size);
let color = span
.color
.or(style.color)
.unwrap_or(defaults.text_color);
let baseline = translation
+ Vector::new(
0.0,
size.0 + (line_height.0 - size.0) / 2.0,
);
if span.underline || is_hovered_link {
for bounds in &regions {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle::new(
bounds.position() + baseline
- Vector::new(0.0, size.0 * 0.08),
Size::new(bounds.width, 1.0),
),
..Default::default()
},
color,
);
}
}
if span.strikethrough {
for bounds in &regions {
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle::new(
bounds.position() + baseline
- Vector::new(0.0, size.0 / 2.0),
Size::new(bounds.width, 1.0),
),
..Default::default()
},
color,
);
}
}
}
}
}
text::draw(
renderer,
defaults,
layout,
&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>,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: 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,
horizontal_alignment,
vertical_alignment,
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,
horizontal_alignment,
vertical_alignment,
shaping: Shaping::Advanced,
wrapping,
}) {
core::text::Difference::None => {}
core::text::Difference::Bounds => {
state.paragraph.resize(bounds);
}
core::text::Difference::Shape => {
state.paragraph =
Renderer::Paragraph::with_spans(text_with_spans());
}
}
}
state.paragraph.min_bounds()
})
}
impl<'a, Link, Message, Theme, Renderer>
FromIterator<Span<'a, Link, Renderer::Font>>
for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
Renderer: core::text::Renderer,
Renderer::Font: 'a,
{
fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
spans: T,
) -> Self {
Self::with_spans(spans.into_iter().collect::<Vec<_>>())
}
}
impl<'a, Link, Message, Theme, Renderer>
From<Rich<'a, Link, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Link: Clone + 'a,
Theme: Catalog + 'a,
Renderer: core::text::Renderer + 'a,
{
fn from(
text: Rich<'a, Link, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
Element::new(text)
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,13 @@
use crate::text_input::Value;
/// The cursor of a text input.
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Cursor {
state: State,
}
/// The state of a [`Cursor`].
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum State {
/// Cursor without a selection
Index(usize),

View file

@ -1,14 +1,13 @@
use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle,
Shell, Size, Vector, Widget,
Background, Clipboard, Color, Element, Event, Layout, Length, Point,
Rectangle, Shell, Size, Vector, Widget,
};
use std::marker::PhantomData;
@ -64,8 +63,8 @@ where
}
}
impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer>
for Themer<'a, Message, Theme, NewTheme, F, Renderer>
impl<Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer>
for Themer<'_, Message, Theme, NewTheme, F, Renderer>
where
F: Fn(&Theme) -> NewTheme,
Renderer: crate::core::Renderer,
@ -104,27 +103,27 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
self.content
.as_widget()
.operate(tree, layout, renderer, operation);
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.content.as_widget_mut().on_event(
) {
self.content.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
)
);
}
fn mouse_interaction(
@ -188,9 +187,9 @@ where
content: overlay::Element<'a, Message, NewTheme, Renderer>,
}
impl<'a, Message, Theme, NewTheme, Renderer>
impl<Message, Theme, NewTheme, Renderer>
overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, Message, Theme, NewTheme, Renderer>
for Overlay<'_, Message, Theme, NewTheme, Renderer>
where
Renderer: crate::core::Renderer,
{
@ -219,24 +218,24 @@ where
);
}
fn on_event(
fn update(
&mut self,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
) {
self.content
.on_event(event, layout, cursor, renderer, clipboard, shell)
.update(event, layout, cursor, renderer, clipboard, shell);
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
operation: &mut dyn Operation,
) {
self.content.operate(layout, renderer, operation);
}

View file

@ -1,6 +1,36 @@
//! Show toggle controls using togglers.
//! 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::event;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@ -8,6 +38,7 @@ 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::{
Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
Rectangle, Shell, Size, Theme, Widget,
@ -16,17 +47,34 @@ use crate::core::{
/// A toggler widget.
///
/// # Example
///
/// ```no_run
/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>;
/// # 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>;
/// #
/// pub enum Message {
/// use iced::widget::toggler;
///
/// struct State {
/// is_checked: bool,
/// }
///
/// enum Message {
/// TogglerToggled(bool),
/// }
///
/// let is_toggled = true;
/// fn view(state: &State) -> Element<'_, Message> {
/// toggler(state.is_checked)
/// .label("Toggle me!")
/// .on_toggle(Message::TogglerToggled)
/// .into()
/// }
///
/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
/// fn update(state: &mut State, message: Message) {
/// match message {
/// Message::TogglerToggled(is_checked) => {
/// state.is_checked = is_checked;
/// }
/// }
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Toggler<
@ -39,17 +87,19 @@ pub struct Toggler<
Renderer: text::Renderer,
{
is_toggled: bool,
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
label: Option<String>,
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: alignment::Horizontal,
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>
@ -68,30 +118,55 @@ where
/// * a function that will be called when the [`Toggler`] is toggled. It
/// will receive the new state of the [`Toggler`] and must produce a
/// `Message`.
pub fn new<F>(
label: impl Into<Option<String>>,
is_toggled: bool,
f: F,
) -> Self
where
F: 'a + Fn(bool) -> Message,
{
pub fn new(is_toggled: bool) -> Self {
Toggler {
is_toggled,
on_toggle: Box::new(f),
label: label.into(),
width: Length::Fill,
on_toggle: None,
label: None,
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
text_size: None,
text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left,
text_shaping: text::Shaping::Basic,
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;
@ -131,6 +206,12 @@ where
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;
@ -164,8 +245,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Toggler<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Toggler<'_, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
@ -216,6 +297,7 @@ where
self.text_alignment,
alignment::Vertical::Top,
self.text_shaping,
self.text_wrapping,
)
} else {
layout::Node::new(Size::ZERO)
@ -224,31 +306,53 @@ where
)
}
fn on_event(
fn update(
&mut self,
_state: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
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((self.on_toggle)(!self.is_toggled));
event::Status::Captured
} else {
event::Status::Ignored
shell.publish(on_toggle(!self.is_toggled));
shell.capture_event();
}
}
_ => event::Status::Ignored,
_ => {}
}
let current_status = if self.on_toggle.is_none() {
Status::Disabled
} 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();
}
}
@ -261,7 +365,11 @@ where
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
if self.on_toggle.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::NotAllowed
}
} else {
mouse::Interaction::default()
}
@ -274,7 +382,7 @@ where
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
/// Makes sure that the border radius of the toggler looks good at every size.
@ -289,31 +397,22 @@ where
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,
style,
label_layout,
tree.state.downcast_ref(),
state.0.raw(),
crate::text::Style::default(),
viewport,
);
}
let bounds = toggler_layout.bounds();
let is_mouse_over = cursor.is_over(layout.bounds());
let status = if is_mouse_over {
Status::Hovered {
is_toggled: self.is_toggled,
}
} else {
Status::Active {
is_toggled: self.is_toggled,
}
};
let style = theme.style(&self.class, status);
let style = theme
.style(&self.class, self.last_status.unwrap_or(Status::Disabled));
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
let space = SPACE_RATIO * bounds.height;
@ -392,10 +491,12 @@ pub enum Status {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
/// The [`Toggler`] is disabled.
Disabled,
}
/// The appearance of a toggler.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
/// The background [`Color`] of the toggler.
pub background: Color,
@ -452,6 +553,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.strong.color
}
}
Status::Disabled => palette.background.weak.color,
};
let foreground = match status {
@ -472,6 +574,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.weak.color
}
}
Status::Disabled => palette.background.base.color,
};
Style {

View file

@ -1,6 +1,27 @@
//! Display a widget over another.
//! Tooltips display a hint of information over some element when hovered.
//!
//! # 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::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
@ -8,11 +29,33 @@ use crate::core::renderer;
use crate::core::text;
use crate::core::widget::{self, Widget};
use crate::core::{
Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
Vector,
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()
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct Tooltip<
'a,
@ -99,8 +142,8 @@ where
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Tooltip<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Tooltip<'_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: text::Renderer,
@ -146,17 +189,17 @@ where
.layout(&mut tree.children[0], renderer, limits)
}
fn on_event(
fn update(
&mut self,
tree: &mut widget::Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
let was_idle = *state == State::Idle;
@ -170,9 +213,12 @@ where
if was_idle != is_idle {
shell.invalidate_layout();
shell.request_redraw();
} else if !is_idle && self.position == Position::FollowCursor {
shell.request_redraw();
}
self.content.as_widget_mut().on_event(
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
@ -181,7 +227,7 @@ where
clipboard,
shell,
viewport,
)
);
}
fn mouse_interaction(
@ -326,9 +372,8 @@ where
class: &'b Theme::Class<'a>,
}
impl<'a, 'b, Message, Theme, Renderer>
overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, 'b, Message, Theme, Renderer>
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'_, '_, Message, Theme, Renderer>
where
Theme: container::Catalog,
Renderer: text::Renderer,
@ -425,8 +470,10 @@ where
layout::Node::with_children(
tooltip_bounds.size(),
vec![tooltip_layout
.translate(Vector::new(self.padding, self.padding))],
vec![
tooltip_layout
.translate(Vector::new(self.padding, self.padding)),
],
)
.translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
}

View file

@ -1,11 +1,40 @@
//! Display an interactive selector of a single value from a range of values.
//! 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::{
default, Catalog, Handle, HandleShape, Status, Style, StyleFn,
Catalog, Handle, HandleShape, Status, Style, StyleFn, default,
};
use crate::core::event::{self, Event};
use crate::core::border::Border;
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
use crate::core::layout::{self, Layout};
@ -13,8 +42,9 @@ 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, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell,
self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell,
Size, Widget,
};
@ -28,16 +58,31 @@ use crate::core::{
///
/// # Example
/// ```no_run
/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>;
/// # 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>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
/// SliderChanged(f32),
/// use iced::widget::vertical_slider;
///
/// struct State {
/// value: f32,
/// }
///
/// let value = 50.0;
/// #[derive(Debug, Clone)]
/// enum Message {
/// ValueChanged(f32),
/// }
///
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
/// 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;
/// }
/// }
/// }
/// ```
#[allow(missing_debug_implementations)]
pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
@ -54,6 +99,7 @@ where
width: f32,
height: Length,
class: Theme::Class<'a>,
status: Option<Status>,
}
impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
@ -71,8 +117,8 @@ where
/// * 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`.
/// 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,
@ -100,6 +146,7 @@ where
width: Self::DEFAULT_WIDTH,
height: Length::Fill,
class: Theme::default(),
status: None,
}
}
@ -167,8 +214,8 @@ where
}
}
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VerticalSlider<'a, T, Message, Theme>
impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VerticalSlider<'_, T, Message, Theme>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
@ -199,17 +246,17 @@ where
layout::atomic(limits, self.width, self.height)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
let is_dragging = state.is_dragging;
let current_value = self.value;
@ -239,7 +286,7 @@ where
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value)
T::from_f64(value.min(end))
};
new_value
@ -305,7 +352,7 @@ where
state.is_dragging = true;
}
return event::Status::Captured;
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
@ -317,7 +364,7 @@ where
}
state.is_dragging = false;
return event::Status::Captured;
shell.capture_event();
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
@ -325,11 +372,29 @@ where
if is_dragging {
let _ = cursor.position().and_then(locate).map(change);
return event::Status::Captured;
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.position_over(layout.bounds()).is_some() {
if cursor.is_over(layout.bounds()) {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
@ -340,42 +405,44 @@ where
_ => (),
}
return event::Status::Captured;
shell.capture_event();
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
state.keyboard_modifiers = *modifiers;
}
_ => {}
}
event::Status::Ignored
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,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
let style = theme.style(
&self.class,
if state.is_dragging {
Status::Dragged
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
},
);
let style =
theme.style(&self.class, self.status.unwrap_or(Status::Active));
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
@ -412,10 +479,10 @@ where
width: style.rail.width,
height: offset + handle_width / 2.0,
},
border: Border::rounded(style.rail.border_radius),
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.colors.1,
style.rail.backgrounds.1,
);
renderer.fill_quad(
@ -426,10 +493,10 @@ where
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
border: Border::rounded(style.rail.border_radius),
border: style.rail.border,
..renderer::Quad::default()
},
style.rail.colors.0,
style.rail.backgrounds.0,
);
renderer.fill_quad(
@ -447,7 +514,7 @@ where
},
..renderer::Quad::default()
},
style.handle.color,
style.handle.background,
);
}