Merge branch 'master' into beacon
This commit is contained in:
commit
aaf396256e
284 changed files with 18747 additions and 15450 deletions
|
|
@ -10,6 +10,9 @@ homepage.workspace = true
|
|||
categories.workspace = true
|
||||
keywords.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
all-features = true
|
||||
|
|
@ -21,13 +24,14 @@ svg = ["iced_renderer/svg"]
|
|||
canvas = ["iced_renderer/geometry"]
|
||||
qr_code = ["canvas", "qrcode"]
|
||||
wgpu = ["iced_renderer/wgpu"]
|
||||
advanced = []
|
||||
|
||||
[dependencies]
|
||||
iced_renderer.workspace = true
|
||||
iced_runtime.workspace = true
|
||||
iced_style.workspace = true
|
||||
|
||||
num-traits.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
thiserror.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
//! Allow your users to perform actions by pressing a button.
|
||||
//!
|
||||
//! A [`Button`] has some local [`State`].
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::theme::palette;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::Operation;
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
|
||||
Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
|
||||
};
|
||||
|
||||
pub use crate::style::button::{Appearance, StyleSheet};
|
||||
|
||||
/// A generic widget that produces a message when pressed.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # type Button<'a, Message> =
|
||||
/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
|
||||
/// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
|
|
@ -34,8 +30,7 @@ pub use crate::style::button::{Appearance, StyleSheet};
|
|||
/// be disabled:
|
||||
///
|
||||
/// ```
|
||||
/// # type Button<'a, Message> =
|
||||
/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
|
||||
/// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// enum Message {
|
||||
|
|
@ -53,8 +48,8 @@ pub use crate::style::button::{Appearance, StyleSheet};
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
on_press: Option<Message>,
|
||||
|
|
@ -62,13 +57,13 @@ where
|
|||
height: Length,
|
||||
padding: Padding,
|
||||
clip: bool,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// Creates a new [`Button`] with the given content.
|
||||
pub fn new(
|
||||
|
|
@ -82,9 +77,9 @@ where
|
|||
on_press: None,
|
||||
width: size.width.fluid(),
|
||||
height: size.height.fluid(),
|
||||
padding: Padding::new(5.0),
|
||||
padding: DEFAULT_PADDING,
|
||||
clip: false,
|
||||
style: Theme::Style::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,33 +118,50 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Button`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the contents of the [`Button`] should be clipped on
|
||||
/// overflow.
|
||||
pub fn clip(mut self, clip: bool) -> Self {
|
||||
self.clip = clip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Button`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Button`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
struct State {
|
||||
is_pressed: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Button<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Theme: StyleSheet,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
|
|
@ -173,13 +185,19 @@ where
|
|||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(limits, self.width, self.height, self.padding, |limits| {
|
||||
self.content.as_widget().layout(
|
||||
&mut tree.children[0],
|
||||
renderer,
|
||||
limits,
|
||||
)
|
||||
})
|
||||
layout::padded(
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.padding,
|
||||
|limits| {
|
||||
self.content.as_widget().layout(
|
||||
&mut tree.children[0],
|
||||
renderer,
|
||||
limits,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
|
|
@ -223,9 +241,48 @@ where
|
|||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
update(event, layout, cursor, shell, &self.on_press, || {
|
||||
tree.state.downcast_mut::<State>()
|
||||
})
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if self.on_press.is_some() {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if cursor.is_over(bounds) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
state.is_pressed = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if let Some(on_press) = self.on_press.clone() {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if state.is_pressed {
|
||||
state.is_pressed = false;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if cursor.is_over(bounds) {
|
||||
shell.publish(on_press);
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
state.is_pressed = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -240,16 +297,39 @@ where
|
|||
) {
|
||||
let bounds = layout.bounds();
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let styling = draw(
|
||||
renderer,
|
||||
bounds,
|
||||
cursor,
|
||||
self.on_press.is_some(),
|
||||
theme,
|
||||
&self.style,
|
||||
|| tree.state.downcast_ref::<State>(),
|
||||
);
|
||||
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);
|
||||
|
||||
if style.background.is_some()
|
||||
|| style.border.width > 0.0
|
||||
|| style.shadow.color.a > 0.0
|
||||
{
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: style.border,
|
||||
shadow: style.shadow,
|
||||
},
|
||||
style
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
}
|
||||
|
||||
let viewport = if self.clip {
|
||||
bounds.intersection(viewport).unwrap_or(*viewport)
|
||||
|
|
@ -262,7 +342,7 @@ where
|
|||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
text_color: styling.text_color,
|
||||
text_color: style.text_color,
|
||||
},
|
||||
content_layout,
|
||||
cursor,
|
||||
|
|
@ -278,7 +358,13 @@ where
|
|||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor, self.on_press.is_some())
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
|
||||
if is_mouse_over && self.on_press.is_some() {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
|
|
@ -301,7 +387,7 @@ impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone + 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
{
|
||||
fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
|
||||
|
|
@ -309,143 +395,182 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Button`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
is_pressed: bool,
|
||||
/// The default [`Padding`] of a [`Button`].
|
||||
pub(crate) const DEFAULT_PADDING: Padding = Padding {
|
||||
top: 5.0,
|
||||
bottom: 5.0,
|
||||
right: 10.0,
|
||||
left: 10.0,
|
||||
};
|
||||
|
||||
/// The possible status of a [`Button`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Button`] can be pressed.
|
||||
Active,
|
||||
/// The [`Button`] can be pressed and it is being hovered.
|
||||
Hovered,
|
||||
/// The [`Button`] is being pressed.
|
||||
Pressed,
|
||||
/// The [`Button`] cannot be pressed.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
/// The style of a button.
|
||||
#[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.
|
||||
pub border: Border,
|
||||
/// The [`Shadow`] of the butoon.
|
||||
pub shadow: Shadow,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Updates the [`Style`] with the given [`Background`].
|
||||
pub fn with_background(self, background: impl Into<Background>) -> Self {
|
||||
Self {
|
||||
background: Some(background.into()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
|
||||
/// accordingly.
|
||||
pub fn update<'a, Message: Clone>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
on_press: &Option<Message>,
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if on_press.is_some() {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if cursor.is_over(bounds) {
|
||||
let state = state();
|
||||
|
||||
state.is_pressed = true;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
background: None,
|
||||
text_color: Color::BLACK,
|
||||
border: Border::default(),
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
if let Some(on_press) = on_press.clone() {
|
||||
let state = state();
|
||||
|
||||
if state.is_pressed {
|
||||
state.is_pressed = false;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if cursor.is_over(bounds) {
|
||||
shell.publish(on_press);
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
let state = state();
|
||||
|
||||
state.is_pressed = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`Button`].
|
||||
pub fn draw<'a, Theme, Renderer: crate::core::Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
is_enabled: bool,
|
||||
theme: &Theme,
|
||||
style: &Theme::Style,
|
||||
state: impl FnOnce() -> &'a State,
|
||||
) -> Appearance
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
{
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
/// The theme catalog of a [`Button`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
let styling = if !is_enabled {
|
||||
theme.disabled(style)
|
||||
} else if is_mouse_over {
|
||||
let state = state();
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
if state.is_pressed {
|
||||
theme.pressed(style)
|
||||
} else {
|
||||
theme.hovered(style)
|
||||
}
|
||||
} else {
|
||||
theme.active(style)
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Button`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(primary)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// A primary button; denoting a main action.
|
||||
pub fn primary(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
let base = styled(palette.primary.strong);
|
||||
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
background: Some(Background::Color(palette.primary.base.color)),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
}
|
||||
}
|
||||
|
||||
/// A secondary button; denoting a complementary action.
|
||||
pub fn secondary(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
let base = styled(palette.secondary.base);
|
||||
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
background: Some(Background::Color(palette.secondary.strong.color)),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
}
|
||||
}
|
||||
|
||||
/// A success button; denoting a good outcome.
|
||||
pub fn success(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
let base = styled(palette.success.base);
|
||||
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
background: Some(Background::Color(palette.success.strong.color)),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
}
|
||||
}
|
||||
|
||||
/// A danger button; denoting a destructive action.
|
||||
pub fn danger(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
let base = styled(palette.danger.base);
|
||||
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
background: Some(Background::Color(palette.danger.strong.color)),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
}
|
||||
}
|
||||
|
||||
/// A text button; useful for links.
|
||||
pub fn text(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
let base = Style {
|
||||
text_color: palette.background.base.text,
|
||||
..Style::default()
|
||||
};
|
||||
|
||||
if styling.background.is_some()
|
||||
|| styling.border.width > 0.0
|
||||
|| styling.shadow.color.a > 0.0
|
||||
{
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: styling.border,
|
||||
shadow: styling.shadow,
|
||||
},
|
||||
styling
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
}
|
||||
|
||||
styling
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Button`].
|
||||
pub fn layout(
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
layout::padded(limits, width, height, padding, layout_content)
|
||||
}
|
||||
|
||||
/// Returns the [`mouse::Interaction`] of a [`Button`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
is_enabled: bool,
|
||||
) -> mouse::Interaction {
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
|
||||
if is_mouse_over && is_enabled {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
match status {
|
||||
Status::Active | Status::Pressed => base,
|
||||
Status::Hovered => Style {
|
||||
text_color: palette.background.base.text.scale_alpha(0.8),
|
||||
..base
|
||||
},
|
||||
Status::Disabled => disabled(base),
|
||||
}
|
||||
}
|
||||
|
||||
fn styled(pair: palette::Pair) -> Style {
|
||||
Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border::rounded(2),
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(style: Style) -> Style {
|
||||
Style {
|
||||
background: style
|
||||
.background
|
||||
.map(|background| background.scale_alpha(0.5)),
|
||||
text_color: style.text_color.scale_alpha(0.5),
|
||||
..style
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ mod program;
|
|||
pub use event::Event;
|
||||
pub use program::Program;
|
||||
|
||||
pub use crate::graphics::geometry::*;
|
||||
pub use crate::renderer::geometry::*;
|
||||
pub use crate::graphics::cache::Group;
|
||||
pub use crate::graphics::geometry::{
|
||||
fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin,
|
||||
Path, Stroke, Style, Text,
|
||||
};
|
||||
|
||||
use crate::core;
|
||||
use crate::core::layout::{self, Layout};
|
||||
|
|
@ -15,12 +18,25 @@ use crate::core::mouse;
|
|||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Length, Rectangle, Shell, Size, Transformation, Widget,
|
||||
Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
use crate::graphics::geometry;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A simple cache that stores generated [`Geometry`] to avoid recomputation.
|
||||
///
|
||||
/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer
|
||||
/// change or it is explicitly cleared.
|
||||
pub type Cache<Renderer = crate::Renderer> = geometry::Cache<Renderer>;
|
||||
|
||||
/// The geometry supported by a renderer.
|
||||
pub type Geometry<Renderer = crate::Renderer> =
|
||||
<Renderer as geometry::Renderer>::Geometry;
|
||||
|
||||
/// The frame supported by a renderer.
|
||||
pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>;
|
||||
|
||||
/// A widget capable of drawing 2D graphics.
|
||||
///
|
||||
/// ## Drawing a simple circle
|
||||
|
|
@ -42,7 +58,7 @@ use std::marker::PhantomData;
|
|||
/// impl Program<()> for Circle {
|
||||
/// 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<Geometry> {
|
||||
/// // We prepare a new `Frame`
|
||||
/// let mut frame = Frame::new(renderer, bounds.size());
|
||||
///
|
||||
|
|
@ -207,12 +223,15 @@ where
|
|||
|
||||
let state = tree.state.downcast_ref::<P::State>();
|
||||
|
||||
renderer.with_transformation(
|
||||
Transformation::translate(bounds.x, bounds.y),
|
||||
renderer.with_translation(
|
||||
Vector::new(bounds.x, bounds.y),
|
||||
|renderer| {
|
||||
renderer.draw(
|
||||
self.program.draw(state, renderer, theme, bounds, cursor),
|
||||
);
|
||||
let layers =
|
||||
self.program.draw(state, renderer, theme, bounds, cursor);
|
||||
|
||||
for layer in layers {
|
||||
renderer.draw_geometry(layer);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::canvas::event::{self, Event};
|
||||
use crate::canvas::mouse;
|
||||
use crate::canvas::Geometry;
|
||||
use crate::core::Rectangle;
|
||||
use crate::graphics::geometry;
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ where
|
|||
theme: &Theme,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Vec<Renderer::Geometry>;
|
||||
) -> Vec<Geometry<Renderer>>;
|
||||
|
||||
/// Returns the current mouse interaction of the [`Program`].
|
||||
///
|
||||
|
|
@ -94,7 +95,7 @@ where
|
|||
theme: &Theme,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Vec<Renderer::Geometry> {
|
||||
) -> Vec<Geometry<Renderer>> {
|
||||
T::draw(self, state, renderer, theme, bounds, cursor)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,22 +5,21 @@ use crate::core::layout;
|
|||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::theme::palette;
|
||||
use crate::core::touch;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget,
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
pub use crate::style::checkbox::{Appearance, StyleSheet};
|
||||
|
||||
/// A box that can be checked.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # type Checkbox<'a, Message> =
|
||||
/// # iced_widget::Checkbox<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
|
||||
/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// CheckboxToggled(bool),
|
||||
|
|
@ -39,8 +38,8 @@ pub struct Checkbox<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: StyleSheet + crate::text::StyleSheet,
|
||||
Renderer: text::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
is_checked: bool,
|
||||
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||
|
|
@ -53,19 +52,19 @@ pub struct Checkbox<
|
|||
text_shaping: text::Shaping,
|
||||
font: Option<Renderer::Font>,
|
||||
icon: Icon<Renderer::Font>,
|
||||
style: <Theme as StyleSheet>::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
Theme: StyleSheet + crate::text::StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// The default size of a [`Checkbox`].
|
||||
const DEFAULT_SIZE: f32 = 20.0;
|
||||
const DEFAULT_SIZE: f32 = 16.0;
|
||||
|
||||
/// The default spacing of a [`Checkbox`].
|
||||
const DEFAULT_SPACING: f32 = 10.0;
|
||||
const DEFAULT_SPACING: f32 = 8.0;
|
||||
|
||||
/// Creates a new [`Checkbox`].
|
||||
///
|
||||
|
|
@ -91,7 +90,7 @@ where
|
|||
line_height: text::LineHeight::default(),
|
||||
shaping: text::Shaping::Basic,
|
||||
},
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,11 +173,20 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`Checkbox`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Checkbox`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -186,8 +194,8 @@ where
|
|||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Checkbox<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet + crate::text::StyleSheet,
|
||||
Renderer: text::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
|
||||
|
|
@ -286,24 +294,27 @@ where
|
|||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
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 custom_style = if is_disabled {
|
||||
theme.disabled(&self.style, self.is_checked)
|
||||
let status = if is_disabled {
|
||||
Status::Disabled { is_checked }
|
||||
} else if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_checked)
|
||||
Status::Hovered { is_checked }
|
||||
} else {
|
||||
theme.active(&self.style, self.is_checked)
|
||||
Status::Active { is_checked }
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
|
|
@ -311,10 +322,10 @@ where
|
|||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: custom_style.border,
|
||||
border: style.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
custom_style.background,
|
||||
style.background,
|
||||
);
|
||||
|
||||
let Icon {
|
||||
|
|
@ -329,7 +340,7 @@ where
|
|||
if self.is_checked {
|
||||
renderer.fill_text(
|
||||
text::Text {
|
||||
content: &code_point.to_string(),
|
||||
content: code_point.to_string(),
|
||||
font: *font,
|
||||
size,
|
||||
line_height: *line_height,
|
||||
|
|
@ -339,7 +350,7 @@ where
|
|||
shaping: *shaping,
|
||||
},
|
||||
bounds.center(),
|
||||
custom_style.icon_color,
|
||||
style.icon_color,
|
||||
*viewport,
|
||||
);
|
||||
}
|
||||
|
|
@ -350,11 +361,11 @@ where
|
|||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
defaults,
|
||||
label_layout,
|
||||
tree.state.downcast_ref(),
|
||||
crate::text::Appearance {
|
||||
color: custom_style.text_color,
|
||||
crate::text::Style {
|
||||
color: style.text_color,
|
||||
},
|
||||
viewport,
|
||||
);
|
||||
|
|
@ -366,7 +377,7 @@ impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a + StyleSheet + crate::text::StyleSheet,
|
||||
Theme: 'a + Catalog,
|
||||
Renderer: 'a + text::Renderer,
|
||||
{
|
||||
fn from(
|
||||
|
|
@ -390,3 +401,191 @@ pub struct Icon<Font> {
|
|||
/// The shaping strategy of the icon.
|
||||
pub shaping: text::Shaping,
|
||||
}
|
||||
|
||||
/// The possible status of a [`Checkbox`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Checkbox`] can be interacted with.
|
||||
Active {
|
||||
/// Indicates if the [`Checkbox`] is currently checked.
|
||||
is_checked: bool,
|
||||
},
|
||||
/// The [`Checkbox`] can be interacted with and it is being hovered.
|
||||
Hovered {
|
||||
/// Indicates if the [`Checkbox`] is currently checked.
|
||||
is_checked: bool,
|
||||
},
|
||||
/// The [`Checkbox`] cannot be interacted with.
|
||||
Disabled {
|
||||
/// Indicates if the [`Checkbox`] is currently checked.
|
||||
is_checked: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// The style of a checkbox.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
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.
|
||||
pub border: Border,
|
||||
/// The text [`Color`] of the checkbox.
|
||||
pub text_color: Option<Color>,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Checkbox`].
|
||||
pub trait Catalog: Sized {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Checkbox`].
|
||||
///
|
||||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(primary)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// A primary checkbox; denoting a main toggle.
|
||||
pub fn primary(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.primary.strong.text,
|
||||
palette.background.base,
|
||||
palette.primary.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.primary.strong.text,
|
||||
palette.background.weak,
|
||||
palette.primary.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.primary.strong.text,
|
||||
palette.background.weak,
|
||||
palette.background.strong,
|
||||
is_checked,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// A secondary checkbox; denoting a complementary toggle.
|
||||
pub fn secondary(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.background.base.text,
|
||||
palette.background.base,
|
||||
palette.background.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.background.base.text,
|
||||
palette.background.weak,
|
||||
palette.background.strong,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.background.strong.color,
|
||||
palette.background.weak,
|
||||
palette.background.weak,
|
||||
is_checked,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// A success checkbox; denoting a positive toggle.
|
||||
pub fn success(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
match status {
|
||||
Status::Active { is_checked } => styled(
|
||||
palette.success.base.text,
|
||||
palette.background.base,
|
||||
palette.success.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.success.base.text,
|
||||
palette.background.weak,
|
||||
palette.success.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.success.base.text,
|
||||
palette.background.weak,
|
||||
palette.success.weak,
|
||||
is_checked,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// A danger checkbox; denoting a negaive 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.base,
|
||||
palette.danger.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Hovered { is_checked } => styled(
|
||||
palette.danger.base.text,
|
||||
palette.background.weak,
|
||||
palette.danger.base,
|
||||
is_checked,
|
||||
),
|
||||
Status::Disabled { is_checked } => styled(
|
||||
palette.danger.base.text,
|
||||
palette.background.weak,
|
||||
palette.danger.weak,
|
||||
is_checked,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn styled(
|
||||
icon_color: Color,
|
||||
base: palette::Pair,
|
||||
accent: palette::Pair,
|
||||
is_checked: bool,
|
||||
) -> Style {
|
||||
Style {
|
||||
background: Background::Color(if is_checked {
|
||||
accent.color
|
||||
} else {
|
||||
base.color
|
||||
}),
|
||||
icon_color,
|
||||
border: Border {
|
||||
radius: 2.0.into(),
|
||||
width: 1.0,
|
||||
color: accent.color,
|
||||
},
|
||||
text_color: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,18 @@ where
|
|||
Self::from_vec(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self::from_vec(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self::new().extend(children)
|
||||
let iterator = children.into_iter();
|
||||
|
||||
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] from an already allocated [`Vec`].
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ 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, Vector,
|
||||
Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector,
|
||||
};
|
||||
use crate::overlay::menu;
|
||||
use crate::text::LineHeight;
|
||||
use crate::{container, scrollable, text_input, TextInput};
|
||||
use crate::text_input::{self, TextInput};
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Display;
|
||||
|
|
@ -32,7 +32,7 @@ pub struct ComboBox<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: text_input::StyleSheet + menu::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
state: &'a State<T>,
|
||||
|
|
@ -43,7 +43,7 @@ pub struct ComboBox<
|
|||
on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
|
||||
on_close: Option<Message>,
|
||||
on_input: Option<Box<dyn Fn(String) -> Message>>,
|
||||
menu_style: <Theme as menu::StyleSheet>::Style,
|
||||
menu_class: <Theme as menu::Catalog>::Class<'a>,
|
||||
padding: Padding,
|
||||
size: Option<f32>,
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ pub struct ComboBox<
|
|||
impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: std::fmt::Display + Clone,
|
||||
Theme: text_input::StyleSheet + menu::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// Creates a new [`ComboBox`] with the given list of options, a placeholder,
|
||||
|
|
@ -64,7 +64,8 @@ where
|
|||
on_selected: impl Fn(T) -> Message + 'static,
|
||||
) -> Self {
|
||||
let text_input = TextInput::new(placeholder, &state.value())
|
||||
.on_input(TextInputEvent::TextChanged);
|
||||
.on_input(TextInputEvent::TextChanged)
|
||||
.class(Theme::default_input());
|
||||
|
||||
let selection = selection.map(T::to_string).unwrap_or_default();
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ where
|
|||
on_option_hovered: None,
|
||||
on_input: None,
|
||||
on_close: None,
|
||||
menu_style: Default::default(),
|
||||
menu_class: <Theme as Catalog>::default_menu(),
|
||||
padding: text_input::DEFAULT_PADDING,
|
||||
size: None,
|
||||
}
|
||||
|
|
@ -117,28 +118,6 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`ComboBox`].
|
||||
// TODO: Define its own `StyleSheet` trait
|
||||
pub fn style<S>(mut self, style: S) -> Self
|
||||
where
|
||||
S: Into<<Theme as text_input::StyleSheet>::Style>
|
||||
+ Into<<Theme as menu::StyleSheet>::Style>
|
||||
+ Clone,
|
||||
{
|
||||
self.menu_style = style.clone().into();
|
||||
self.text_input = self.text_input.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`TextInput`] of the [`ComboBox`].
|
||||
pub fn text_input_style<S>(mut self, style: S) -> Self
|
||||
where
|
||||
S: Into<<Theme as text_input::StyleSheet>::Style> + Clone,
|
||||
{
|
||||
self.text_input = self.text_input.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Renderer::Font`] of the [`ComboBox`].
|
||||
///
|
||||
/// [`Renderer::Font`]: text::Renderer
|
||||
|
|
@ -176,6 +155,55 @@ where
|
|||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the input of the [`ComboBox`].
|
||||
#[must_use]
|
||||
pub fn input_style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
|
||||
) -> Self
|
||||
where
|
||||
<Theme as text_input::Catalog>::Class<'a>:
|
||||
From<text_input::StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.text_input = self.text_input.style(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the menu of the [`ComboBox`].
|
||||
#[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 input of the [`ComboBox`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn input_class(
|
||||
mut self,
|
||||
class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
|
||||
) -> Self {
|
||||
self.text_input = self.text_input.class(class);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the menu of the [`ComboBox`].
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`ComboBox`].
|
||||
|
|
@ -299,10 +327,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
|||
where
|
||||
T: Display + Clone + 'static,
|
||||
Message: Clone,
|
||||
Theme: container::StyleSheet
|
||||
+ text_input::StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
|
|
@ -675,38 +700,47 @@ where
|
|||
..
|
||||
} = tree.state.downcast_mut::<Menu<T>>();
|
||||
|
||||
let bounds = layout.bounds();
|
||||
|
||||
self.state.sync_filtered_options(filtered_options);
|
||||
|
||||
let mut menu = menu::Menu::new(
|
||||
menu,
|
||||
&filtered_options.options,
|
||||
hovered_option,
|
||||
|x| {
|
||||
tree.children[0]
|
||||
.state
|
||||
.downcast_mut::<text_input::State<Renderer::Paragraph>>(
|
||||
)
|
||||
.unfocus();
|
||||
if filtered_options.options.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
(self.on_selected)(x)
|
||||
},
|
||||
self.on_option_hovered.as_deref(),
|
||||
)
|
||||
.width(bounds.width)
|
||||
.padding(self.padding)
|
||||
.style(self.menu_style.clone());
|
||||
let mut menu = menu::Menu::new(
|
||||
menu,
|
||||
&filtered_options.options,
|
||||
hovered_option,
|
||||
|x| {
|
||||
tree.children[0]
|
||||
.state
|
||||
.downcast_mut::<text_input::State<Renderer::Paragraph>>(
|
||||
)
|
||||
.unfocus();
|
||||
|
||||
if let Some(font) = self.font {
|
||||
menu = menu.font(font);
|
||||
(self.on_selected)(x)
|
||||
},
|
||||
self.on_option_hovered.as_deref(),
|
||||
&self.menu_class,
|
||||
)
|
||||
.width(bounds.width)
|
||||
.padding(self.padding);
|
||||
|
||||
if let Some(font) = self.font {
|
||||
menu = menu.font(font);
|
||||
}
|
||||
|
||||
if let Some(size) = self.size {
|
||||
menu = menu.text_size(size);
|
||||
}
|
||||
|
||||
Some(
|
||||
menu.overlay(
|
||||
layout.position() + translation,
|
||||
bounds.height,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(size) = self.size {
|
||||
menu = menu.text_size(size);
|
||||
}
|
||||
|
||||
Some(menu.overlay(layout.position() + translation, bounds.height))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -719,11 +753,7 @@ impl<'a, T, Message, Theme, Renderer>
|
|||
where
|
||||
T: Display + Clone + 'static,
|
||||
Message: Clone + 'a,
|
||||
Theme: container::StyleSheet
|
||||
+ text_input::StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ menu::StyleSheet
|
||||
+ 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
{
|
||||
fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
|
||||
|
|
@ -731,8 +761,22 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Search list of options for a given query.
|
||||
pub fn search<'a, T, A>(
|
||||
/// The theme catalog of a [`ComboBox`].
|
||||
pub trait Catalog: text_input::Catalog + menu::Catalog {
|
||||
/// The default class for the text input of the [`ComboBox`].
|
||||
fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
|
||||
<Self as text_input::Catalog>::default()
|
||||
}
|
||||
|
||||
/// The default class for the menu of the [`ComboBox`].
|
||||
fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
|
||||
<Self as menu::Catalog>::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Catalog for Theme {}
|
||||
|
||||
fn search<'a, T, A>(
|
||||
options: impl IntoIterator<Item = T> + 'a,
|
||||
option_matchers: impl IntoIterator<Item = &'a A> + 'a,
|
||||
query: &'a str,
|
||||
|
|
@ -759,8 +803,7 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
/// Build matchers from given list of options.
|
||||
pub fn build_matchers<'a, T>(
|
||||
fn build_matchers<'a, T>(
|
||||
options: impl IntoIterator<Item = T> + 'a,
|
||||
) -> Vec<String>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Decorate content and apply alignment.
|
||||
use crate::core::alignment::{self, Alignment};
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::gradient::{self, Gradient};
|
||||
use crate::core::layout;
|
||||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
|
|
@ -8,13 +9,12 @@ use crate::core::renderer;
|
|||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::{self, Operation};
|
||||
use crate::core::{
|
||||
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
|
||||
Point, Rectangle, Shell, Size, Vector, Widget,
|
||||
self, Background, Border, Clipboard, Color, Element, Layout, Length,
|
||||
Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector,
|
||||
Widget,
|
||||
};
|
||||
use crate::runtime::Command;
|
||||
|
||||
pub use iced_style::container::{Appearance, StyleSheet};
|
||||
|
||||
/// An element decorating some content.
|
||||
///
|
||||
/// It is normally used for alignment purposes.
|
||||
|
|
@ -25,8 +25,8 @@ pub struct Container<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
id: Option<Id>,
|
||||
padding: Padding,
|
||||
|
|
@ -36,21 +36,20 @@ pub struct Container<
|
|||
max_height: f32,
|
||||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
style: Theme::Style,
|
||||
clip: bool,
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates an empty [`Container`].
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
{
|
||||
/// Creates a [`Container`] with the given content.
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let content = content.into();
|
||||
let size = content.as_widget().size_hint();
|
||||
|
||||
|
|
@ -63,8 +62,8 @@ where
|
|||
max_height: f32::INFINITY,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
style: Default::default(),
|
||||
clip: false,
|
||||
class: Theme::default(),
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +92,49 @@ 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.
|
||||
///
|
||||
/// This can be useful to quickly position content when chained with
|
||||
/// alignment functions—like [`center`].
|
||||
///
|
||||
/// 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;
|
||||
|
|
@ -117,22 +159,31 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Centers the contents in the horizontal axis of the [`Container`].
|
||||
/// Sets the [`Container`] to fill the available space in the horizontal axis
|
||||
/// and centers its contents there.
|
||||
pub fn center_x(mut self) -> Self {
|
||||
self.width = Length::Fill;
|
||||
self.horizontal_alignment = alignment::Horizontal::Center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Centers the contents in the vertical axis of the [`Container`].
|
||||
/// Sets the [`Container`] to fill the available space in the vertical axis
|
||||
/// and centers its contents there.
|
||||
pub fn center_y(mut self) -> Self {
|
||||
self.height = Length::Fill;
|
||||
self.vertical_alignment = alignment::Vertical::Center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Container`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
/// Centers the contents in both the horizontal and vertical axes of the
|
||||
/// [`Container`].
|
||||
///
|
||||
/// This is equivalent to chaining [`center_x`] and [`center_y`].
|
||||
///
|
||||
/// [`center_x`]: Self::center_x
|
||||
/// [`center_y`]: Self::center_y
|
||||
pub fn center(self) -> Self {
|
||||
self.center_x().center_y()
|
||||
}
|
||||
|
||||
/// Sets whether the contents of the [`Container`] should be clipped on
|
||||
|
|
@ -141,13 +192,31 @@ where
|
|||
self.clip = clip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Container`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Container`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Container<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
|
|
@ -262,10 +331,11 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let style = theme.appearance(&self.style);
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||
draw_background(renderer, &style, layout.bounds());
|
||||
if let Some(clipped_viewport) = bounds.intersection(viewport) {
|
||||
draw_background(renderer, &style, bounds);
|
||||
|
||||
self.content.as_widget().draw(
|
||||
tree,
|
||||
|
|
@ -307,8 +377,8 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a + StyleSheet,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
column: Container<'a, Message, Theme, Renderer>,
|
||||
|
|
@ -345,25 +415,25 @@ pub fn layout(
|
|||
)
|
||||
}
|
||||
|
||||
/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`.
|
||||
/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`.
|
||||
pub fn draw_background<Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
appearance: &Appearance,
|
||||
style: &Style,
|
||||
bounds: Rectangle,
|
||||
) where
|
||||
Renderer: crate::core::Renderer,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
if appearance.background.is_some()
|
||||
|| appearance.border.width > 0.0
|
||||
|| appearance.shadow.color.a > 0.0
|
||||
if style.background.is_some()
|
||||
|| style.border.width > 0.0
|
||||
|| style.shadow.color.a > 0.0
|
||||
{
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: appearance.border,
|
||||
shadow: appearance.shadow,
|
||||
border: style.border,
|
||||
shadow: style.shadow,
|
||||
},
|
||||
appearance
|
||||
style
|
||||
.background
|
||||
.unwrap_or(Background::Color(Color::TRANSPARENT)),
|
||||
);
|
||||
|
|
@ -482,3 +552,118 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
|
|||
bounds: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The appearance of a container.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Style {
|
||||
/// The text [`Color`] of the container.
|
||||
pub text_color: Option<Color>,
|
||||
/// The [`Background`] of the container.
|
||||
pub background: Option<Background>,
|
||||
/// The [`Border`] of the container.
|
||||
pub border: Border,
|
||||
/// The [`Shadow`] of the container.
|
||||
pub shadow: Shadow,
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
border: Border {
|
||||
color: color.into(),
|
||||
width: width.into().0,
|
||||
..Border::default()
|
||||
},
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the background of the [`Style`].
|
||||
pub fn with_background(self, background: impl Into<Background>) -> Self {
|
||||
Self {
|
||||
background: Some(background.into()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
fn from(color: Color) -> Self {
|
||||
Self::default().with_background(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gradient> for Style {
|
||||
fn from(gradient: Gradient) -> Self {
|
||||
Self::default().with_background(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gradient::Linear> for Style {
|
||||
fn from(gradient: gradient::Linear) -> Self {
|
||||
Self::default().with_background(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Container`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Container`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(transparent)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||
class(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A transparent [`Container`].
|
||||
pub fn transparent<Theme>(_theme: &Theme) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
/// A 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),
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A bordered [`Container`] with a background.
|
||||
pub fn bordered_box(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
background: Some(palette.background.weak.color.into()),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
color: palette.background.strong.color,
|
||||
},
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::combo_box::{self, ComboBox};
|
|||
use crate::container::{self, Container};
|
||||
use crate::core;
|
||||
use crate::core::widget::operation;
|
||||
use crate::core::{Element, Length, Pixels};
|
||||
use crate::core::{Element, Length, Pixels, Widget};
|
||||
use crate::keyed;
|
||||
use crate::overlay;
|
||||
use crate::pane_grid::{self, PaneGrid};
|
||||
|
|
@ -16,13 +16,13 @@ use crate::rule::{self, Rule};
|
|||
use crate::runtime::Command;
|
||||
use crate::scrollable::{self, Scrollable};
|
||||
use crate::slider::{self, Slider};
|
||||
use crate::style::application;
|
||||
use crate::text::{self, Text};
|
||||
use crate::text_editor::{self, TextEditor};
|
||||
use crate::text_input::{self, TextInput};
|
||||
use crate::toggler::{self, Toggler};
|
||||
use crate::tooltip::{self, Tooltip};
|
||||
use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider};
|
||||
use crate::vertical_slider::{self, VerticalSlider};
|
||||
use crate::{Column, MouseArea, Row, Space, Stack, Themer};
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::ops::RangeInclusive;
|
||||
|
|
@ -53,6 +53,19 @@ macro_rules! row {
|
|||
);
|
||||
}
|
||||
|
||||
/// Creates a [`Stack`] with the given children.
|
||||
///
|
||||
/// [`Stack`]: crate::Stack
|
||||
#[macro_export]
|
||||
macro_rules! stack {
|
||||
() => (
|
||||
$crate::Stack::new()
|
||||
);
|
||||
($($x:expr),+ $(,)?) => (
|
||||
$crate::Stack::with_children([$($crate::core::Element::from($x)),+])
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new [`Container`] with the provided content.
|
||||
///
|
||||
/// [`Container`]: crate::Container
|
||||
|
|
@ -60,12 +73,33 @@ pub fn container<'a, Message, Theme, Renderer>(
|
|||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Container<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet,
|
||||
Theme: container::Catalog + 'a,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
Container::new(content)
|
||||
}
|
||||
|
||||
/// Creates a new [`Container`] that fills all the available space
|
||||
/// and centers its contents inside.
|
||||
///
|
||||
/// This is equivalent to:
|
||||
/// ```rust,no_run
|
||||
/// # use iced_widget::Container;
|
||||
/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() }
|
||||
/// let centered = container("Centered!").center();
|
||||
/// ```
|
||||
///
|
||||
/// [`Container`]: crate::Container
|
||||
pub fn center<'a, Message, Theme, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Container<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::Catalog + 'a,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
container(content).fill().center()
|
||||
}
|
||||
|
||||
/// Creates a new [`Column`] with the given children.
|
||||
pub fn column<'a, Message, Theme, Renderer>(
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
|
|
@ -99,6 +133,428 @@ where
|
|||
Row::with_children(children)
|
||||
}
|
||||
|
||||
/// Creates a new [`Stack`] with the given children.
|
||||
///
|
||||
/// [`Stack`]: crate::Stack
|
||||
pub fn stack<'a, Message, Theme, Renderer>(
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Stack<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
Stack::with_children(children)
|
||||
}
|
||||
|
||||
/// Wraps the given widget and captures any mouse button presses inside the bounds of
|
||||
/// the widget—effectively making it _opaque_.
|
||||
///
|
||||
/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse
|
||||
/// events from passing through layers.
|
||||
///
|
||||
/// [`Stack`]: crate::Stack
|
||||
pub fn opaque<'a, Message, Theme, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
use crate::core::event::{self, 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::{Rectangle, Shell, Size};
|
||||
|
||||
struct Opaque<'a, Message, Theme, Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Opaque<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
self.content.as_widget().state()
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.content.as_widget().children()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
self.content.as_widget().diff(tree);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
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(tree, renderer, limits)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn operation::Operation<Message>,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
state: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn core::Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let is_mouse_press = matches!(
|
||||
event,
|
||||
core::Event::Mouse(mouse::Event::ButtonPressed(_))
|
||||
);
|
||||
|
||||
if let core::event::Status::Captured =
|
||||
self.content.as_widget_mut().on_event(
|
||||
state, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport,
|
||||
)
|
||||
{
|
||||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
if is_mouse_press && cursor.is_over(layout.bounds()) {
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
state: &core::widget::Tree,
|
||||
layout: core::Layout<'_>,
|
||||
cursor: core::mouse::Cursor,
|
||||
viewport: &core::Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> core::mouse::Interaction {
|
||||
let interaction = self
|
||||
.content
|
||||
.as_widget()
|
||||
.mouse_interaction(state, layout, cursor, viewport, renderer);
|
||||
|
||||
if interaction == mouse::Interaction::None
|
||||
&& cursor.is_over(layout.bounds())
|
||||
{
|
||||
mouse::Interaction::Idle
|
||||
} else {
|
||||
interaction
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
state: &'b mut core::widget::Tree,
|
||||
layout: core::Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: core::Vector,
|
||||
) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>>
|
||||
{
|
||||
self.content.as_widget_mut().overlay(
|
||||
state,
|
||||
layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Element::new(Opaque {
|
||||
content: content.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Displays a widget on top of another one, only when the base widget is hovered.
|
||||
///
|
||||
/// This works analogously to a [`stack`], but it will only display the layer on top
|
||||
/// when the cursor is over the base. It can be useful for removing visual clutter.
|
||||
///
|
||||
/// [`stack`]: stack()
|
||||
pub fn hover<'a, Message, Theme, Renderer>(
|
||||
base: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
top: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
use crate::core::event::{self, 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::{Rectangle, Shell, Size};
|
||||
|
||||
struct Hover<'a, Message, Theme, Renderer> {
|
||||
base: Element<'a, Message, Theme, Renderer>,
|
||||
top: Element<'a, Message, Theme, Renderer>,
|
||||
is_top_overlay_active: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Hover<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
struct Tag;
|
||||
tree::Tag::of::<Tag>()
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.base), Tree::new(&self.top)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&[&self.base, &self.top]);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.base.as_widget().size()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.base.as_widget().size_hint()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let base = self.base.as_widget().layout(
|
||||
&mut tree.children[0],
|
||||
renderer,
|
||||
limits,
|
||||
);
|
||||
|
||||
let top = self.top.as_widget().layout(
|
||||
&mut tree.children[1],
|
||||
renderer,
|
||||
&layout::Limits::new(Size::ZERO, base.size()),
|
||||
);
|
||||
|
||||
layout::Node::with_children(base.size(), vec![base, top])
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Some(bounds) = layout.bounds().intersection(viewport) {
|
||||
let mut children = layout.children().zip(&tree.children);
|
||||
|
||||
let (base_layout, base_tree) = children.next().unwrap();
|
||||
|
||||
self.base.as_widget().draw(
|
||||
base_tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
base_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
|
||||
if cursor.is_over(layout.bounds()) || self.is_top_overlay_active
|
||||
{
|
||||
let (top_layout, top_tree) = children.next().unwrap();
|
||||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
self.top.as_widget().draw(
|
||||
top_tree, renderer, theme, style, top_layout,
|
||||
cursor, viewport,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn operation::Operation<Message>,
|
||||
) {
|
||||
let children = [&self.base, &self.top]
|
||||
.into_iter()
|
||||
.zip(layout.children().zip(&mut tree.children));
|
||||
|
||||
for (child, (layout, tree)) in children {
|
||||
child.as_widget().operate(tree, layout, renderer, operation);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn core::Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let mut children = layout.children().zip(&mut tree.children);
|
||||
let (base_layout, base_tree) = children.next().unwrap();
|
||||
|
||||
let top_status = if matches!(
|
||||
event,
|
||||
Event::Mouse(
|
||||
mouse::Event::CursorMoved { .. }
|
||||
| mouse::Event::ButtonReleased(_)
|
||||
)
|
||||
) || cursor.is_over(layout.bounds())
|
||||
{
|
||||
let (top_layout, top_tree) = children.next().unwrap();
|
||||
|
||||
self.top.as_widget_mut().on_event(
|
||||
top_tree,
|
||||
event.clone(),
|
||||
top_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
};
|
||||
|
||||
if top_status == event::Status::Captured {
|
||||
return top_status;
|
||||
}
|
||||
|
||||
self.base.as_widget_mut().on_event(
|
||||
base_tree,
|
||||
event.clone(),
|
||||
base_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
[&self.base, &self.top]
|
||||
.into_iter()
|
||||
.rev()
|
||||
.zip(layout.children().rev().zip(tree.children.iter().rev()))
|
||||
.map(|(child, (layout, tree))| {
|
||||
child.as_widget().mouse_interaction(
|
||||
tree, layout, cursor, viewport, renderer,
|
||||
)
|
||||
})
|
||||
.find(|&interaction| interaction != mouse::Interaction::None)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut core::widget::Tree,
|
||||
layout: core::Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: core::Vector,
|
||||
) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>>
|
||||
{
|
||||
let mut overlays = [&mut self.base, &mut self.top]
|
||||
.into_iter()
|
||||
.zip(layout.children().zip(tree.children.iter_mut()))
|
||||
.map(|(child, (layout, tree))| {
|
||||
child.as_widget_mut().overlay(
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(base_overlay) = overlays.next()? {
|
||||
return Some(base_overlay);
|
||||
}
|
||||
|
||||
let top_overlay = overlays.next()?;
|
||||
self.is_top_overlay_active = top_overlay.is_some();
|
||||
|
||||
top_overlay
|
||||
}
|
||||
}
|
||||
|
||||
Element::new(Hover {
|
||||
base: base.into(),
|
||||
top: top.into(),
|
||||
is_top_overlay_active: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new [`Scrollable`] with the provided content.
|
||||
///
|
||||
/// [`Scrollable`]: crate::Scrollable
|
||||
|
|
@ -106,7 +562,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>(
|
|||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Scrollable<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: scrollable::StyleSheet,
|
||||
Theme: scrollable::Catalog + 'a,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
Scrollable::new(content)
|
||||
|
|
@ -119,8 +575,8 @@ pub fn button<'a, Message, Theme, Renderer>(
|
|||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Button<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: button::Catalog + 'a,
|
||||
Renderer: core::Renderer,
|
||||
Theme: button::StyleSheet,
|
||||
{
|
||||
Button::new(content)
|
||||
}
|
||||
|
|
@ -136,7 +592,7 @@ pub fn tooltip<'a, Message, Theme, Renderer>(
|
|||
position: tooltip::Position,
|
||||
) -> crate::Tooltip<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet + text::StyleSheet,
|
||||
Theme: container::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
Tooltip::new(content, tooltip, position)
|
||||
|
|
@ -146,13 +602,26 @@ where
|
|||
///
|
||||
/// [`Text`]: core::widget::Text
|
||||
pub fn text<'a, Theme, Renderer>(
|
||||
text: impl ToString,
|
||||
text: impl text::IntoFragment<'a>,
|
||||
) -> Text<'a, Theme, Renderer>
|
||||
where
|
||||
Theme: text::StyleSheet,
|
||||
Theme: text::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
Text::new(text.to_string())
|
||||
Text::new(text)
|
||||
}
|
||||
|
||||
/// Creates a new [`Text`] widget that displays the provided value.
|
||||
///
|
||||
/// [`Text`]: core::widget::Text
|
||||
pub fn value<'a, Theme, Renderer>(
|
||||
value: impl ToString,
|
||||
) -> Text<'a, Theme, Renderer>
|
||||
where
|
||||
Theme: text::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
Text::new(value.to_string())
|
||||
}
|
||||
|
||||
/// Creates a new [`Checkbox`].
|
||||
|
|
@ -163,7 +632,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>(
|
|||
is_checked: bool,
|
||||
) -> Checkbox<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: checkbox::StyleSheet + text::StyleSheet,
|
||||
Theme: checkbox::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
Checkbox::new(label, is_checked)
|
||||
|
|
@ -172,15 +641,15 @@ where
|
|||
/// Creates a new [`Radio`].
|
||||
///
|
||||
/// [`Radio`]: crate::Radio
|
||||
pub fn radio<Message, Theme, Renderer, V>(
|
||||
pub fn radio<'a, Message, Theme, Renderer, V>(
|
||||
label: impl Into<String>,
|
||||
value: V,
|
||||
selected: Option<V>,
|
||||
on_click: impl FnOnce(V) -> Message,
|
||||
) -> Radio<Message, Theme, Renderer>
|
||||
) -> Radio<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: radio::StyleSheet,
|
||||
Theme: radio::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
V: Copy + Eq,
|
||||
{
|
||||
|
|
@ -196,8 +665,8 @@ pub fn toggler<'a, Message, Theme, Renderer>(
|
|||
f: impl Fn(bool) -> Message + 'a,
|
||||
) -> Toggler<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: toggler::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
Theme: toggler::StyleSheet,
|
||||
{
|
||||
Toggler::new(label, is_checked, f)
|
||||
}
|
||||
|
|
@ -211,7 +680,7 @@ pub fn text_input<'a, Message, Theme, Renderer>(
|
|||
) -> TextInput<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: text_input::StyleSheet,
|
||||
Theme: text_input::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
TextInput::new(placeholder, value)
|
||||
|
|
@ -220,12 +689,12 @@ where
|
|||
/// Creates a new [`TextEditor`].
|
||||
///
|
||||
/// [`TextEditor`]: crate::TextEditor
|
||||
pub fn text_editor<Message, Theme, Renderer>(
|
||||
content: &text_editor::Content<Renderer>,
|
||||
) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Theme, Renderer>
|
||||
pub fn text_editor<'a, Message, Theme, Renderer>(
|
||||
content: &'a text_editor::Content<Renderer>,
|
||||
) -> TextEditor<'a, core::text::highlighter::PlainText, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: text_editor::StyleSheet,
|
||||
Theme: text_editor::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
TextEditor::new(content)
|
||||
|
|
@ -242,7 +711,7 @@ pub fn slider<'a, T, Message, Theme>(
|
|||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Theme: slider::StyleSheet,
|
||||
Theme: slider::Catalog + 'a,
|
||||
{
|
||||
Slider::new(range, value, on_change)
|
||||
}
|
||||
|
|
@ -258,7 +727,7 @@ pub fn vertical_slider<'a, T, Message, Theme>(
|
|||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Theme: slider::StyleSheet,
|
||||
Theme: vertical_slider::Catalog + 'a,
|
||||
{
|
||||
VerticalSlider::new(range, value, on_change)
|
||||
}
|
||||
|
|
@ -276,13 +745,8 @@ where
|
|||
L: Borrow<[T]> + 'a,
|
||||
V: Borrow<T> + 'a,
|
||||
Message: Clone,
|
||||
Theme: pick_list::Catalog + overlay::menu::Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
Theme: pick_list::StyleSheet
|
||||
+ scrollable::StyleSheet
|
||||
+ overlay::menu::StyleSheet
|
||||
+ container::StyleSheet,
|
||||
<Theme as overlay::menu::StyleSheet>::Style:
|
||||
From<<Theme as pick_list::StyleSheet>::Style>,
|
||||
{
|
||||
PickList::new(options, selected, on_selected)
|
||||
}
|
||||
|
|
@ -298,7 +762,7 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>(
|
|||
) -> ComboBox<'a, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: std::fmt::Display + Clone,
|
||||
Theme: text_input::StyleSheet + overlay::menu::StyleSheet,
|
||||
Theme: combo_box::Catalog + 'a,
|
||||
Renderer: core::text::Renderer,
|
||||
{
|
||||
ComboBox::new(state, placeholder, selection, on_selected)
|
||||
|
|
@ -323,9 +787,9 @@ pub fn vertical_space() -> Space {
|
|||
/// Creates a horizontal [`Rule`] with the given height.
|
||||
///
|
||||
/// [`Rule`]: crate::Rule
|
||||
pub fn horizontal_rule<Theme>(height: impl Into<Pixels>) -> Rule<Theme>
|
||||
pub fn horizontal_rule<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme>
|
||||
where
|
||||
Theme: rule::StyleSheet,
|
||||
Theme: rule::Catalog + 'a,
|
||||
{
|
||||
Rule::horizontal(height)
|
||||
}
|
||||
|
|
@ -333,9 +797,9 @@ where
|
|||
/// Creates a vertical [`Rule`] with the given width.
|
||||
///
|
||||
/// [`Rule`]: crate::Rule
|
||||
pub fn vertical_rule<Theme>(width: impl Into<Pixels>) -> Rule<Theme>
|
||||
pub fn vertical_rule<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme>
|
||||
where
|
||||
Theme: rule::StyleSheet,
|
||||
Theme: rule::Catalog + 'a,
|
||||
{
|
||||
Rule::vertical(width)
|
||||
}
|
||||
|
|
@ -347,12 +811,12 @@ where
|
|||
/// * the current value of the [`ProgressBar`].
|
||||
///
|
||||
/// [`ProgressBar`]: crate::ProgressBar
|
||||
pub fn progress_bar<Theme>(
|
||||
pub fn progress_bar<'a, Theme>(
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
) -> ProgressBar<Theme>
|
||||
) -> ProgressBar<'a, Theme>
|
||||
where
|
||||
Theme: progress_bar::StyleSheet,
|
||||
Theme: progress_bar::Catalog + 'a,
|
||||
{
|
||||
ProgressBar::new(range, value)
|
||||
}
|
||||
|
|
@ -370,9 +834,11 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
|
|||
/// [`Svg`]: crate::Svg
|
||||
/// [`Handle`]: crate::svg::Handle
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn svg<Theme>(handle: impl Into<core::svg::Handle>) -> crate::Svg<Theme>
|
||||
pub fn svg<'a, Theme>(
|
||||
handle: impl Into<core::svg::Handle>,
|
||||
) -> crate::Svg<'a, Theme>
|
||||
where
|
||||
Theme: crate::svg::StyleSheet,
|
||||
Theme: crate::svg::Catalog,
|
||||
{
|
||||
crate::Svg::new(handle)
|
||||
}
|
||||
|
|
@ -396,9 +862,11 @@ where
|
|||
/// [`QRCode`]: crate::QRCode
|
||||
/// [`Data`]: crate::qr_code::Data
|
||||
#[cfg(feature = "qr_code")]
|
||||
pub fn qr_code<Theme>(data: &crate::qr_code::Data) -> crate::QRCode<'_, Theme>
|
||||
pub fn qr_code<'a, Theme>(
|
||||
data: &'a crate::qr_code::Data,
|
||||
) -> crate::QRCode<'a, Theme>
|
||||
where
|
||||
Theme: crate::qr_code::StyleSheet,
|
||||
Theme: crate::qr_code::Catalog + 'a,
|
||||
{
|
||||
crate::QRCode::new(data)
|
||||
}
|
||||
|
|
@ -440,16 +908,23 @@ where
|
|||
MouseArea::new(widget)
|
||||
}
|
||||
|
||||
/// Creates a new [`Themer`].
|
||||
pub fn themer<'a, Message, Theme, Renderer>(
|
||||
theme: Theme,
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Themer<'a, Message, Theme, Renderer>
|
||||
/// A widget that applies any `Theme` to its contents.
|
||||
pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>(
|
||||
new_theme: NewTheme,
|
||||
content: impl Into<Element<'a, Message, NewTheme, Renderer>>,
|
||||
) -> Themer<
|
||||
'a,
|
||||
Message,
|
||||
OldTheme,
|
||||
NewTheme,
|
||||
impl Fn(&OldTheme) -> NewTheme,
|
||||
Renderer,
|
||||
>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Theme: application::StyleSheet,
|
||||
NewTheme: Clone,
|
||||
{
|
||||
Themer::new(theme, content)
|
||||
Themer::new(move |_| new_theme.clone(), content)
|
||||
}
|
||||
|
||||
/// Creates a new [`PaneGrid`].
|
||||
|
|
@ -463,7 +938,7 @@ pub fn pane_grid<'a, T, Message, Theme, Renderer>(
|
|||
) -> PaneGrid<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: core::Renderer,
|
||||
Theme: pane_grid::StyleSheet + container::StyleSheet,
|
||||
Theme: pane_grid::Catalog,
|
||||
{
|
||||
PaneGrid::new(state, view)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ use crate::core::mouse;
|
|||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget,
|
||||
ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
pub use image::{FilterMethod, Handle};
|
||||
|
||||
/// Creates a new [`Viewer`] with the given image `Handle`.
|
||||
|
|
@ -38,6 +37,8 @@ pub struct Image<Handle> {
|
|||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
filter_method: FilterMethod,
|
||||
rotation: Rotation,
|
||||
opacity: f32,
|
||||
}
|
||||
|
||||
impl<Handle> Image<Handle> {
|
||||
|
|
@ -47,8 +48,10 @@ impl<Handle> Image<Handle> {
|
|||
handle: handle.into(),
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
content_fit: ContentFit::Contain,
|
||||
content_fit: ContentFit::default(),
|
||||
filter_method: FilterMethod::default(),
|
||||
rotation: Rotation::default(),
|
||||
opacity: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +80,21 @@ impl<Handle> Image<Handle> {
|
|||
self.filter_method = filter_method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the given [`Rotation`] to the [`Image`].
|
||||
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
|
||||
self.rotation = rotation.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the opacity of the [`Image`].
|
||||
///
|
||||
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
|
||||
/// and `1.0` meaning completely opaque.
|
||||
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
|
||||
self.opacity = opacity.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of an [`Image`].
|
||||
|
|
@ -87,22 +105,24 @@ pub fn layout<Renderer, Handle>(
|
|||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
rotation: Rotation,
|
||||
) -> layout::Node
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
{
|
||||
// The raw w/h of the underlying image
|
||||
let image_size = {
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
let image_size = renderer.measure_image(handle);
|
||||
let image_size =
|
||||
Size::new(image_size.width as f32, image_size.height as f32);
|
||||
|
||||
Size::new(width as f32, height as f32)
|
||||
};
|
||||
// The rotated size of the image
|
||||
let rotated_size = rotation.apply(image_size);
|
||||
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits.resolve(width, height, image_size);
|
||||
let raw_size = limits.resolve(width, height, rotated_size);
|
||||
|
||||
// The uncropped size of the image when fit to the bounds above
|
||||
let full_size = content_fit.fit(image_size, raw_size);
|
||||
let full_size = content_fit.fit(rotated_size, raw_size);
|
||||
|
||||
// Shrink the widget to fit the resized image, if requested
|
||||
let final_size = Size {
|
||||
|
|
@ -126,29 +146,47 @@ pub fn draw<Renderer, Handle>(
|
|||
handle: &Handle,
|
||||
content_fit: ContentFit,
|
||||
filter_method: FilterMethod,
|
||||
rotation: Rotation,
|
||||
opacity: f32,
|
||||
) where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
Handle: Clone,
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
let Size { width, height } = renderer.measure_image(handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
let rotated_size = rotation.apply(image_size);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = content_fit.fit(image_size, bounds.size());
|
||||
let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
|
||||
|
||||
let scale = Vector::new(
|
||||
adjusted_fit.width / rotated_size.width,
|
||||
adjusted_fit.height / rotated_size.height,
|
||||
);
|
||||
|
||||
let final_size = image_size * scale;
|
||||
|
||||
let position = match content_fit {
|
||||
ContentFit::None => Point::new(
|
||||
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
|
||||
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
|
||||
),
|
||||
_ => Point::new(
|
||||
bounds.center_x() - final_size.width / 2.0,
|
||||
bounds.center_y() - final_size.height / 2.0,
|
||||
),
|
||||
};
|
||||
|
||||
let drawing_bounds = Rectangle::new(position, final_size);
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
let offset = Vector::new(
|
||||
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
|
||||
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
|
||||
renderer.draw_image(
|
||||
handle.clone(),
|
||||
filter_method,
|
||||
drawing_bounds,
|
||||
rotation.radians(),
|
||||
opacity,
|
||||
);
|
||||
|
||||
let drawing_bounds = Rectangle {
|
||||
width: adjusted_fit.width,
|
||||
height: adjusted_fit.height,
|
||||
..bounds
|
||||
};
|
||||
|
||||
renderer.draw(handle.clone(), filter_method, drawing_bounds + offset);
|
||||
};
|
||||
|
||||
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
|
||||
|
|
@ -163,7 +201,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
|
|||
for Image<Handle>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
Handle: Clone,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
|
|
@ -185,6 +223,7 @@ where
|
|||
self.width,
|
||||
self.height,
|
||||
self.content_fit,
|
||||
self.rotation,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +243,8 @@ where
|
|||
&self.handle,
|
||||
self.content_fit,
|
||||
self.filter_method,
|
||||
self.rotation,
|
||||
self.opacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -212,7 +253,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash + 'a,
|
||||
Handle: Clone + 'a,
|
||||
{
|
||||
fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(image)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ use crate::core::mouse;
|
|||
use crate::core::renderer;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
/// A frame that displays an image with the ability to zoom in/out and pan.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Viewer<Handle> {
|
||||
|
|
@ -40,6 +38,12 @@ impl<Handle> Viewer<Handle> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets the [`image::FilterMethod`] of the [`Viewer`].
|
||||
pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
|
||||
self.filter_method = filter_method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding of the [`Viewer`].
|
||||
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
|
||||
self.padding = padding.into().0;
|
||||
|
|
@ -88,7 +92,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
|
|||
for Viewer<Handle>
|
||||
where
|
||||
Renderer: image::Renderer<Handle = Handle>,
|
||||
Handle: Clone + Hash,
|
||||
Handle: Clone,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
|
|
@ -111,7 +115,7 @@ where
|
|||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let Size { width, height } = renderer.measure_image(&self.handle);
|
||||
|
||||
let mut size = limits.resolve(
|
||||
self.width,
|
||||
|
|
@ -126,7 +130,7 @@ where
|
|||
};
|
||||
|
||||
// Only calculate viewport sizes if the images are constrained to a limited space.
|
||||
// If they are Fill|Portion let them expand within their alotted space.
|
||||
// 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;
|
||||
|
|
@ -212,7 +216,7 @@ where
|
|||
event::Status::Captured
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let Some(cursor_position) = cursor.position() else {
|
||||
let Some(cursor_position) = cursor.position_over(bounds) else {
|
||||
return event::Status::Ignored;
|
||||
};
|
||||
|
||||
|
|
@ -298,7 +302,7 @@ where
|
|||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::Idle
|
||||
mouse::Interaction::None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,8 +333,7 @@ where
|
|||
|
||||
renderer.with_layer(bounds, |renderer| {
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
image::Renderer::draw(
|
||||
renderer,
|
||||
renderer.draw_image(
|
||||
self.handle.clone(),
|
||||
self.filter_method,
|
||||
Rectangle {
|
||||
|
|
@ -338,6 +341,8 @@ where
|
|||
y: bounds.y,
|
||||
..Rectangle::with_size(image_size)
|
||||
},
|
||||
Radians(0.0),
|
||||
1.0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -396,7 +401,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
|
|||
where
|
||||
Renderer: 'a + image::Renderer<Handle = Handle>,
|
||||
Message: 'a,
|
||||
Handle: Clone + Hash + 'a,
|
||||
Handle: Clone + 'a,
|
||||
{
|
||||
fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(viewer)
|
||||
|
|
@ -415,7 +420,7 @@ pub fn image_size<Renderer>(
|
|||
where
|
||||
Renderer: image::Renderer,
|
||||
{
|
||||
let Size { width, height } = renderer.dimensions(handle);
|
||||
let Size { width, height } = renderer.measure_image(handle);
|
||||
|
||||
let (width, height) = {
|
||||
let dimensions = (width as f32, height as f32);
|
||||
|
|
|
|||
|
|
@ -40,27 +40,49 @@ where
|
|||
{
|
||||
/// Creates an empty [`Column`].
|
||||
pub fn new() -> Self {
|
||||
Column {
|
||||
Self::from_vecs(Vec::new(), Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] from already allocated [`Vec`]s.
|
||||
///
|
||||
/// Keep in mind that the [`Column`] will not inspect the [`Vec`]s, which means
|
||||
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||
///
|
||||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||
/// call [`Column::width`] or [`Column::height`] accordingly.
|
||||
pub fn from_vecs(
|
||||
keys: Vec<Key>,
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
align_items: Alignment::Start,
|
||||
keys: Vec::new(),
|
||||
children: Vec::new(),
|
||||
keys,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self::from_vecs(
|
||||
Vec::with_capacity(capacity),
|
||||
Vec::with_capacity(capacity),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<
|
||||
Item = (Key, Element<'a, Message, Theme, Renderer>),
|
||||
>,
|
||||
) -> Self {
|
||||
children
|
||||
.into_iter()
|
||||
.fold(Self::new(), |column, (key, child)| column.push(key, child))
|
||||
let iterator = children.into_iter();
|
||||
|
||||
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||
}
|
||||
|
||||
/// Sets the vertical spacing _between_ elements.
|
||||
|
|
@ -132,6 +154,18 @@ where
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the [`Column`] with the given children.
|
||||
pub fn extend(
|
||||
self,
|
||||
children: impl IntoIterator<
|
||||
Item = (Key, Element<'a, Message, Theme, Renderer>),
|
||||
>,
|
||||
) -> Self {
|
||||
children
|
||||
.into_iter()
|
||||
.fold(self, |column, (key, child)| column.push(key, child))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
|
||||
|
|
@ -190,7 +224,7 @@ where
|
|||
);
|
||||
|
||||
if state.keys != self.keys {
|
||||
state.keys = self.keys.clone();
|
||||
state.keys.clone_from(&self.keys);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ use crate::core::widget::tree::{self, Tree};
|
|||
use crate::core::widget::{self, Widget};
|
||||
use crate::core::Element;
|
||||
use crate::core::{
|
||||
self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector,
|
||||
self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector,
|
||||
};
|
||||
use crate::runtime::overlay::Nested;
|
||||
|
||||
use ouroboros::self_referencing;
|
||||
use rustc_hash::FxHasher;
|
||||
use std::cell::RefCell;
|
||||
use std::hash::{Hash, Hasher as H};
|
||||
use std::rc::Rc;
|
||||
|
|
@ -106,9 +107,12 @@ where
|
|||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
let mut hasher = Hasher::default();
|
||||
self.dependency.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
let hash = {
|
||||
let mut hasher = FxHasher::default();
|
||||
self.dependency.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
let element =
|
||||
Rc::new(RefCell::new(Some((self.view)(&self.dependency).into())));
|
||||
|
|
@ -127,9 +131,12 @@ where
|
|||
.state
|
||||
.downcast_mut::<Internal<Message, Theme, Renderer>>();
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
self.dependency.hash(&mut hasher);
|
||||
let new_hash = hasher.finish();
|
||||
let new_hash = {
|
||||
let mut hasher = FxHasher::default();
|
||||
self.dependency.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
if current.hash != new_hash {
|
||||
current.hash = new_hash;
|
||||
|
|
|
|||
|
|
@ -478,12 +478,14 @@ where
|
|||
translation: Vector,
|
||||
) -> 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 overlay = Overlay(Some(
|
||||
InnerBuilder {
|
||||
instance: self,
|
||||
|
|
|
|||
|
|
@ -308,10 +308,13 @@ where
|
|||
content_layout_node.as_ref().unwrap(),
|
||||
);
|
||||
|
||||
element
|
||||
.as_widget_mut()
|
||||
.overlay(tree, content_layout, renderer, translation)
|
||||
.map(|overlay| RefCell::new(Nested::new(overlay)))
|
||||
(
|
||||
element
|
||||
.as_widget_mut()
|
||||
.overlay(tree, content_layout, renderer, translation)
|
||||
.map(|overlay| RefCell::new(Nested::new(overlay))),
|
||||
content_layout_node,
|
||||
)
|
||||
},
|
||||
}
|
||||
.build();
|
||||
|
|
@ -341,7 +344,10 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> {
|
|||
|
||||
#[borrows(mut content, mut tree)]
|
||||
#[not_covariant]
|
||||
overlay: Option<RefCell<Nested<'this, Message, Theme, Renderer>>>,
|
||||
overlay: (
|
||||
Option<RefCell<Nested<'this, Message, Theme, Renderer>>>,
|
||||
&'this mut Option<layout::Node>,
|
||||
),
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
|
|
@ -351,7 +357,7 @@ impl<'a, 'b, Message, Theme, Renderer>
|
|||
&self,
|
||||
f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.with_overlay(|overlay| {
|
||||
self.with_overlay(|(overlay, _layout)| {
|
||||
overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
|
||||
})
|
||||
}
|
||||
|
|
@ -360,7 +366,7 @@ impl<'a, 'b, Message, Theme, Renderer>
|
|||
&mut self,
|
||||
f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T,
|
||||
) -> Option<T> {
|
||||
self.with_overlay_mut(|overlay| {
|
||||
self.with_overlay_mut(|(overlay, _layout)| {
|
||||
overlay.as_mut().map(|nested| (f)(nested.get_mut()))
|
||||
})
|
||||
}
|
||||
|
|
@ -412,10 +418,27 @@ where
|
|||
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 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,
|
||||
);
|
||||
|
||||
is_layout_invalid = shell.is_layout_invalid();
|
||||
|
||||
event_status
|
||||
})
|
||||
.unwrap_or(event::Status::Ignored);
|
||||
|
||||
if is_layout_invalid {
|
||||
self.with_overlay_mut(|(_overlay, layout)| {
|
||||
**layout = None;
|
||||
});
|
||||
}
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn is_over(
|
||||
|
|
|
|||
|
|
@ -2,23 +2,17 @@
|
|||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
|
||||
)]
|
||||
#![forbid(unsafe_code, rust_2018_idioms)]
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
unused_results,
|
||||
rustdoc::broken_intra_doc_links
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
pub use iced_renderer as renderer;
|
||||
pub use iced_renderer::graphics;
|
||||
pub use iced_runtime as runtime;
|
||||
pub use iced_runtime::core;
|
||||
pub use iced_style as style;
|
||||
|
||||
mod column;
|
||||
mod mouse_area;
|
||||
mod row;
|
||||
mod space;
|
||||
mod stack;
|
||||
mod themer;
|
||||
|
||||
pub mod button;
|
||||
|
|
@ -34,7 +28,6 @@ pub mod radio;
|
|||
pub mod rule;
|
||||
pub mod scrollable;
|
||||
pub mod slider;
|
||||
pub mod space;
|
||||
pub mod text;
|
||||
pub mod text_editor;
|
||||
pub mod text_input;
|
||||
|
|
@ -86,6 +79,8 @@ pub use slider::Slider;
|
|||
#[doc(no_inline)]
|
||||
pub use space::Space;
|
||||
#[doc(no_inline)]
|
||||
pub use stack::Stack;
|
||||
#[doc(no_inline)]
|
||||
pub use text::Text;
|
||||
#[doc(no_inline)]
|
||||
pub use text_editor::TextEditor;
|
||||
|
|
@ -135,5 +130,5 @@ pub mod qr_code;
|
|||
#[doc(no_inline)]
|
||||
pub use qr_code::QRCode;
|
||||
|
||||
pub use crate::core::theme::{self, Theme};
|
||||
pub use renderer::Renderer;
|
||||
pub use style::theme::{self, Theme};
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ where
|
|||
);
|
||||
|
||||
match (self.interaction, content_interaction) {
|
||||
(Some(interaction), mouse::Interaction::Idle)
|
||||
(Some(interaction), mouse::Interaction::None)
|
||||
if cursor.is_over(layout.bounds()) =>
|
||||
{
|
||||
interaction
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//! Build and show dropdown menus.
|
||||
use crate::container::{self, Container};
|
||||
use crate::core::alignment;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::layout::{self, Layout};
|
||||
|
|
@ -10,24 +9,25 @@ use crate::core::text::{self, Text};
|
|||
use crate::core::touch;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
Border, Clipboard, Length, Padding, Pixels, Point, Rectangle, Size, Vector,
|
||||
Background, Border, Clipboard, Color, Length, Padding, Pixels, Point,
|
||||
Rectangle, Size, Theme, Vector,
|
||||
};
|
||||
use crate::core::{Element, Shell, Widget};
|
||||
use crate::scrollable::{self, Scrollable};
|
||||
|
||||
pub use iced_style::menu::{Appearance, StyleSheet};
|
||||
|
||||
/// A list of selectable options.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Menu<
|
||||
'a,
|
||||
'b,
|
||||
T,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
'b: 'a,
|
||||
{
|
||||
state: &'a mut State,
|
||||
options: &'a [T],
|
||||
|
|
@ -40,24 +40,27 @@ pub struct Menu<
|
|||
text_line_height: text::LineHeight,
|
||||
text_shaping: text::Shaping,
|
||||
font: Option<Renderer::Font>,
|
||||
style: Theme::Style,
|
||||
class: &'a <Theme as Catalog>::Class<'b>,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer>
|
||||
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||
Menu<'a, 'b, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: ToString + Clone,
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
'b: 'a,
|
||||
{
|
||||
/// Creates a new [`Menu`] with the given [`State`], a list of options, and
|
||||
/// the message to produced when an option is selected.
|
||||
/// Creates a new [`Menu`] with the given [`State`], a list of options,
|
||||
/// the message to produced when an option is selected, and its [`Style`].
|
||||
pub fn new(
|
||||
state: &'a mut State,
|
||||
options: &'a [T],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
on_selected: impl FnMut(T) -> Message + 'a,
|
||||
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
|
||||
class: &'a <Theme as Catalog>::Class<'b>,
|
||||
) -> Self {
|
||||
Menu {
|
||||
state,
|
||||
|
|
@ -71,7 +74,7 @@ where
|
|||
text_line_height: text::LineHeight::default(),
|
||||
text_shaping: text::Shaping::Basic,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
class,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,15 +117,6 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Menu`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Theme as StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Turns the [`Menu`] into an overlay [`Element`] at the given target
|
||||
/// position.
|
||||
///
|
||||
|
|
@ -163,28 +157,29 @@ impl Default for State {
|
|||
}
|
||||
}
|
||||
|
||||
struct Overlay<'a, Message, Theme, Renderer>
|
||||
struct Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet + container::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
position: Point,
|
||||
state: &'a mut Tree,
|
||||
container: Container<'a, Message, Theme, Renderer>,
|
||||
list: Scrollable<'a, Message, Theme, Renderer>,
|
||||
width: f32,
|
||||
target_height: f32,
|
||||
style: <Theme as StyleSheet>::Style,
|
||||
class: &'a <Theme as Catalog>::Class<'b>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer>
|
||||
impl<'a, 'b, Message, Theme, Renderer> Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a,
|
||||
Theme: Catalog + scrollable::Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
'b: 'a,
|
||||
{
|
||||
pub fn new<T>(
|
||||
position: Point,
|
||||
menu: Menu<'a, T, Message, Theme, Renderer>,
|
||||
menu: Menu<'a, 'b, T, Message, Theme, Renderer>,
|
||||
target_height: f32,
|
||||
) -> Self
|
||||
where
|
||||
|
|
@ -202,40 +197,43 @@ where
|
|||
text_size,
|
||||
text_line_height,
|
||||
text_shaping,
|
||||
style,
|
||||
class,
|
||||
} = menu;
|
||||
|
||||
let container = Container::new(Scrollable::new(List {
|
||||
options,
|
||||
hovered_option,
|
||||
on_selected,
|
||||
on_option_hovered,
|
||||
font,
|
||||
text_size,
|
||||
text_line_height,
|
||||
text_shaping,
|
||||
padding,
|
||||
style: style.clone(),
|
||||
}));
|
||||
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(),
|
||||
);
|
||||
|
||||
state.tree.diff(&container as &dyn Widget<_, _, _>);
|
||||
state.tree.diff(&list as &dyn Widget<_, _, _>);
|
||||
|
||||
Self {
|
||||
position,
|
||||
state: &mut state.tree,
|
||||
container,
|
||||
list,
|
||||
width,
|
||||
target_height,
|
||||
style,
|
||||
class,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
crate::core::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, Message, Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet + container::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
|
|
@ -256,7 +254,7 @@ where
|
|||
)
|
||||
.width(self.width);
|
||||
|
||||
let node = self.container.layout(self.state, renderer, &limits);
|
||||
let node = self.list.layout(self.state, renderer, &limits);
|
||||
let size = node.size();
|
||||
|
||||
node.move_to(if space_below > space_above {
|
||||
|
|
@ -277,7 +275,7 @@ where
|
|||
) -> event::Status {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
self.container.on_event(
|
||||
self.list.on_event(
|
||||
self.state, event, layout, cursor, renderer, clipboard, shell,
|
||||
&bounds,
|
||||
)
|
||||
|
|
@ -290,7 +288,7 @@ where
|
|||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.container
|
||||
self.list
|
||||
.mouse_interaction(self.state, layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
|
|
@ -298,30 +296,32 @@ where
|
|||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
let appearance = StyleSheet::appearance(theme, &self.style);
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let style = Catalog::style(theme, self.class);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: appearance.border,
|
||||
border: style.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
appearance.background,
|
||||
style.background,
|
||||
);
|
||||
|
||||
self.container
|
||||
.draw(self.state, renderer, theme, style, layout, cursor, &bounds);
|
||||
self.list.draw(
|
||||
self.state, renderer, theme, defaults, layout, cursor, &bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct List<'a, T, Message, Theme, Renderer>
|
||||
struct List<'a, 'b, T, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
options: &'a [T],
|
||||
|
|
@ -333,14 +333,14 @@ where
|
|||
text_line_height: text::LineHeight,
|
||||
text_shaping: text::Shaping,
|
||||
font: Option<Renderer::Font>,
|
||||
style: Theme::Style,
|
||||
class: &'a <Theme as Catalog>::Class<'b>,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for List<'a, T, Message, Theme, Renderer>
|
||||
impl<'a, 'b, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for List<'a, 'b, T, Message, Theme, Renderer>
|
||||
where
|
||||
T: Clone + ToString,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
|
|
@ -483,7 +483,7 @@ where
|
|||
_cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let appearance = theme.appearance(&self.style);
|
||||
let style = Catalog::style(theme, self.class);
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let text_size =
|
||||
|
|
@ -513,20 +513,20 @@ where
|
|||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + appearance.border.width,
|
||||
width: bounds.width - appearance.border.width * 2.0,
|
||||
x: bounds.x + style.border.width,
|
||||
width: bounds.width - style.border.width * 2.0,
|
||||
..bounds
|
||||
},
|
||||
border: Border::with_radius(appearance.border.radius),
|
||||
border: Border::rounded(style.border.radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
appearance.selected_background,
|
||||
style.selected_background,
|
||||
);
|
||||
}
|
||||
|
||||
renderer.fill_text(
|
||||
Text {
|
||||
content: &option.to_string(),
|
||||
content: option.to_string(),
|
||||
bounds: Size::new(f32::INFINITY, bounds.height),
|
||||
size: text_size,
|
||||
line_height: self.text_line_height,
|
||||
|
|
@ -537,9 +537,9 @@ where
|
|||
},
|
||||
Point::new(bounds.x + self.padding.left, bounds.center_y()),
|
||||
if is_selected {
|
||||
appearance.selected_text_color
|
||||
style.selected_text_color
|
||||
} else {
|
||||
appearance.text_color
|
||||
style.text_color
|
||||
},
|
||||
*viewport,
|
||||
);
|
||||
|
|
@ -547,16 +547,81 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer>
|
||||
From<List<'a, T, Message, Theme, Renderer>>
|
||||
impl<'a, 'b, T, Message, Theme, Renderer>
|
||||
From<List<'a, 'b, T, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
T: ToString + Clone,
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Theme: 'a + Catalog,
|
||||
Renderer: 'a + text::Renderer,
|
||||
'b: 'a,
|
||||
{
|
||||
fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self {
|
||||
fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self {
|
||||
Element::new(list)
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of a [`Menu`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the menu.
|
||||
pub background: Background,
|
||||
/// The [`Border`] of the menu.
|
||||
pub border: Border,
|
||||
/// The text [`Color`] of the menu.
|
||||
pub text_color: Color,
|
||||
/// The text [`Color`] of a selected option in the menu.
|
||||
pub selected_text_color: Color,
|
||||
/// The background [`Color`] of a selected option in the menu.
|
||||
pub selected_background: Background,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Menu`].
|
||||
pub trait Catalog: scrollable::Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> <Self as Catalog>::Class<'a>;
|
||||
|
||||
/// The default class for the scrollable of the [`Menu`].
|
||||
fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
|
||||
<Self as scrollable::Catalog>::default()
|
||||
}
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Menu`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> StyleFn<'a, Self> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &StyleFn<'_, Self>) -> Style {
|
||||
class(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of the list of a [`Menu`].
|
||||
pub fn default(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
background: palette.background.weak.color.into(),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
color: palette.background.strong.color,
|
||||
},
|
||||
text_color: palette.background.weak.text,
|
||||
selected_text_color: palette.primary.strong.text,
|
||||
selected_background: palette.primary.strong.color.into(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@ use crate::core::overlay;
|
|||
use crate::core::renderer;
|
||||
use crate::core::widget::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
|
||||
self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
|
||||
};
|
||||
use crate::pane_grid::{Draggable, TitleBar};
|
||||
|
||||
|
|
@ -20,29 +20,29 @@ pub struct Content<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>,
|
||||
body: Element<'a, Message, Theme, Renderer>,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates a new [`Content`] with the provided body.
|
||||
pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
|
||||
Self {
|
||||
title_bar: None,
|
||||
body: body.into(),
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`TitleBar`] of this [`Content`].
|
||||
/// Sets the [`TitleBar`] of the [`Content`].
|
||||
pub fn title_bar(
|
||||
mut self,
|
||||
title_bar: TitleBar<'a, Message, Theme, Renderer>,
|
||||
|
|
@ -52,16 +52,31 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`Content`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme) -> container::Style + 'a,
|
||||
) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Content`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
pub(super) fn state(&self) -> Tree {
|
||||
let children = if let Some(title_bar) = self.title_bar.as_ref() {
|
||||
|
|
@ -90,7 +105,7 @@ where
|
|||
|
||||
/// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
|
||||
///
|
||||
/// [`Renderer`]: crate::core::Renderer
|
||||
/// [`Renderer`]: core::Renderer
|
||||
pub fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
|
|
@ -104,7 +119,7 @@ where
|
|||
let bounds = layout.bounds();
|
||||
|
||||
{
|
||||
let style = theme.appearance(&self.style);
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
container::draw_background(renderer, &style, bounds);
|
||||
}
|
||||
|
|
@ -370,8 +385,8 @@ where
|
|||
impl<'a, Message, Theme, Renderer> Draggable
|
||||
for &Content<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn can_be_dragged_at(
|
||||
&self,
|
||||
|
|
@ -393,8 +408,8 @@ impl<'a, T, Message, Theme, Renderer> From<T>
|
|||
for Content<'a, Message, Theme, Renderer>
|
||||
where
|
||||
T: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog + 'a,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn from(element: T) -> Self {
|
||||
Self::new(element)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::pane_grid::{
|
|||
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
|
||||
};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// The state of a [`PaneGrid`].
|
||||
///
|
||||
|
|
@ -25,7 +25,7 @@ pub struct State<T> {
|
|||
/// The panes of the [`PaneGrid`].
|
||||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub panes: HashMap<Pane, T>,
|
||||
pub panes: FxHashMap<Pane, T>,
|
||||
|
||||
/// The internal state of the [`PaneGrid`].
|
||||
///
|
||||
|
|
@ -52,7 +52,7 @@ 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 = HashMap::new();
|
||||
let mut panes = FxHashMap::default();
|
||||
|
||||
let internal =
|
||||
Internal::from_configuration(&mut panes, config.into(), 0);
|
||||
|
|
@ -353,7 +353,7 @@ impl Internal {
|
|||
///
|
||||
/// [`PaneGrid`]: super::PaneGrid
|
||||
pub fn from_configuration<T>(
|
||||
panes: &mut HashMap<Pane, T>,
|
||||
panes: &mut FxHashMap<Pane, T>,
|
||||
content: Configuration<T>,
|
||||
next_id: usize,
|
||||
) -> Self {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use crate::core::overlay;
|
|||
use crate::core::renderer;
|
||||
use crate::core::widget::{self, Tree};
|
||||
use crate::core::{
|
||||
Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, Vector,
|
||||
self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
|
||||
Vector,
|
||||
};
|
||||
|
||||
/// The title bar of a [`Pane`].
|
||||
|
|
@ -19,32 +20,31 @@ pub struct TitleBar<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
controls: Option<Element<'a, Message, Theme, Renderer>>,
|
||||
padding: Padding,
|
||||
always_show_controls: bool,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
/// Creates a new [`TitleBar`] with the given content.
|
||||
pub fn new<E>(content: E) -> Self
|
||||
where
|
||||
E: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
{
|
||||
pub fn new(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
controls: None,
|
||||
padding: Padding::ZERO,
|
||||
always_show_controls: false,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,12 +63,6 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`TitleBar`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
|
||||
/// always visible.
|
||||
///
|
||||
|
|
@ -81,12 +75,33 @@ where
|
|||
self.always_show_controls = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`TitleBar`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme) -> container::Style + 'a,
|
||||
) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`TitleBar`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: container::Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
pub(super) fn state(&self) -> Tree {
|
||||
let children = if let Some(controls) = self.controls.as_ref() {
|
||||
|
|
@ -115,7 +130,7 @@ where
|
|||
|
||||
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
|
||||
///
|
||||
/// [`Renderer`]: crate::core::Renderer
|
||||
/// [`Renderer`]: core::Renderer
|
||||
pub fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
|
|
@ -128,7 +143,8 @@ where
|
|||
show_controls: bool,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.appearance(&self.style);
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
let inherited_style = renderer::Style {
|
||||
text_color: style.text_color.unwrap_or(inherited_style.text_color),
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,17 +3,18 @@ use crate::core::layout;
|
|||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{Border, Element, Layout, Length, Rectangle, Size, Widget};
|
||||
use crate::core::{
|
||||
self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme,
|
||||
Widget,
|
||||
};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::progress_bar::{Appearance, StyleSheet};
|
||||
|
||||
/// A bar that displays progress.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type ProgressBar = iced_widget::ProgressBar<iced_widget::style::Theme>;
|
||||
/// # type ProgressBar<'a> = iced_widget::ProgressBar<'a>;
|
||||
/// #
|
||||
/// let value = 50.0;
|
||||
///
|
||||
|
|
@ -22,20 +23,20 @@ pub use iced_style::progress_bar::{Appearance, StyleSheet};
|
|||
///
|
||||
/// 
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ProgressBar<Theme = crate::Theme>
|
||||
pub struct ProgressBar<'a, Theme = crate::Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
range: RangeInclusive<f32>,
|
||||
value: f32,
|
||||
width: Length,
|
||||
height: Option<Length>,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<Theme> ProgressBar<Theme>
|
||||
impl<'a, Theme> ProgressBar<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// The default height of a [`ProgressBar`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 30.0;
|
||||
|
|
@ -51,7 +52,7 @@ where
|
|||
range,
|
||||
width: Length::Fill,
|
||||
height: None,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,17 +69,29 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`ProgressBar`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`ProgressBar`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ProgressBar<Theme>
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ProgressBar<'a, Theme>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
|
|
@ -120,12 +133,12 @@ where
|
|||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
let style = theme.appearance(&self.style);
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle { ..bounds },
|
||||
border: Border::with_radius(style.border_radius),
|
||||
border: style.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.background,
|
||||
|
|
@ -138,7 +151,7 @@ where
|
|||
width: active_progress_width,
|
||||
..bounds
|
||||
},
|
||||
border: Border::with_radius(style.border_radius),
|
||||
border: Border::rounded(style.border.radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.bar,
|
||||
|
|
@ -147,16 +160,101 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<ProgressBar<Theme>>
|
||||
impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Theme: 'a + Catalog,
|
||||
Renderer: 'a + core::Renderer,
|
||||
{
|
||||
fn from(
|
||||
progress_bar: ProgressBar<Theme>,
|
||||
progress_bar: ProgressBar<'a, Theme>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(progress_bar)
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of a progress bar.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the progress bar.
|
||||
pub background: Background,
|
||||
/// The [`Background`] of the bar of the progress bar.
|
||||
pub bar: Background,
|
||||
/// The [`Border`] of the progress bar.
|
||||
pub border: Border,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`ProgressBar`].
|
||||
pub trait Catalog: Sized {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`ProgressBar`].
|
||||
///
|
||||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(primary)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||
class(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The primary style of a [`ProgressBar`].
|
||||
pub fn primary(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
styled(
|
||||
palette.background.strong.color,
|
||||
palette.primary.strong.color,
|
||||
)
|
||||
}
|
||||
|
||||
/// The secondary style of a [`ProgressBar`].
|
||||
pub fn secondary(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
styled(
|
||||
palette.background.strong.color,
|
||||
palette.secondary.base.color,
|
||||
)
|
||||
}
|
||||
|
||||
/// The success style of a [`ProgressBar`].
|
||||
pub fn success(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
styled(palette.background.strong.color, palette.success.base.color)
|
||||
}
|
||||
|
||||
/// The danger style of a [`ProgressBar`].
|
||||
pub fn danger(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
styled(palette.background.strong.color, palette.danger.base.color)
|
||||
}
|
||||
|
||||
fn styled(
|
||||
background: impl Into<Background>,
|
||||
bar: impl Into<Background>,
|
||||
) -> Style {
|
||||
Style {
|
||||
background: background.into(),
|
||||
bar: bar.into(),
|
||||
border: Border::rounded(2),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,41 +5,39 @@ use crate::core::mouse;
|
|||
use crate::core::renderer::{self, Renderer as _};
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||
Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector,
|
||||
Widget,
|
||||
};
|
||||
use crate::graphics::geometry::Renderer as _;
|
||||
use crate::Renderer;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::style::qr_code::{Appearance, StyleSheet};
|
||||
|
||||
const DEFAULT_CELL_SIZE: u16 = 4;
|
||||
const QUIET_ZONE: usize = 2;
|
||||
|
||||
/// A type of matrix barcode consisting of squares arranged in a grid which
|
||||
/// can be read by an imaging device, such as a camera.
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct QRCode<'a, Theme = crate::Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
data: &'a Data,
|
||||
cell_size: u16,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Theme> QRCode<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// Creates a new [`QRCode`] with the provided [`Data`].
|
||||
pub fn new(data: &'a Data) -> Self {
|
||||
Self {
|
||||
data,
|
||||
cell_size: DEFAULT_CELL_SIZE,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,15 +48,27 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`QRCode`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`QRCode`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
|
|
@ -102,13 +112,13 @@ where
|
|||
let bounds = layout.bounds();
|
||||
let side_length = self.data.width + 2 * QUIET_ZONE;
|
||||
|
||||
let appearance = theme.appearance(&self.style);
|
||||
let mut last_appearance = state.last_appearance.borrow_mut();
|
||||
let style = theme.style(&self.class);
|
||||
let mut last_style = state.last_style.borrow_mut();
|
||||
|
||||
if Some(appearance) != *last_appearance {
|
||||
if Some(style) != *last_style {
|
||||
self.data.cache.clear();
|
||||
|
||||
*last_appearance = Some(appearance);
|
||||
*last_style = Some(style);
|
||||
}
|
||||
|
||||
// Reuse cache if possible
|
||||
|
|
@ -120,7 +130,7 @@ where
|
|||
frame.fill_rectangle(
|
||||
Point::ORIGIN,
|
||||
Size::new(side_length as f32, side_length as f32),
|
||||
appearance.background,
|
||||
style.background,
|
||||
);
|
||||
|
||||
// Avoid drawing on the quiet zone
|
||||
|
|
@ -139,7 +149,7 @@ where
|
|||
frame.fill_rectangle(
|
||||
Point::new(column as f32, row as f32),
|
||||
Size::UNIT,
|
||||
appearance.cell,
|
||||
style.cell,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -147,7 +157,9 @@ where
|
|||
renderer.with_translation(
|
||||
bounds.position() - Point::ORIGIN,
|
||||
|renderer| {
|
||||
renderer.draw(vec![geometry]);
|
||||
use crate::graphics::geometry::Renderer as _;
|
||||
|
||||
renderer.draw_geometry(geometry);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -156,7 +168,7 @@ where
|
|||
impl<'a, Message, Theme> From<QRCode<'a, Theme>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
{
|
||||
fn from(qr_code: QRCode<'a, Theme>) -> Self {
|
||||
Self::new(qr_code)
|
||||
|
|
@ -170,7 +182,7 @@ where
|
|||
pub struct Data {
|
||||
contents: Vec<qrcode::Color>,
|
||||
width: usize,
|
||||
cache: canvas::Cache,
|
||||
cache: canvas::Cache<Renderer>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
|
|
@ -328,5 +340,51 @@ impl From<qrcode::types::QrError> for Error {
|
|||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
last_appearance: RefCell<Option<Appearance>>,
|
||||
last_style: RefCell<Option<Style>>,
|
||||
}
|
||||
|
||||
/// The appearance of a QR code.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The color of the QR code data cells
|
||||
pub cell: Color,
|
||||
/// The color of the QR code background
|
||||
pub background: Color,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`QRCode`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`QRCode`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||
class(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of a [`QRCode`].
|
||||
pub fn default(theme: &Theme) -> Style {
|
||||
let palette = theme.palette();
|
||||
|
||||
Style {
|
||||
cell: palette.text,
|
||||
background: palette.background,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,16 @@ use crate::core::touch;
|
|||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size,
|
||||
Widget,
|
||||
Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
pub use iced_style::radio::{Appearance, StyleSheet};
|
||||
|
||||
/// A circular button representing a choice.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type Radio<Message> =
|
||||
/// # iced_widget::Radio<Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
|
||||
/// # type Radio<'a, Message> =
|
||||
/// # iced_widget::Radio<'a, Message, iced_widget::Theme, iced_widget::renderer::Renderer>;
|
||||
/// #
|
||||
/// # use iced_widget::column;
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -69,9 +67,9 @@ pub use iced_style::radio::{Appearance, StyleSheet};
|
|||
/// let content = column![a, b, c, all];
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Radio<Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
is_selected: bool,
|
||||
|
|
@ -84,20 +82,20 @@ where
|
|||
text_line_height: text::LineHeight,
|
||||
text_shaping: text::Shaping,
|
||||
font: Option<Renderer::Font>,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Radio<Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// The default size of a [`Radio`] button.
|
||||
pub const DEFAULT_SIZE: f32 = 28.0;
|
||||
pub const DEFAULT_SIZE: f32 = 16.0;
|
||||
|
||||
/// The default spacing of a [`Radio`] button.
|
||||
pub const DEFAULT_SPACING: f32 = 15.0;
|
||||
pub const DEFAULT_SPACING: f32 = 8.0;
|
||||
|
||||
/// Creates a new [`Radio`] button.
|
||||
///
|
||||
|
|
@ -128,7 +126,7 @@ where
|
|||
text_line_height: text::LineHeight::default(),
|
||||
text_shaping: text::Shaping::Basic,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,17 +176,29 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`Radio`] button.
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Radio`] button.
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Radio<Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Radio<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: StyleSheet + crate::text::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
|
|
@ -285,21 +295,24 @@ where
|
|||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
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 custom_style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_selected)
|
||||
let status = if is_mouse_over {
|
||||
Status::Hovered { is_selected }
|
||||
} else {
|
||||
theme.active(&self.style, self.is_selected)
|
||||
Status::Active { is_selected }
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
|
||||
{
|
||||
let layout = children.next().unwrap();
|
||||
let bounds = layout.bounds();
|
||||
|
|
@ -312,12 +325,12 @@ where
|
|||
bounds,
|
||||
border: Border {
|
||||
radius: (size / 2.0).into(),
|
||||
width: custom_style.border_width,
|
||||
color: custom_style.border_color,
|
||||
width: style.border_width,
|
||||
color: style.border_color,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
custom_style.background,
|
||||
style.background,
|
||||
);
|
||||
|
||||
if self.is_selected {
|
||||
|
|
@ -329,10 +342,10 @@ where
|
|||
width: bounds.width - dot_size,
|
||||
height: bounds.height - dot_size,
|
||||
},
|
||||
border: Border::with_radius(dot_size / 2.0),
|
||||
border: Border::rounded(dot_size / 2.0),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
custom_style.dot_color,
|
||||
style.dot_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -342,11 +355,11 @@ where
|
|||
|
||||
crate::text::draw(
|
||||
renderer,
|
||||
style,
|
||||
defaults,
|
||||
label_layout,
|
||||
tree.state.downcast_ref(),
|
||||
crate::text::Appearance {
|
||||
color: custom_style.text_color,
|
||||
crate::text::Style {
|
||||
color: style.text_color,
|
||||
},
|
||||
viewport,
|
||||
);
|
||||
|
|
@ -354,16 +367,95 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Radio<Message, Theme, Renderer>>
|
||||
impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Theme: StyleSheet + crate::text::StyleSheet + 'a,
|
||||
Theme: 'a + Catalog,
|
||||
Renderer: 'a + text::Renderer,
|
||||
{
|
||||
fn from(
|
||||
radio: Radio<Message, Theme, Renderer>,
|
||||
radio: Radio<'a, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(radio)
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible status of a [`Radio`] button.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Radio`] button can be interacted with.
|
||||
Active {
|
||||
/// Indicates whether the [`Radio`] button is currently selected.
|
||||
is_selected: bool,
|
||||
},
|
||||
/// The [`Radio`] button is being hovered.
|
||||
Hovered {
|
||||
/// Indicates whether the [`Radio`] button is currently selected.
|
||||
is_selected: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// The appearance of a radio button.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the radio button.
|
||||
pub background: Background,
|
||||
/// The [`Color`] of the dot of the radio button.
|
||||
pub dot_color: Color,
|
||||
/// The border width of the radio button.
|
||||
pub border_width: f32,
|
||||
/// The border [`Color`] of the radio button.
|
||||
pub border_color: Color,
|
||||
/// The text [`Color`] of the radio button.
|
||||
pub text_color: Option<Color>,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Radio`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Radio`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of a [`Radio`] button.
|
||||
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
let active = Style {
|
||||
background: Color::TRANSPARENT.into(),
|
||||
dot_color: palette.primary.strong.color,
|
||||
border_width: 1.0,
|
||||
border_color: palette.primary.strong.color,
|
||||
text_color: None,
|
||||
};
|
||||
|
||||
match status {
|
||||
Status::Active { .. } => active,
|
||||
Status::Hovered { .. } => Style {
|
||||
dot_color: palette.primary.strong.color,
|
||||
background: palette.primary.weak.color.into(),
|
||||
..active
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,18 @@ where
|
|||
Self::from_vec(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self::from_vec(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self::new().extend(children)
|
||||
let iterator = children.into_iter();
|
||||
|
||||
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||
}
|
||||
|
||||
/// Creates a [`Row`] from an already allocated [`Vec`].
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
//! Display a horizontal or vertical rule for dividing content.
|
||||
use crate::core;
|
||||
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::{
|
||||
Border, Element, Layout, Length, Pixels, Rectangle, Size, Widget,
|
||||
Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
pub use crate::style::rule::{Appearance, FillMode, StyleSheet};
|
||||
|
||||
/// Display a horizontal or vertical rule for dividing content.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Rule<Theme = crate::Theme>
|
||||
pub struct Rule<'a, Theme = crate::Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
width: Length,
|
||||
height: Length,
|
||||
is_horizontal: bool,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<Theme> Rule<Theme>
|
||||
impl<'a, Theme> Rule<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// Creates a horizontal [`Rule`] with the given height.
|
||||
pub fn horizontal(height: impl Into<Pixels>) -> Self {
|
||||
|
|
@ -31,7 +31,7 @@ where
|
|||
width: Length::Fill,
|
||||
height: Length::Fixed(height.into().0),
|
||||
is_horizontal: true,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,21 +41,34 @@ where
|
|||
width: Length::Fixed(width.into().0),
|
||||
height: Length::Fill,
|
||||
is_horizontal: false,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Rule`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Rule`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<Theme>
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Rule<'a, Theme>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: StyleSheet,
|
||||
Renderer: core::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
|
|
@ -84,7 +97,7 @@ where
|
|||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let style = theme.appearance(&self.style);
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
let bounds = if self.is_horizontal {
|
||||
let line_y = (bounds.y + (bounds.height / 2.0)
|
||||
|
|
@ -119,7 +132,7 @@ where
|
|||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: Border::with_radius(style.radius),
|
||||
border: Border::rounded(style.radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.color,
|
||||
|
|
@ -127,14 +140,132 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Rule<Theme>>
|
||||
impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
Theme: 'a + Catalog,
|
||||
Renderer: 'a + core::Renderer,
|
||||
{
|
||||
fn from(rule: Rule<Theme>) -> Element<'a, Message, Theme, Renderer> {
|
||||
fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(rule)
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of a rule.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The color of the rule.
|
||||
pub color: Color,
|
||||
/// The width (thickness) of the rule line.
|
||||
pub width: u16,
|
||||
/// The radius of the line corners.
|
||||
pub radius: border::Radius,
|
||||
/// The [`FillMode`] of the rule.
|
||||
pub fill_mode: FillMode,
|
||||
}
|
||||
|
||||
/// The fill mode of a rule.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FillMode {
|
||||
/// Fill the whole length of the container.
|
||||
Full,
|
||||
/// Fill a percent of the length of the container. The rule
|
||||
/// will be centered in that container.
|
||||
///
|
||||
/// The range is `[0.0, 100.0]`.
|
||||
Percent(f32),
|
||||
/// Uniform offset from each end, length units.
|
||||
Padded(u16),
|
||||
/// Different offset on each end of the rule, length units.
|
||||
/// First = top or left.
|
||||
AsymmetricPadding(u16, u16),
|
||||
}
|
||||
|
||||
impl FillMode {
|
||||
/// Return the starting offset and length of the rule.
|
||||
///
|
||||
/// * `space` - The space to fill.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * (`starting_offset`, `length`)
|
||||
pub fn fill(&self, space: f32) -> (f32, f32) {
|
||||
match *self {
|
||||
FillMode::Full => (0.0, space),
|
||||
FillMode::Percent(percent) => {
|
||||
if percent >= 100.0 {
|
||||
(0.0, space)
|
||||
} else {
|
||||
let percent_width = (space * percent / 100.0).round();
|
||||
|
||||
(((space - percent_width) / 2.0).round(), percent_width)
|
||||
}
|
||||
}
|
||||
FillMode::Padded(padding) => {
|
||||
if padding == 0 {
|
||||
(0.0, space)
|
||||
} else {
|
||||
let padding = padding as f32;
|
||||
let mut line_width = space - (padding * 2.0);
|
||||
if line_width < 0.0 {
|
||||
line_width = 0.0;
|
||||
}
|
||||
|
||||
(padding, line_width)
|
||||
}
|
||||
}
|
||||
FillMode::AsymmetricPadding(first_pad, second_pad) => {
|
||||
let first_pad = first_pad as f32;
|
||||
let second_pad = second_pad as f32;
|
||||
let mut line_width = space - first_pad - second_pad;
|
||||
if line_width < 0.0 {
|
||||
line_width = 0.0;
|
||||
}
|
||||
|
||||
(first_pad, line_width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Rule`].
|
||||
pub trait Catalog: Sized {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Rule`].
|
||||
///
|
||||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||
class(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default styling of a [`Rule`].
|
||||
pub fn default(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
color: palette.background.strong.color,
|
||||
width: 1,
|
||||
radius: 0.0.into(),
|
||||
fill_mode: FillMode::Full,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,12 +13,13 @@ 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::renderer::wgpu::primitive::pipeline;
|
||||
use crate::renderer::wgpu::primitive;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub use crate::graphics::Viewport;
|
||||
pub use crate::renderer::wgpu::wgpu;
|
||||
pub use pipeline::{Primitive, Storage};
|
||||
pub use primitive::{Primitive, Storage};
|
||||
|
||||
/// A widget which can render custom shaders with Iced's `wgpu` backend.
|
||||
///
|
||||
|
|
@ -60,7 +61,7 @@ impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
|||
for Shader<Message, P>
|
||||
where
|
||||
P: Program<Message>,
|
||||
Renderer: pipeline::Renderer,
|
||||
Renderer: primitive::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
struct Tag<T>(T);
|
||||
|
|
@ -160,7 +161,7 @@ where
|
|||
let bounds = layout.bounds();
|
||||
let state = tree.state.downcast_ref::<P::State>();
|
||||
|
||||
renderer.draw_pipeline_primitive(
|
||||
renderer.draw_primitive(
|
||||
bounds,
|
||||
self.program.draw(state, cursor_position, bounds),
|
||||
);
|
||||
|
|
@ -171,7 +172,7 @@ impl<'a, Message, Theme, Renderer, P> From<Shader<Message, P>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: pipeline::Renderer,
|
||||
Renderer: primitive::Renderer,
|
||||
P: Program<Message> + 'a,
|
||||
{
|
||||
fn from(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::core::event;
|
||||
use crate::core::mouse;
|
||||
use crate::core::{Rectangle, Shell};
|
||||
use crate::renderer::wgpu::primitive::pipeline;
|
||||
use crate::renderer::wgpu::Primitive;
|
||||
use crate::shader;
|
||||
|
||||
/// The state and logic of a [`Shader`] widget.
|
||||
|
|
@ -15,7 +15,7 @@ pub trait Program<Message> {
|
|||
type State: Default + 'static;
|
||||
|
||||
/// The type of primitive this [`Program`] can draw.
|
||||
type Primitive: pipeline::Primitive + 'static;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//!
|
||||
//! A [`Slider`] has some local [`State`].
|
||||
use crate::core::border;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::keyboard;
|
||||
use crate::core::keyboard::key::{self, Key};
|
||||
|
|
@ -10,16 +9,12 @@ use crate::core::renderer;
|
|||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
|
||||
Shell, Size, Widget,
|
||||
self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use iced_style::slider::{
|
||||
Appearance, Handle, HandleShape, Rail, StyleSheet,
|
||||
};
|
||||
|
||||
/// An horizontal bar and a handle that selects a single value from a range of
|
||||
/// values.
|
||||
///
|
||||
|
|
@ -30,8 +25,7 @@ pub use iced_style::slider::{
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type Slider<'a, T, Message> =
|
||||
/// # iced_widget::Slider<'a, Message, T, iced_widget::style::Theme>;
|
||||
/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
|
|
@ -47,7 +41,7 @@ pub use iced_style::slider::{
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct Slider<'a, T, Message, Theme = crate::Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
range: RangeInclusive<T>,
|
||||
step: T,
|
||||
|
|
@ -58,17 +52,17 @@ where
|
|||
on_release: Option<Message>,
|
||||
width: Length,
|
||||
height: f32,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
|
||||
where
|
||||
T: Copy + From<u8> + PartialOrd,
|
||||
Message: Clone,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// The default height of a [`Slider`].
|
||||
pub const DEFAULT_HEIGHT: f32 = 22.0;
|
||||
pub const DEFAULT_HEIGHT: f32 = 16.0;
|
||||
|
||||
/// Creates a new [`Slider`].
|
||||
///
|
||||
|
|
@ -104,7 +98,7 @@ where
|
|||
on_release: None,
|
||||
width: Length::Fill,
|
||||
height: Self::DEFAULT_HEIGHT,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,12 +133,6 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Slider`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the step size of the [`Slider`].
|
||||
pub fn step(mut self, step: impl Into<T>) -> Self {
|
||||
self.step = step.into();
|
||||
|
|
@ -158,6 +146,24 @@ where
|
|||
self.shift_step = Some(shift_step.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Slider`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Slider`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
|
|
@ -165,15 +171,15 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
|||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
|
|
@ -203,20 +209,143 @@ where
|
|||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
shell,
|
||||
tree.state.downcast_mut::<State>(),
|
||||
&mut self.value,
|
||||
self.default,
|
||||
&self.range,
|
||||
self.step,
|
||||
self.shift_step,
|
||||
self.on_change.as_ref(),
|
||||
&self.on_release,
|
||||
)
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
let is_dragging = state.is_dragging;
|
||||
let current_value = self.value;
|
||||
|
||||
let locate = |cursor_position: Point| -> Option<T> {
|
||||
let bounds = layout.bounds();
|
||||
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)
|
||||
};
|
||||
|
||||
new_value
|
||||
};
|
||||
|
||||
let increment = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps + 1.0);
|
||||
|
||||
if new_value > (*self.range.end()).into() {
|
||||
return Some(*self.range.end());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let decrement = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps - 1.0);
|
||||
|
||||
if new_value < (*self.range.start()).into() {
|
||||
return Some(*self.range.start());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let change = |new_value: T| {
|
||||
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((self.on_change)(new_value));
|
||||
|
||||
self.value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
if state.keyboard_modifiers.command() {
|
||||
let _ = self.default.map(change);
|
||||
state.is_dragging = false;
|
||||
} else {
|
||||
let _ = locate(cursor_position).map(change);
|
||||
state.is_dragging = true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
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) => {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
Key::Named(key::Named::ArrowDown) => {
|
||||
let _ = decrement(current_value).map(change);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
state.keyboard_modifiers = modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -229,15 +358,92 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
cursor,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
self.value,
|
||||
&self.range,
|
||||
theme,
|
||||
&self.style,
|
||||
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 (handle_width, handle_height, handle_border_radius) =
|
||||
match style.handle.shape {
|
||||
HandleShape::Circle { radius } => {
|
||||
(radius * 2.0, radius * 2.0, radius.into())
|
||||
}
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.height, border_radius),
|
||||
};
|
||||
|
||||
let value = self.value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = self.range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.width - handle_width) * (value - range_start)
|
||||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
let rail_y = bounds.y + bounds.height / 2.0;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: rail_y - style.rail.width / 2.0,
|
||||
width: offset + handle_width / 2.0,
|
||||
height: style.rail.width,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + offset + handle_width / 2.0,
|
||||
y: rail_y - style.rail.width / 2.0,
|
||||
width: bounds.width - offset - handle_width / 2.0,
|
||||
height: style.rail.width,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.1,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + offset,
|
||||
y: rail_y - handle_height / 2.0,
|
||||
width: handle_width,
|
||||
height: handle_height,
|
||||
},
|
||||
border: Border {
|
||||
radius: handle_border_radius,
|
||||
width: style.handle.border_width,
|
||||
color: style.handle.border_color,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +455,17 @@ where
|
|||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>())
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,8 +474,8 @@ impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
|
|||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
|
||||
Message: Clone + 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
slider: Slider<'a, T, Message, Theme>,
|
||||
|
|
@ -268,290 +484,132 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`Slider`]
|
||||
/// accordingly.
|
||||
pub fn update<Message, T>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
value: &mut T,
|
||||
default: Option<T>,
|
||||
range: &RangeInclusive<T>,
|
||||
step: T,
|
||||
shift_step: Option<T>,
|
||||
on_change: &dyn Fn(T) -> Message,
|
||||
on_release: &Option<Message>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
{
|
||||
let is_dragging = state.is_dragging;
|
||||
let current_value = *value;
|
||||
|
||||
let locate = |cursor_position: Point| -> Option<T> {
|
||||
let bounds = layout.bounds();
|
||||
let new_value = if cursor_position.x <= bounds.x {
|
||||
Some(*range.start())
|
||||
} else if cursor_position.x >= bounds.x + bounds.width {
|
||||
Some(*range.end())
|
||||
} else {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
shift_step.unwrap_or(step)
|
||||
} else {
|
||||
step
|
||||
}
|
||||
.into();
|
||||
|
||||
let start = (*range.start()).into();
|
||||
let end = (*range.end()).into();
|
||||
|
||||
let percent = f64::from(cursor_position.x - bounds.x)
|
||||
/ f64::from(bounds.width);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
T::from_f64(value)
|
||||
};
|
||||
|
||||
new_value
|
||||
};
|
||||
|
||||
let increment = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
shift_step.unwrap_or(step)
|
||||
} else {
|
||||
step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps + 1.0);
|
||||
|
||||
if new_value > (*range.end()).into() {
|
||||
return Some(*range.end());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let decrement = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
shift_step.unwrap_or(step)
|
||||
} else {
|
||||
step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps - 1.0);
|
||||
|
||||
if new_value < (*range.start()).into() {
|
||||
return Some(*range.start());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let change = |new_value: T| {
|
||||
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((on_change)(new_value));
|
||||
|
||||
*value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(cursor_position) = cursor.position_over(layout.bounds())
|
||||
{
|
||||
if state.keyboard_modifiers.command() {
|
||||
let _ = default.map(change);
|
||||
state.is_dragging = false;
|
||||
} else {
|
||||
let _ = locate(cursor_position).map(change);
|
||||
state.is_dragging = true;
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if is_dragging {
|
||||
if let Some(on_release) = on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
let _ = cursor.position().and_then(locate).map(change);
|
||||
|
||||
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) => {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
Key::Named(key::Named::ArrowDown) => {
|
||||
let _ = decrement(current_value).map(change);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
state.keyboard_modifiers = modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`Slider`].
|
||||
pub fn draw<T, Theme, Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
state: &State,
|
||||
value: T,
|
||||
range: &RangeInclusive<T>,
|
||||
theme: &Theme,
|
||||
style: &Theme::Style,
|
||||
) where
|
||||
T: Into<f64> + Copy,
|
||||
Theme: StyleSheet,
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let style = if state.is_dragging {
|
||||
theme.dragging(style)
|
||||
} else if is_mouse_over {
|
||||
theme.hovered(style)
|
||||
} else {
|
||||
theme.active(style)
|
||||
};
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) =
|
||||
match style.handle.shape {
|
||||
HandleShape::Circle { radius } => {
|
||||
(radius * 2.0, radius * 2.0, radius.into())
|
||||
}
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.height, border_radius),
|
||||
};
|
||||
|
||||
let value = value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.width - handle_width) * (value - range_start)
|
||||
/ (range_end - range_start)
|
||||
};
|
||||
|
||||
let rail_y = bounds.y + bounds.height / 2.0;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: rail_y - style.rail.width / 2.0,
|
||||
width: offset + handle_width / 2.0,
|
||||
height: style.rail.width,
|
||||
},
|
||||
border: Border::with_radius(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + offset + handle_width / 2.0,
|
||||
y: rail_y - style.rail.width / 2.0,
|
||||
width: bounds.width - offset - handle_width / 2.0,
|
||||
height: style.rail.width,
|
||||
},
|
||||
border: Border::with_radius(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.1,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + offset,
|
||||
y: rail_y - handle_height / 2.0,
|
||||
width: handle_width,
|
||||
height: handle_height,
|
||||
},
|
||||
border: Border {
|
||||
radius: handle_border_radius,
|
||||
width: style.handle.border_width,
|
||||
color: style.handle.border_color,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the current [`mouse::Interaction`] of a [`Slider`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
state: &State,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Slider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
struct State {
|
||||
is_dragging: bool,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
/// The possible status of a [`Slider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Slider`] can be interacted with.
|
||||
Active,
|
||||
/// The [`Slider`] is being hovered.
|
||||
Hovered,
|
||||
/// The [`Slider`] is being dragged.
|
||||
Dragged,
|
||||
}
|
||||
|
||||
/// The appearance of a slider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The colors of the rail of the slider.
|
||||
pub rail: Rail,
|
||||
/// The appearance of the [`Handle`] of the slider.
|
||||
pub handle: Handle,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Changes the [`HandleShape`] of the [`Style`] to a circle
|
||||
/// with the given radius.
|
||||
pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
|
||||
self.handle.shape = HandleShape::Circle {
|
||||
radius: radius.into().0,
|
||||
};
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of a slider rail
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Rail {
|
||||
/// The colors of the rail of the slider.
|
||||
pub colors: (Color, Color),
|
||||
/// 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 appearance of the handle of a slider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Handle {
|
||||
/// The shape of the handle.
|
||||
pub shape: HandleShape,
|
||||
/// The [`Color`] of the handle.
|
||||
pub color: Color,
|
||||
/// The border width of the handle.
|
||||
pub border_width: f32,
|
||||
/// The border [`Color`] of the handle.
|
||||
pub border_color: Color,
|
||||
}
|
||||
|
||||
/// The shape of the handle of a slider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum HandleShape {
|
||||
/// A circular handle.
|
||||
Circle {
|
||||
/// The radius of the circle.
|
||||
radius: f32,
|
||||
},
|
||||
/// A rectangular shape.
|
||||
Rectangle {
|
||||
/// The width of the rectangle.
|
||||
width: u16,
|
||||
/// The border radius of the corners of the rectangle.
|
||||
border_radius: border::Radius,
|
||||
},
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Slider`].
|
||||
pub trait Catalog: Sized {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Slider`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of a [`Slider`].
|
||||
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
let color = match status {
|
||||
Status::Active => palette.primary.strong.color,
|
||||
Status::Hovered => palette.primary.base.color,
|
||||
Status::Dragged => palette.primary.strong.color,
|
||||
};
|
||||
|
||||
Style {
|
||||
rail: Rail {
|
||||
colors: (color, palette.secondary.base.color),
|
||||
width: 4.0,
|
||||
border_radius: 2.0.into(),
|
||||
},
|
||||
handle: Handle {
|
||||
shape: HandleShape::Circle { radius: 7.0 },
|
||||
color,
|
||||
border_color: Color::TRANSPARENT,
|
||||
border_width: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
333
widget/src/stack.rs
Normal file
333
widget/src/stack.rs
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
//! 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,
|
||||
};
|
||||
|
||||
/// A container that displays children on top of each other.
|
||||
///
|
||||
/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and
|
||||
/// will be displayed as the base layer. Every consecutive [`Element`] will be
|
||||
/// renderer on top; on its own layer.
|
||||
///
|
||||
/// Keep in mind that too much layering will normally produce bad UX as well as
|
||||
/// introduce certain rendering overhead. Use this widget sparingly!
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
{
|
||||
width: Length,
|
||||
height: Length,
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
/// Creates an empty [`Stack`].
|
||||
pub fn new() -> Self {
|
||||
Self::from_vec(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Stack`] with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self::from_vec(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Creates a [`Stack`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let iterator = children.into_iter();
|
||||
|
||||
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||
}
|
||||
|
||||
/// Creates a [`Stack`] from an already allocated [`Vec`].
|
||||
///
|
||||
/// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means
|
||||
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||
///
|
||||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||
/// call [`Stack::width`] or [`Stack::height`] accordingly.
|
||||
pub fn from_vec(
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Stack`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Stack`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Stack`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let child = child.into();
|
||||
|
||||
if self.children.is_empty() {
|
||||
let child_size = child.as_widget().size_hint();
|
||||
|
||||
self.width = self.width.enclose(child_size.width);
|
||||
self.height = self.height.enclose(child_size.height);
|
||||
}
|
||||
|
||||
self.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Stack`], if `Some`.
|
||||
pub fn push_maybe(
|
||||
self,
|
||||
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||
) -> Self {
|
||||
if let Some(child) = child {
|
||||
self.push(child)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the [`Stack`] with the given children.
|
||||
pub fn extend(
|
||||
self,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
children.into_iter().fold(self, Self::push)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Stack<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
tree.diff_children(&self.children);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
|
||||
if self.children.is_empty() {
|
||||
return layout::Node::new(limits.resolve(
|
||||
self.width,
|
||||
self.height,
|
||||
Size::ZERO,
|
||||
));
|
||||
}
|
||||
|
||||
let base = self.children[0].as_widget().layout(
|
||||
&mut tree.children[0],
|
||||
renderer,
|
||||
&limits,
|
||||
);
|
||||
|
||||
let size = limits.resolve(self.width, self.height, base.size());
|
||||
let limits = layout::Limits::new(Size::ZERO, size);
|
||||
|
||||
let nodes = std::iter::once(base)
|
||||
.chain(self.children[1..].iter().zip(&mut tree.children[1..]).map(
|
||||
|(layer, tree)| {
|
||||
let node =
|
||||
layer.as_widget().layout(tree, renderer, &limits);
|
||||
|
||||
node
|
||||
},
|
||||
))
|
||||
.collect();
|
||||
|
||||
layout::Node::with_children(size, nodes)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation<Message>,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), layout)| {
|
||||
child
|
||||
.as_widget()
|
||||
.operate(state, layout, renderer, operation);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
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)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.children
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(tree.children.iter().rev())
|
||||
.zip(layout.children().rev())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state, layout, cursor, viewport, renderer,
|
||||
)
|
||||
})
|
||||
.find(|&interaction| interaction != mouse::Interaction::None)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||
for (i, ((layer, state), layout)) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.enumerate()
|
||||
{
|
||||
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,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
&clipped_viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
overlay::from_children(
|
||||
&mut self.children,
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a,
|
||||
Renderer: crate::core::Renderer + 'a,
|
||||
{
|
||||
fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self {
|
||||
Self::new(stack)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,13 @@ use crate::core::renderer;
|
|||
use crate::core::svg;
|
||||
use crate::core::widget::Tree;
|
||||
use crate::core::{
|
||||
ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget,
|
||||
Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation,
|
||||
Size, Theme, Vector, Widget,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use crate::style::svg::{Appearance, StyleSheet};
|
||||
pub use svg::Handle;
|
||||
pub use crate::core::svg::Handle;
|
||||
|
||||
/// A vector graphics image.
|
||||
///
|
||||
|
|
@ -20,20 +20,22 @@ pub use svg::Handle;
|
|||
/// [`Svg`] images can have a considerable rendering cost when resized,
|
||||
/// specially when they are complex.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Svg<Theme = crate::Theme>
|
||||
pub struct Svg<'a, Theme = crate::Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
handle: Handle,
|
||||
width: Length,
|
||||
height: Length,
|
||||
content_fit: ContentFit,
|
||||
style: <Theme as StyleSheet>::Style,
|
||||
class: Theme::Class<'a>,
|
||||
rotation: Rotation,
|
||||
opacity: f32,
|
||||
}
|
||||
|
||||
impl<Theme> Svg<Theme>
|
||||
impl<'a, Theme> Svg<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// Creates a new [`Svg`] from the given [`Handle`].
|
||||
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||
|
|
@ -42,7 +44,9 @@ where
|
|||
width: Length::Fill,
|
||||
height: Length::Shrink,
|
||||
content_fit: ContentFit::Contain,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
rotation: Rotation::default(),
|
||||
opacity: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,18 +82,45 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Svg`].
|
||||
/// Sets the style of the [`Svg`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Svg`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies the given [`Rotation`] to the [`Svg`].
|
||||
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
|
||||
self.rotation = rotation.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the opacity of the [`Svg`].
|
||||
///
|
||||
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
|
||||
/// and `1.0` meaning completely opaque.
|
||||
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
|
||||
self.opacity = opacity.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<Theme>
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Svg<'a, Theme>
|
||||
where
|
||||
Theme: iced_style::svg::StyleSheet,
|
||||
Renderer: svg::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
|
|
@ -105,14 +136,17 @@ where
|
|||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
// The raw w/h of the underlying image
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let Size { width, height } = renderer.measure_svg(&self.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
|
||||
// The rotated size of the svg
|
||||
let rotated_size = self.rotation.apply(image_size);
|
||||
|
||||
// The size to be available to the widget prior to `Shrink`ing
|
||||
let raw_size = limits.resolve(self.width, self.height, image_size);
|
||||
let raw_size = limits.resolve(self.width, self.height, rotated_size);
|
||||
|
||||
// The uncropped size of the image when fit to the bounds above
|
||||
let full_size = self.content_fit.fit(image_size, raw_size);
|
||||
let full_size = self.content_fit.fit(rotated_size, raw_size);
|
||||
|
||||
// Shrink the widget to fit the resized image, if requested
|
||||
let final_size = Size {
|
||||
|
|
@ -139,35 +173,49 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let Size { width, height } = renderer.dimensions(&self.handle);
|
||||
let Size { width, height } = renderer.measure_svg(&self.handle);
|
||||
let image_size = Size::new(width as f32, height as f32);
|
||||
let rotated_size = self.rotation.apply(image_size);
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
|
||||
let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
|
||||
let scale = Vector::new(
|
||||
adjusted_fit.width / rotated_size.width,
|
||||
adjusted_fit.height / rotated_size.height,
|
||||
);
|
||||
|
||||
let final_size = image_size * scale;
|
||||
|
||||
let position = match self.content_fit {
|
||||
ContentFit::None => Point::new(
|
||||
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
|
||||
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
|
||||
),
|
||||
_ => Point::new(
|
||||
bounds.center_x() - final_size.width / 2.0,
|
||||
bounds.center_y() - final_size.height / 2.0,
|
||||
),
|
||||
};
|
||||
|
||||
let drawing_bounds = Rectangle::new(position, final_size);
|
||||
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let status = if is_mouse_over {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Idle
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
|
||||
let render = |renderer: &mut Renderer| {
|
||||
let offset = Vector::new(
|
||||
(bounds.width - adjusted_fit.width).max(0.0) / 2.0,
|
||||
(bounds.height - adjusted_fit.height).max(0.0) / 2.0,
|
||||
);
|
||||
|
||||
let drawing_bounds = Rectangle {
|
||||
width: adjusted_fit.width,
|
||||
height: adjusted_fit.height,
|
||||
..bounds
|
||||
};
|
||||
|
||||
let appearance = if is_mouse_over {
|
||||
theme.hovered(&self.style)
|
||||
} else {
|
||||
theme.appearance(&self.style)
|
||||
};
|
||||
|
||||
renderer.draw(
|
||||
renderer.draw_svg(
|
||||
self.handle.clone(),
|
||||
appearance.color,
|
||||
drawing_bounds + offset,
|
||||
style.color,
|
||||
drawing_bounds,
|
||||
self.rotation.radians(),
|
||||
self.opacity,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -181,13 +229,68 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Svg<Theme>>
|
||||
impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: iced_style::svg::StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: svg::Renderer + 'a,
|
||||
{
|
||||
fn from(icon: Svg<Theme>) -> Element<'a, Message, Theme, Renderer> {
|
||||
fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(icon)
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible status of an [`Svg`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Svg`] is idle.
|
||||
Idle,
|
||||
/// The [`Svg`] is being hovered.
|
||||
Hovered,
|
||||
}
|
||||
|
||||
/// The appearance of an [`Svg`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Style {
|
||||
/// The [`Color`] filter of an [`Svg`].
|
||||
///
|
||||
/// Useful for coloring a symbolic icon.
|
||||
///
|
||||
/// `None` keeps the original color.
|
||||
pub color: Option<Color>,
|
||||
}
|
||||
|
||||
/// The theme catalog of an [`Svg`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(|_theme, _status| Style::default())
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// A styling function for an [`Svg`].
|
||||
///
|
||||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
|
||||
fn from(style: Style) -> Self {
|
||||
Box::new(move |_theme, _status| style)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ use crate::core::text::highlighter::{self, Highlighter};
|
|||
use crate::core::text::{self, LineHeight};
|
||||
use crate::core::widget::{self, Widget};
|
||||
use crate::core::{
|
||||
Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector,
|
||||
Background, Border, Color, Element, Length, Padding, Pixels, Rectangle,
|
||||
Shell, Size, Theme, Vector,
|
||||
};
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
|
@ -19,7 +20,6 @@ use std::fmt;
|
|||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use crate::style::text_editor::{Appearance, StyleSheet};
|
||||
pub use text::editor::{Action, Edit, Motion};
|
||||
|
||||
/// A multi-line text input.
|
||||
|
|
@ -32,7 +32,7 @@ pub struct TextEditor<
|
|||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Highlighter: text::Highlighter,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
content: &'a Content<Renderer>,
|
||||
|
|
@ -42,7 +42,7 @@ pub struct TextEditor<
|
|||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
|
||||
highlighter_settings: Highlighter::Settings,
|
||||
highlighter_format: fn(
|
||||
|
|
@ -54,7 +54,7 @@ pub struct TextEditor<
|
|||
impl<'a, Message, Theme, Renderer>
|
||||
TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// Creates new [`TextEditor`] with the given [`Content`].
|
||||
|
|
@ -67,7 +67,7 @@ where
|
|||
width: Length::Fill,
|
||||
height: Length::Shrink,
|
||||
padding: Padding::new(5.0),
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
on_edit: None,
|
||||
highlighter_settings: (),
|
||||
highlighter_format: |_highlight, _theme| {
|
||||
|
|
@ -81,7 +81,7 @@ impl<'a, Highlighter, Message, Theme, Renderer>
|
|||
TextEditor<'a, Highlighter, Message, Theme, Renderer>
|
||||
where
|
||||
Highlighter: text::Highlighter,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// Sets the height of the [`TextEditor`].
|
||||
|
|
@ -110,6 +110,21 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`TextEditor`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`text::LineHeight`] of the [`TextEditor`].
|
||||
pub fn line_height(
|
||||
mut self,
|
||||
line_height: impl Into<text::LineHeight>,
|
||||
) -> Self {
|
||||
self.line_height = line_height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`TextEditor`].
|
||||
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
|
||||
self.padding = padding.into();
|
||||
|
|
@ -134,7 +149,7 @@ where
|
|||
width: self.width,
|
||||
height: self.height,
|
||||
padding: self.padding,
|
||||
style: self.style,
|
||||
class: self.class,
|
||||
on_edit: self.on_edit,
|
||||
highlighter_settings: settings,
|
||||
highlighter_format: to_format,
|
||||
|
|
@ -142,8 +157,20 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`TextEditor`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`TextEditor`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -292,7 +319,9 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
struct State<Highlighter: text::Highlighter> {
|
||||
/// The state of a [`TextEditor`].
|
||||
#[derive(Debug)]
|
||||
pub struct State<Highlighter: text::Highlighter> {
|
||||
is_focused: bool,
|
||||
last_click: Option<mouse::Click>,
|
||||
drag_click: Option<mouse::click::Kind>,
|
||||
|
|
@ -302,11 +331,18 @@ struct State<Highlighter: text::Highlighter> {
|
|||
highlighter_format_address: usize,
|
||||
}
|
||||
|
||||
impl<Highlighter: text::Highlighter> State<Highlighter> {
|
||||
/// Returns whether the [`TextEditor`] is currently focused or not.
|
||||
pub fn is_focused(&self) -> bool {
|
||||
self.is_focused
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for TextEditor<'a, Highlighter, Message, Theme, Renderer>
|
||||
where
|
||||
Highlighter: text::Highlighter,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
|
|
@ -477,7 +513,7 @@ where
|
|||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
defaults: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
|
|
@ -496,30 +532,32 @@ where
|
|||
let is_disabled = self.on_edit.is_none();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let appearance = if is_disabled {
|
||||
theme.disabled(&self.style)
|
||||
let status = if is_disabled {
|
||||
Status::Disabled
|
||||
} else if state.is_focused {
|
||||
theme.focused(&self.style)
|
||||
Status::Focused
|
||||
} else if is_mouse_over {
|
||||
theme.hovered(&self.style)
|
||||
Status::Hovered
|
||||
} else {
|
||||
theme.active(&self.style)
|
||||
Status::Active
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: appearance.border,
|
||||
border: style.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
appearance.background,
|
||||
style.background,
|
||||
);
|
||||
|
||||
renderer.fill_editor(
|
||||
&internal.editor,
|
||||
bounds.position()
|
||||
+ Vector::new(self.padding.left, self.padding.top),
|
||||
style.text_color,
|
||||
defaults.text_color,
|
||||
*viewport,
|
||||
);
|
||||
|
||||
|
|
@ -531,27 +569,31 @@ where
|
|||
if state.is_focused {
|
||||
match internal.editor.cursor() {
|
||||
Cursor::Caret(position) => {
|
||||
let position = position + translation;
|
||||
let cursor =
|
||||
Rectangle::new(
|
||||
position + translation,
|
||||
Size::new(
|
||||
1.0,
|
||||
self.line_height
|
||||
.to_absolute(self.text_size.unwrap_or_else(
|
||||
|| renderer.default_size(),
|
||||
))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
|
||||
if bounds.contains(position) {
|
||||
if let Some(clipped_cursor) = bounds.intersection(&cursor) {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: 1.0,
|
||||
height: self
|
||||
.line_height
|
||||
.to_absolute(
|
||||
self.text_size.unwrap_or_else(
|
||||
|| renderer.default_size(),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
x: clipped_cursor.x.floor(),
|
||||
y: clipped_cursor.y,
|
||||
width: clipped_cursor.width,
|
||||
height: clipped_cursor.height,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
theme.value_color(&self.style),
|
||||
style.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -564,7 +606,7 @@ where
|
|||
bounds: range,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
theme.selection_color(&self.style),
|
||||
style.selection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -600,7 +642,7 @@ impl<'a, Highlighter, Message, Theme, Renderer>
|
|||
where
|
||||
Highlighter: text::Highlighter,
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn from(
|
||||
|
|
@ -776,3 +818,101 @@ mod platform {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible status of a [`TextEditor`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`TextEditor`] can be interacted with.
|
||||
Active,
|
||||
/// The [`TextEditor`] is being hovered.
|
||||
Hovered,
|
||||
/// The [`TextEditor`] is focused.
|
||||
Focused,
|
||||
/// The [`TextEditor`] cannot be interacted with.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// The appearance of a text input.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The [`Background`] of the text input.
|
||||
pub background: Background,
|
||||
/// The [`Border`] of the text input.
|
||||
pub border: Border,
|
||||
/// The [`Color`] of the icon of the text input.
|
||||
pub icon: Color,
|
||||
/// The [`Color`] of the placeholder of the text input.
|
||||
pub placeholder: Color,
|
||||
/// The [`Color`] of the value of the text input.
|
||||
pub value: Color,
|
||||
/// The [`Color`] of the selection of the text input.
|
||||
pub selection: Color,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`TextEditor`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`TextEditor`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of a [`TextEditor`].
|
||||
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
let active = Style {
|
||||
background: Background::Color(palette.background.base.color),
|
||||
border: Border {
|
||||
radius: 2.0.into(),
|
||||
width: 1.0,
|
||||
color: palette.background.strong.color,
|
||||
},
|
||||
icon: palette.background.weak.text,
|
||||
placeholder: palette.background.strong.color,
|
||||
value: palette.background.base.text,
|
||||
selection: palette.primary.weak.color,
|
||||
};
|
||||
|
||||
match status {
|
||||
Status::Active => active,
|
||||
Status::Hovered => Style {
|
||||
border: Border {
|
||||
color: palette.background.base.text,
|
||||
..active.border
|
||||
},
|
||||
..active
|
||||
},
|
||||
Status::Focused => Style {
|
||||
border: Border {
|
||||
color: palette.primary.strong.color,
|
||||
..active.border
|
||||
},
|
||||
..active
|
||||
},
|
||||
Status::Disabled => Style {
|
||||
background: Background::Color(palette.background.weak.color),
|
||||
value: active.placeholder,
|
||||
..active
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,58 +7,68 @@ use crate::core::renderer;
|
|||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::widget::Operation;
|
||||
use crate::core::{
|
||||
Background, Clipboard, Element, Layout, Length, Point, Rectangle, Shell,
|
||||
Size, Vector, Widget,
|
||||
Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle,
|
||||
Shell, Size, Vector, Widget,
|
||||
};
|
||||
use crate::style::application;
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A widget that applies any `Theme` to its contents.
|
||||
///
|
||||
/// This widget can be useful to leverage multiple `Theme`
|
||||
/// types in an application.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Themer<'a, Message, Theme, Renderer>
|
||||
pub struct Themer<'a, Message, Theme, NewTheme, F, Renderer = crate::Renderer>
|
||||
where
|
||||
F: Fn(&Theme) -> NewTheme,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: application::StyleSheet,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
theme: Theme,
|
||||
style: Theme::Style,
|
||||
show_background: bool,
|
||||
content: Element<'a, Message, NewTheme, Renderer>,
|
||||
to_theme: F,
|
||||
text_color: Option<fn(&NewTheme) -> Color>,
|
||||
background: Option<fn(&NewTheme) -> Background>,
|
||||
old_theme: PhantomData<Theme>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, NewTheme, F, Renderer>
|
||||
Themer<'a, Message, Theme, NewTheme, F, Renderer>
|
||||
where
|
||||
F: Fn(&Theme) -> NewTheme,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: application::StyleSheet,
|
||||
{
|
||||
/// Creates an empty [`Themer`] that applies the given `Theme`
|
||||
/// to the provided `content`.
|
||||
pub fn new<T>(theme: Theme, content: T) -> Self
|
||||
pub fn new<T>(to_theme: F, content: T) -> Self
|
||||
where
|
||||
T: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
T: Into<Element<'a, Message, NewTheme, Renderer>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
theme,
|
||||
style: Theme::Style::default(),
|
||||
show_background: false,
|
||||
to_theme,
|
||||
text_color: None,
|
||||
background: None,
|
||||
old_theme: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets whether to draw the background color of the `Theme`.
|
||||
pub fn background(mut self, background: bool) -> Self {
|
||||
self.show_background = background;
|
||||
/// Sets the default text [`Color`] of the [`Themer`].
|
||||
pub fn text_color(mut self, f: fn(&NewTheme) -> Color) -> Self {
|
||||
self.text_color = Some(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Background`] of the [`Themer`].
|
||||
pub fn background(mut self, f: fn(&NewTheme) -> Background) -> Self {
|
||||
self.background = Some(f);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, AnyTheme, Message, Theme, Renderer> Widget<Message, AnyTheme, Renderer>
|
||||
for Themer<'a, Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Themer<'a, Message, Theme, NewTheme, F, Renderer>
|
||||
where
|
||||
F: Fn(&Theme) -> NewTheme,
|
||||
Renderer: crate::core::Renderer,
|
||||
Theme: application::StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
|
|
@ -134,38 +144,36 @@ where
|
|||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &AnyTheme,
|
||||
_style: &renderer::Style,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let appearance = self.theme.appearance(&self.style);
|
||||
let theme = (self.to_theme)(theme);
|
||||
|
||||
if self.show_background {
|
||||
if let Some(background) = self.background {
|
||||
container::draw_background(
|
||||
renderer,
|
||||
&container::Appearance {
|
||||
background: Some(Background::Color(
|
||||
appearance.background_color,
|
||||
)),
|
||||
..container::Appearance::default()
|
||||
&container::Style {
|
||||
background: Some(background(&theme)),
|
||||
..container::Style::default()
|
||||
},
|
||||
layout.bounds(),
|
||||
);
|
||||
}
|
||||
|
||||
self.content.as_widget().draw(
|
||||
tree,
|
||||
renderer,
|
||||
&self.theme,
|
||||
&renderer::Style {
|
||||
text_color: appearance.text_color,
|
||||
},
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
let style = if let Some(text_color) = self.text_color {
|
||||
renderer::Style {
|
||||
text_color: text_color(&theme),
|
||||
}
|
||||
} else {
|
||||
*style
|
||||
};
|
||||
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, &theme, &style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
|
|
@ -174,15 +182,15 @@ where
|
|||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> {
|
||||
struct Overlay<'a, Message, Theme, Renderer> {
|
||||
theme: &'a Theme,
|
||||
content: overlay::Element<'a, Message, Theme, Renderer>,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
struct Overlay<'a, Message, Theme, NewTheme, Renderer> {
|
||||
to_theme: &'a dyn Fn(&Theme) -> NewTheme,
|
||||
content: overlay::Element<'a, Message, NewTheme, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, AnyTheme, Message, Theme, Renderer>
|
||||
overlay::Overlay<Message, AnyTheme, Renderer>
|
||||
for Overlay<'a, Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, NewTheme, Renderer>
|
||||
overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, Message, Theme, NewTheme, Renderer>
|
||||
where
|
||||
Renderer: crate::core::Renderer,
|
||||
{
|
||||
|
|
@ -197,13 +205,18 @@ where
|
|||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &AnyTheme,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
self.content
|
||||
.draw(renderer, self.theme, style, layout, cursor);
|
||||
self.content.draw(
|
||||
renderer,
|
||||
&(self.to_theme)(theme),
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
|
|
@ -252,12 +265,12 @@ where
|
|||
&'b mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>>
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>>
|
||||
{
|
||||
self.content
|
||||
.overlay(layout, renderer)
|
||||
.map(|content| Overlay {
|
||||
theme: self.theme,
|
||||
to_theme: &self.to_theme,
|
||||
content,
|
||||
})
|
||||
.map(|overlay| overlay::Element::new(Box::new(overlay)))
|
||||
|
|
@ -268,24 +281,26 @@ where
|
|||
.as_widget_mut()
|
||||
.overlay(tree, layout, renderer, translation)
|
||||
.map(|content| Overlay {
|
||||
theme: &self.theme,
|
||||
to_theme: &self.to_theme,
|
||||
content,
|
||||
})
|
||||
.map(|overlay| overlay::Element::new(Box::new(overlay)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, AnyTheme, Message, Theme, Renderer>
|
||||
From<Themer<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, AnyTheme, Renderer>
|
||||
impl<'a, Message, Theme, NewTheme, F, Renderer>
|
||||
From<Themer<'a, Message, Theme, NewTheme, F, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: 'a + application::StyleSheet,
|
||||
Theme: 'a,
|
||||
NewTheme: 'a,
|
||||
F: Fn(&Theme) -> NewTheme + 'a,
|
||||
Renderer: 'a + crate::core::Renderer,
|
||||
{
|
||||
fn from(
|
||||
themer: Themer<'a, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, AnyTheme, Renderer> {
|
||||
themer: Themer<'a, Message, Theme, NewTheme, F, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(themer)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,19 +9,16 @@ use crate::core::touch;
|
|||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle,
|
||||
Shell, Size, Widget,
|
||||
Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
|
||||
Rectangle, Shell, Size, Theme, Widget,
|
||||
};
|
||||
|
||||
pub use crate::style::toggler::{Appearance, StyleSheet};
|
||||
|
||||
/// A toggler widget.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # type Toggler<'a, Message> =
|
||||
/// # iced_widget::Toggler<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
|
||||
/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>;
|
||||
/// #
|
||||
/// pub enum Message {
|
||||
/// TogglerToggled(bool),
|
||||
|
|
@ -38,7 +35,7 @@ pub struct Toggler<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
is_toggled: bool,
|
||||
|
|
@ -52,16 +49,16 @@ pub struct Toggler<
|
|||
text_shaping: text::Shaping,
|
||||
spacing: f32,
|
||||
font: Option<Renderer::Font>,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// The default size of a [`Toggler`].
|
||||
pub const DEFAULT_SIZE: f32 = 20.0;
|
||||
pub const DEFAULT_SIZE: f32 = 16.0;
|
||||
|
||||
/// Creates a new [`Toggler`].
|
||||
///
|
||||
|
|
@ -91,7 +88,7 @@ where
|
|||
text_shaping: text::Shaping::Basic,
|
||||
spacing: Self::DEFAULT_SIZE / 2.0,
|
||||
font: None,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,8 +146,20 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`Toggler`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Toggler`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +167,7 @@ where
|
|||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toggler<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: StyleSheet + crate::text::StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
|
|
@ -286,7 +295,7 @@ where
|
|||
style,
|
||||
label_layout,
|
||||
tree.state.downcast_ref(),
|
||||
crate::text::Appearance::default(),
|
||||
crate::text::Style::default(),
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
|
@ -294,12 +303,18 @@ where
|
|||
let bounds = toggler_layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(layout.bounds());
|
||||
|
||||
let style = if is_mouse_over {
|
||||
theme.hovered(&self.style, self.is_toggled)
|
||||
let status = if is_mouse_over {
|
||||
Status::Hovered {
|
||||
is_toggled: self.is_toggled,
|
||||
}
|
||||
} else {
|
||||
theme.active(&self.style, self.is_toggled)
|
||||
Status::Active {
|
||||
is_toggled: self.is_toggled,
|
||||
}
|
||||
};
|
||||
|
||||
let style = theme.style(&self.class, status);
|
||||
|
||||
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
|
||||
let space = SPACE_RATIO * bounds.height;
|
||||
|
||||
|
|
@ -354,7 +369,7 @@ impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: StyleSheet + crate::text::StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
|
|
@ -363,3 +378,108 @@ where
|
|||
Element::new(toggler)
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible status of a [`Toggler`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`Toggler`] can be interacted with.
|
||||
Active {
|
||||
/// Indicates whether the [`Toggler`] is toggled.
|
||||
is_toggled: bool,
|
||||
},
|
||||
/// The [`Toggler`] is being hovered.
|
||||
Hovered {
|
||||
/// Indicates whether the [`Toggler`] is toggled.
|
||||
is_toggled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// The appearance of a toggler.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The background [`Color`] of the toggler.
|
||||
pub background: Color,
|
||||
/// The width of the background border of the toggler.
|
||||
pub background_border_width: f32,
|
||||
/// The [`Color`] of the background border of the toggler.
|
||||
pub background_border_color: Color,
|
||||
/// The foreground [`Color`] of the toggler.
|
||||
pub foreground: Color,
|
||||
/// The width of the foreground border of the toggler.
|
||||
pub foreground_border_width: f32,
|
||||
/// The [`Color`] of the foreground border of the toggler.
|
||||
pub foreground_border_color: Color,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Toggler`].
|
||||
pub trait Catalog: Sized {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`Toggler`].
|
||||
///
|
||||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of a [`Toggler`].
|
||||
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
let background = match status {
|
||||
Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
|
||||
if is_toggled {
|
||||
palette.primary.strong.color
|
||||
} else {
|
||||
palette.background.strong.color
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let foreground = match status {
|
||||
Status::Active { is_toggled } => {
|
||||
if is_toggled {
|
||||
palette.primary.strong.text
|
||||
} else {
|
||||
palette.background.base.color
|
||||
}
|
||||
}
|
||||
Status::Hovered { is_toggled } => {
|
||||
if is_toggled {
|
||||
Color {
|
||||
a: 0.5,
|
||||
..palette.primary.strong.text
|
||||
}
|
||||
} else {
|
||||
palette.background.weak.color
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Style {
|
||||
background,
|
||||
foreground,
|
||||
foreground_border_width: 0.0,
|
||||
foreground_border_color: Color::TRANSPARENT,
|
||||
background_border_width: 0.0,
|
||||
background_border_color: Color::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub struct Tooltip<
|
|||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
|
|
@ -29,12 +29,12 @@ pub struct Tooltip<
|
|||
gap: f32,
|
||||
padding: f32,
|
||||
snap_within_viewport: bool,
|
||||
style: <Theme as container::StyleSheet>::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// The default padding of a [`Tooltip`] drawn by this renderer.
|
||||
|
|
@ -55,7 +55,7 @@ where
|
|||
gap: 0.0,
|
||||
padding: Self::DEFAULT_PADDING,
|
||||
snap_within_viewport: true,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +78,23 @@ where
|
|||
}
|
||||
|
||||
/// Sets the style of the [`Tooltip`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Into<<Theme as container::StyleSheet>::Style>,
|
||||
) -> Self {
|
||||
self.style = style.into();
|
||||
style: impl Fn(&Theme) -> container::Style + 'a,
|
||||
) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Tooltip`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +102,7 @@ where
|
|||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Tooltip<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet + crate::text::StyleSheet,
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
|
|
@ -239,7 +251,7 @@ where
|
|||
positioning: self.position,
|
||||
gap: self.gap,
|
||||
padding: self.padding,
|
||||
style: &self.style,
|
||||
class: &self.class,
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
|
|
@ -262,7 +274,7 @@ impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
|
|||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: container::StyleSheet + crate::text::StyleSheet + 'a,
|
||||
Theme: container::Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
|
|
@ -273,11 +285,10 @@ where
|
|||
}
|
||||
|
||||
/// The position of the tooltip. Defaults to following the cursor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Position {
|
||||
/// The tooltip will follow the cursor.
|
||||
FollowCursor,
|
||||
/// The tooltip will appear on the top of the widget.
|
||||
#[default]
|
||||
Top,
|
||||
/// The tooltip will appear on the bottom of the widget.
|
||||
Bottom,
|
||||
|
|
@ -285,6 +296,8 @@ pub enum Position {
|
|||
Left,
|
||||
/// The tooltip will appear on the right of the widget.
|
||||
Right,
|
||||
/// The tooltip will follow the cursor.
|
||||
FollowCursor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
|
|
@ -298,7 +311,7 @@ enum State {
|
|||
|
||||
struct Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet + widget::text::StyleSheet,
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
position: Point,
|
||||
|
|
@ -310,14 +323,14 @@ where
|
|||
positioning: Position,
|
||||
gap: f32,
|
||||
padding: f32,
|
||||
style: &'b <Theme as container::StyleSheet>::Style,
|
||||
class: &'b Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer>
|
||||
overlay::Overlay<Message, Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: container::StyleSheet + widget::text::StyleSheet,
|
||||
Theme: container::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
|
|
@ -426,7 +439,7 @@ where
|
|||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
) {
|
||||
let style = container::StyleSheet::appearance(theme, self.style);
|
||||
let style = theme.style(self.class);
|
||||
|
||||
container::draw_background(renderer, &style, layout.bounds());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
//! Display an interactive selector of a single value from a range of values.
|
||||
//!
|
||||
//! A [`VerticalSlider`] has some local [`State`].
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet};
|
||||
pub use crate::slider::{
|
||||
default, Catalog, Handle, HandleShape, Status, Style, StyleFn,
|
||||
};
|
||||
|
||||
use crate::core;
|
||||
use crate::core::event::{self, Event};
|
||||
use crate::core::keyboard;
|
||||
use crate::core::keyboard::key::{self, Key};
|
||||
|
|
@ -15,8 +14,8 @@ use crate::core::renderer;
|
|||
use crate::core::touch;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::{
|
||||
Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size,
|
||||
Widget,
|
||||
self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell,
|
||||
Size, Widget,
|
||||
};
|
||||
|
||||
/// An vertical bar and a handle that selects a single value from a range of
|
||||
|
|
@ -29,8 +28,7 @@ use crate::core::{
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # type VerticalSlider<'a, T, Message> =
|
||||
/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::style::Theme>;
|
||||
/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>;
|
||||
/// #
|
||||
/// #[derive(Clone)]
|
||||
/// pub enum Message {
|
||||
|
|
@ -44,7 +42,7 @@ use crate::core::{
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
range: RangeInclusive<T>,
|
||||
step: T,
|
||||
|
|
@ -55,17 +53,17 @@ where
|
|||
on_release: Option<Message>,
|
||||
width: f32,
|
||||
height: Length,
|
||||
style: Theme::Style,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
|
||||
where
|
||||
T: Copy + From<u8> + std::cmp::PartialOrd,
|
||||
Message: Clone,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// The default width of a [`VerticalSlider`].
|
||||
pub const DEFAULT_WIDTH: f32 = 22.0;
|
||||
pub const DEFAULT_WIDTH: f32 = 16.0;
|
||||
|
||||
/// Creates a new [`VerticalSlider`].
|
||||
///
|
||||
|
|
@ -101,7 +99,7 @@ where
|
|||
on_release: None,
|
||||
width: Self::DEFAULT_WIDTH,
|
||||
height: Length::Fill,
|
||||
style: Default::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,12 +134,6 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`VerticalSlider`].
|
||||
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the step size of the [`VerticalSlider`].
|
||||
pub fn step(mut self, step: T) -> Self {
|
||||
self.step = step;
|
||||
|
|
@ -155,6 +147,24 @@ where
|
|||
self.shift_step = Some(shift_step.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`VerticalSlider`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`VerticalSlider`].
|
||||
#[cfg(feature = "advanced")]
|
||||
#[must_use]
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
|
|
@ -162,7 +172,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
|||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
Theme: StyleSheet,
|
||||
Theme: Catalog,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
|
|
@ -170,7 +180,7 @@ where
|
|||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
|
|
@ -200,20 +210,146 @@ where
|
|||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
update(
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
shell,
|
||||
tree.state.downcast_mut::<State>(),
|
||||
&mut self.value,
|
||||
self.default,
|
||||
&self.range,
|
||||
self.step,
|
||||
self.shift_step,
|
||||
self.on_change.as_ref(),
|
||||
&self.on_release,
|
||||
)
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let is_dragging = state.is_dragging;
|
||||
let current_value = self.value;
|
||||
|
||||
let locate = |cursor_position: Point| -> Option<T> {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let new_value = if cursor_position.y >= bounds.y + bounds.height {
|
||||
Some(*self.range.start())
|
||||
} else if cursor_position.y <= bounds.y {
|
||||
Some(*self.range.end())
|
||||
} else {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let start = (*self.range.start()).into();
|
||||
let end = (*self.range.end()).into();
|
||||
|
||||
let percent = 1.0
|
||||
- f64::from(cursor_position.y - bounds.y)
|
||||
/ f64::from(bounds.height);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
T::from_f64(value)
|
||||
};
|
||||
|
||||
new_value
|
||||
};
|
||||
|
||||
let increment = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps + 1.0);
|
||||
|
||||
if new_value > (*self.range.end()).into() {
|
||||
return Some(*self.range.end());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let decrement = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
self.shift_step.unwrap_or(self.step)
|
||||
} else {
|
||||
self.step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps - 1.0);
|
||||
|
||||
if new_value < (*self.range.start()).into() {
|
||||
return Some(*self.range.start());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let change = |new_value: T| {
|
||||
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((self.on_change)(new_value));
|
||||
|
||||
self.value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
if state.keyboard_modifiers.control()
|
||||
|| state.keyboard_modifiers.command()
|
||||
{
|
||||
let _ = self.default.map(change);
|
||||
state.is_dragging = false;
|
||||
} else {
|
||||
let _ = locate(cursor_position).map(change);
|
||||
state.is_dragging = true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
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) => {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
Key::Named(key::Named::ArrowDown) => {
|
||||
let _ = decrement(current_value).map(change);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
state.keyboard_modifiers = modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -226,15 +362,92 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
draw(
|
||||
renderer,
|
||||
layout,
|
||||
cursor,
|
||||
tree.state.downcast_ref::<State>(),
|
||||
self.value,
|
||||
&self.range,
|
||||
theme,
|
||||
&self.style,
|
||||
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 (handle_width, handle_height, handle_border_radius) =
|
||||
match style.handle.shape {
|
||||
HandleShape::Circle { radius } => {
|
||||
(radius * 2.0, radius * 2.0, radius.into())
|
||||
}
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.width, border_radius),
|
||||
};
|
||||
|
||||
let value = self.value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = self.range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.height - handle_width) * (value - range_end)
|
||||
/ (range_start - range_end)
|
||||
};
|
||||
|
||||
let rail_x = bounds.x + bounds.width / 2.0;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - style.rail.width / 2.0,
|
||||
y: bounds.y,
|
||||
width: style.rail.width,
|
||||
height: offset + handle_width / 2.0,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.1,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - style.rail.width / 2.0,
|
||||
y: bounds.y + offset + handle_width / 2.0,
|
||||
width: style.rail.width,
|
||||
height: bounds.height - offset - handle_width / 2.0,
|
||||
},
|
||||
border: Border::rounded(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - handle_height / 2.0,
|
||||
y: bounds.y + offset,
|
||||
width: handle_height,
|
||||
height: handle_width,
|
||||
},
|
||||
border: Border {
|
||||
radius: handle_border_radius,
|
||||
width: style.handle.border_width,
|
||||
color: style.handle.border_color,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +459,17 @@ where
|
|||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>())
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +479,7 @@ impl<'a, T, Message, Theme, Renderer>
|
|||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
|
||||
Message: Clone + 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
|
|
@ -266,294 +489,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`]
|
||||
/// accordingly.
|
||||
pub fn update<Message, T>(
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
state: &mut State,
|
||||
value: &mut T,
|
||||
default: Option<T>,
|
||||
range: &RangeInclusive<T>,
|
||||
step: T,
|
||||
shift_step: Option<T>,
|
||||
on_change: &dyn Fn(T) -> Message,
|
||||
on_release: &Option<Message>,
|
||||
) -> event::Status
|
||||
where
|
||||
T: Copy + Into<f64> + num_traits::FromPrimitive,
|
||||
Message: Clone,
|
||||
{
|
||||
let is_dragging = state.is_dragging;
|
||||
let current_value = *value;
|
||||
|
||||
let locate = |cursor_position: Point| -> Option<T> {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let new_value = if cursor_position.y >= bounds.y + bounds.height {
|
||||
Some(*range.start())
|
||||
} else if cursor_position.y <= bounds.y {
|
||||
Some(*range.end())
|
||||
} else {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
shift_step.unwrap_or(step)
|
||||
} else {
|
||||
step
|
||||
}
|
||||
.into();
|
||||
|
||||
let start = (*range.start()).into();
|
||||
let end = (*range.end()).into();
|
||||
|
||||
let percent = 1.0
|
||||
- f64::from(cursor_position.y - bounds.y)
|
||||
/ f64::from(bounds.height);
|
||||
|
||||
let steps = (percent * (end - start) / step).round();
|
||||
let value = steps * step + start;
|
||||
|
||||
T::from_f64(value)
|
||||
};
|
||||
|
||||
new_value
|
||||
};
|
||||
|
||||
let increment = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
shift_step.unwrap_or(step)
|
||||
} else {
|
||||
step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps + 1.0);
|
||||
|
||||
if new_value > (*range.end()).into() {
|
||||
return Some(*range.end());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let decrement = |value: T| -> Option<T> {
|
||||
let step = if state.keyboard_modifiers.shift() {
|
||||
shift_step.unwrap_or(step)
|
||||
} else {
|
||||
step
|
||||
}
|
||||
.into();
|
||||
|
||||
let steps = (value.into() / step).round();
|
||||
let new_value = step * (steps - 1.0);
|
||||
|
||||
if new_value < (*range.start()).into() {
|
||||
return Some(*range.start());
|
||||
}
|
||||
|
||||
T::from_f64(new_value)
|
||||
};
|
||||
|
||||
let change = |new_value: T| {
|
||||
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
|
||||
shell.publish((on_change)(new_value));
|
||||
|
||||
*value = new_value;
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
if let Some(cursor_position) = cursor.position_over(layout.bounds())
|
||||
{
|
||||
if state.keyboard_modifiers.control()
|
||||
|| state.keyboard_modifiers.command()
|
||||
{
|
||||
let _ = default.map(change);
|
||||
state.is_dragging = false;
|
||||
} else {
|
||||
let _ = locate(cursor_position).map(change);
|
||||
state.is_dragging = true;
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. })
|
||||
| Event::Touch(touch::Event::FingerLost { .. }) => {
|
||||
if is_dragging {
|
||||
if let Some(on_release) = on_release.clone() {
|
||||
shell.publish(on_release);
|
||||
}
|
||||
state.is_dragging = false;
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
| Event::Touch(touch::Event::FingerMoved { .. }) => {
|
||||
if is_dragging {
|
||||
let _ = cursor.position().and_then(locate).map(change);
|
||||
|
||||
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) => {
|
||||
let _ = increment(current_value).map(change);
|
||||
}
|
||||
Key::Named(key::Named::ArrowDown) => {
|
||||
let _ = decrement(current_value).map(change);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
state.keyboard_modifiers = modifiers;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
/// Draws a [`VerticalSlider`].
|
||||
pub fn draw<T, Theme, Renderer>(
|
||||
renderer: &mut Renderer,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
state: &State,
|
||||
value: T,
|
||||
range: &RangeInclusive<T>,
|
||||
style_sheet: &Theme,
|
||||
style: &Theme::Style,
|
||||
) where
|
||||
T: Into<f64> + Copy,
|
||||
Theme: StyleSheet,
|
||||
Renderer: core::Renderer,
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
let style = if state.is_dragging {
|
||||
style_sheet.dragging(style)
|
||||
} else if is_mouse_over {
|
||||
style_sheet.hovered(style)
|
||||
} else {
|
||||
style_sheet.active(style)
|
||||
};
|
||||
|
||||
let (handle_width, handle_height, handle_border_radius) =
|
||||
match style.handle.shape {
|
||||
HandleShape::Circle { radius } => {
|
||||
(radius * 2.0, radius * 2.0, radius.into())
|
||||
}
|
||||
HandleShape::Rectangle {
|
||||
width,
|
||||
border_radius,
|
||||
} => (f32::from(width), bounds.width, border_radius),
|
||||
};
|
||||
|
||||
let value = value.into() as f32;
|
||||
let (range_start, range_end) = {
|
||||
let (start, end) = range.clone().into_inner();
|
||||
|
||||
(start.into() as f32, end.into() as f32)
|
||||
};
|
||||
|
||||
let offset = if range_start >= range_end {
|
||||
0.0
|
||||
} else {
|
||||
(bounds.height - handle_width) * (value - range_end)
|
||||
/ (range_start - range_end)
|
||||
};
|
||||
|
||||
let rail_x = bounds.x + bounds.width / 2.0;
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - style.rail.width / 2.0,
|
||||
y: bounds.y,
|
||||
width: style.rail.width,
|
||||
height: offset + handle_width / 2.0,
|
||||
},
|
||||
border: Border::with_radius(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.1,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - style.rail.width / 2.0,
|
||||
y: bounds.y + offset + handle_width / 2.0,
|
||||
width: style.rail.width,
|
||||
height: bounds.height - offset - handle_width / 2.0,
|
||||
},
|
||||
border: Border::with_radius(style.rail.border_radius),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.rail.colors.0,
|
||||
);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: rail_x - handle_height / 2.0,
|
||||
y: bounds.y + offset,
|
||||
width: handle_height,
|
||||
height: handle_width,
|
||||
},
|
||||
border: Border {
|
||||
radius: handle_border_radius,
|
||||
width: style.handle.border_width,
|
||||
color: style.handle.border_color,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.handle.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
|
||||
pub fn mouse_interaction(
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
state: &State,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
if state.is_dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if is_mouse_over {
|
||||
mouse::Interaction::Grab
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`VerticalSlider`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct State {
|
||||
struct State {
|
||||
is_dragging: bool,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue