summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
Diffstat (limited to 'widget')
-rw-r--r--widget/Cargo.toml14
-rw-r--r--widget/assets/iced-logo.svg2
-rw-r--r--widget/src/button.rs48
-rw-r--r--widget/src/canvas.rs4
-rw-r--r--widget/src/checkbox.rs16
-rw-r--r--widget/src/column.rs17
-rw-r--r--widget/src/combo_box.rs47
-rw-r--r--widget/src/container.rs162
-rw-r--r--widget/src/helpers.rs126
-rw-r--r--widget/src/image.rs13
-rw-r--r--widget/src/image/viewer.rs15
-rw-r--r--widget/src/keyed/column.rs2
-rw-r--r--widget/src/lazy.rs4
-rw-r--r--widget/src/lazy/component.rs13
-rw-r--r--widget/src/lazy/helpers.rs12
-rw-r--r--widget/src/lazy/responsive.rs3
-rw-r--r--widget/src/lib.rs6
-rw-r--r--widget/src/markdown.rs587
-rw-r--r--widget/src/mouse_area.rs51
-rw-r--r--widget/src/overlay/menu.rs35
-rw-r--r--widget/src/pane_grid.rs4
-rw-r--r--widget/src/pane_grid/content.rs2
-rw-r--r--widget/src/pane_grid/controls.rs59
-rw-r--r--widget/src/pane_grid/title_bar.rs319
-rw-r--r--widget/src/pick_list.rs14
-rw-r--r--widget/src/progress_bar.rs10
-rw-r--r--widget/src/radio.rs26
-rw-r--r--widget/src/row.rs217
-rw-r--r--widget/src/rule.rs4
-rw-r--r--widget/src/scrollable.rs728
-rw-r--r--widget/src/slider.rs42
-rw-r--r--widget/src/stack.rs93
-rw-r--r--widget/src/svg.rs10
-rw-r--r--widget/src/text.rs4
-rw-r--r--widget/src/text/rich.rs538
-rw-r--r--widget/src/text_editor.rs599
-rw-r--r--widget/src/text_input.rs225
-rw-r--r--widget/src/themer.rs4
-rw-r--r--widget/src/toggler.rs88
-rw-r--r--widget/src/vertical_slider.rs21
40 files changed, 3385 insertions, 799 deletions
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 3c9f6a54..98a81145 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -22,8 +22,10 @@ lazy = ["ouroboros"]
image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
-qr_code = ["canvas", "qrcode"]
+qr_code = ["canvas", "dep:qrcode"]
wgpu = ["iced_renderer/wgpu"]
+markdown = ["dep:pulldown-cmark", "dep:url"]
+highlighter = ["dep:iced_highlighter"]
advanced = []
[dependencies]
@@ -31,6 +33,7 @@ iced_renderer.workspace = true
iced_runtime.workspace = true
num-traits.workspace = true
+once_cell.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
@@ -40,3 +43,12 @@ ouroboros.optional = true
qrcode.workspace = true
qrcode.optional = true
+
+pulldown-cmark.workspace = true
+pulldown-cmark.optional = true
+
+iced_highlighter.workspace = true
+iced_highlighter.optional = true
+
+url.workspace = true
+url.optional = true
diff --git a/widget/assets/iced-logo.svg b/widget/assets/iced-logo.svg
new file mode 100644
index 00000000..459b7fbb
--- /dev/null
+++ b/widget/assets/iced-logo.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg>
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 5d446fea..eafa71b9 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -1,4 +1,5 @@
//! Allow your users to perform actions by pressing a button.
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -9,8 +10,8 @@ use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
- Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
+ Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle,
+ Shadow, Shell, Size, Theme, Vector, Widget,
};
/// A generic widget that produces a message when pressed.
@@ -52,7 +53,7 @@ where
Theme: Catalog,
{
content: Element<'a, Message, Theme, Renderer>,
- on_press: Option<Message>,
+ on_press: Option<OnPress<'a, Message>>,
width: Length,
height: Length,
padding: Padding,
@@ -60,6 +61,20 @@ where
class: Theme::Class<'a>,
}
+enum OnPress<'a, Message> {
+ Direct(Message),
+ Closure(Box<dyn Fn() -> Message + 'a>),
+}
+
+impl<'a, Message: Clone> OnPress<'a, Message> {
+ fn get(&self) -> Message {
+ match self {
+ OnPress::Direct(message) => message.clone(),
+ OnPress::Closure(f) => f(),
+ }
+ }
+}
+
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
@@ -105,7 +120,23 @@ where
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
pub fn on_press(mut self, on_press: Message) -> Self {
- self.on_press = Some(on_press);
+ self.on_press = Some(OnPress::Direct(on_press));
+ self
+ }
+
+ /// Sets the message that will be produced when the [`Button`] is pressed.
+ ///
+ /// This is analogous to [`Button::on_press`], but using a closure to produce
+ /// the message.
+ ///
+ /// This closure will only be called when the [`Button`] is actually pressed and,
+ /// therefore, this method is useful to reduce overhead if creating the resulting
+ /// message is slow.
+ pub fn on_press_with(
+ mut self,
+ on_press: impl Fn() -> Message + 'a,
+ ) -> Self {
+ self.on_press = Some(OnPress::Closure(Box::new(on_press)));
self
}
@@ -114,7 +145,7 @@ where
///
/// If `None`, the [`Button`] will be disabled.
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
- self.on_press = on_press;
+ self.on_press = on_press.map(OnPress::Direct);
self
}
@@ -205,7 +236,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
@@ -258,7 +289,8 @@ where
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
- if let Some(on_press) = self.on_press.clone() {
+ if let Some(on_press) = self.on_press.as_ref().map(OnPress::get)
+ {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
@@ -560,7 +592,7 @@ fn styled(pair: palette::Pair) -> Style {
Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
- border: Border::rounded(2),
+ border: border::rounded(2),
..Style::default()
}
}
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 73cef087..185fa082 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -8,8 +8,8 @@ pub use program::Program;
pub use crate::graphics::cache::Group;
pub use crate::graphics::geometry::{
- fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin,
- Path, Stroke, Style, Text,
+ fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash,
+ LineJoin, Path, Stroke, Style, Text,
};
use crate::core;
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index 225c316d..32db5090 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -50,6 +50,7 @@ pub struct Checkbox<
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
class: Theme::Class<'a>,
@@ -81,7 +82,8 @@ where
spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
font: None,
icon: Icon {
font: Renderer::ICON_FONT,
@@ -158,6 +160,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`].
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
///
/// [`Renderer::Font`]: crate::core::text::Renderer
@@ -240,6 +248,7 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
},
)
@@ -348,6 +357,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: *shaping,
+ wrapping: text::Wrapping::default(),
},
bounds.center(),
style.icon_color,
@@ -358,12 +368,14 @@ where
{
let label_layout = children.next().unwrap();
+ let state: &widget::text::State<Renderer::Paragraph> =
+ tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout,
- tree.state.downcast_ref(),
+ state.0.raw(),
crate::text::Style {
color: style.text_color,
},
diff --git a/widget/src/column.rs b/widget/src/column.rs
index 0b81c545..d3ea4cf7 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -1,4 +1,5 @@
//! Distribute content vertically.
+use crate::core::alignment::{self, Alignment};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -6,8 +7,8 @@ use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
- Shell, Size, Vector, Widget,
+ Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell,
+ Size, Vector, Widget,
};
/// A container that distributes its contents vertically.
@@ -19,7 +20,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
width: Length,
height: Length,
max_width: f32,
- align_items: Alignment,
+ align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
@@ -63,7 +64,7 @@ where
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
- align_items: Alignment::Start,
+ align: Alignment::Start,
clip: false,
children,
}
@@ -104,8 +105,8 @@ where
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
- pub fn align_items(mut self, align: Alignment) -> Self {
- self.align_items = align;
+ pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
+ self.align = Alignment::from(align.into());
self
}
@@ -210,7 +211,7 @@ where
self.height,
self.padding,
self.spacing,
- self.align_items,
+ self.align,
&self.children,
&mut tree.children,
)
@@ -221,7 +222,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 253850df..62785b2c 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -208,12 +208,14 @@ where
/// The local state of a [`ComboBox`].
#[derive(Debug, Clone)]
-pub struct State<T>(RefCell<Inner<T>>);
+pub struct State<T> {
+ options: Vec<T>,
+ inner: RefCell<Inner<T>>,
+}
#[derive(Debug, Clone)]
struct Inner<T> {
value: String,
- options: Vec<T>,
option_matchers: Vec<String>,
filtered_options: Filtered<T>,
}
@@ -247,39 +249,58 @@ where
.collect(),
);
- Self(RefCell::new(Inner {
- value,
+ Self {
options,
- option_matchers,
- filtered_options,
- }))
+ inner: RefCell::new(Inner {
+ value,
+ option_matchers,
+ filtered_options,
+ }),
+ }
+ }
+
+ /// Returns the options of the [`State`].
+ ///
+ /// These are the options provided when the [`State`]
+ /// was constructed with [`State::new`].
+ pub fn options(&self) -> &[T] {
+ &self.options
}
fn value(&self) -> String {
- let inner = self.0.borrow();
+ let inner = self.inner.borrow();
inner.value.clone()
}
fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
- let inner = self.0.borrow();
+ let inner = self.inner.borrow();
f(&inner)
}
fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
- let mut inner = self.0.borrow_mut();
+ let mut inner = self.inner.borrow_mut();
f(&mut inner);
}
fn sync_filtered_options(&self, options: &mut Filtered<T>) {
- let inner = self.0.borrow();
+ let inner = self.inner.borrow();
inner.filtered_options.sync(options);
}
}
+impl<T> Default for State<T>
+where
+ T: Display + Clone,
+{
+ fn default() -> Self {
+ Self::new(Vec::new())
+ }
+}
+
impl<T> Filtered<T>
where
T: Clone,
@@ -431,7 +452,7 @@ where
state.filtered_options.update(
search(
- &state.options,
+ &self.state.options,
&state.option_matchers,
&state.value,
)
@@ -580,7 +601,7 @@ where
if let Some(selection) = menu.new_selection.take() {
// Clear the value and reset the options and menu
state.value = String::new();
- state.filtered_options.update(state.options.clone());
+ state.filtered_options.update(self.state.options.clone());
menu.menu = menu::State::default();
// Notify the selection
diff --git a/widget/src/container.rs b/widget/src/container.rs
index e917471f..3b794099 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -1,5 +1,6 @@
//! Decorate content and apply alignment.
use crate::core::alignment::{self, Alignment};
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::gradient::{self, Gradient};
use crate::core::layout;
@@ -9,11 +10,11 @@ use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{
- self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ self, color, Background, Clipboard, Color, Element, Layout, Length,
Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector,
Widget,
};
-use crate::runtime::Task;
+use crate::runtime::task::{self, Task};
/// An element decorating some content.
///
@@ -92,46 +93,6 @@ where
self
}
- /// Sets the [`Container`] to fill the available space in the horizontal axis.
- ///
- /// This can be useful to quickly position content when chained with
- /// alignment functions—like [`center_x`].
- ///
- /// Calling this method is equivalent to calling [`width`] with a
- /// [`Length::Fill`].
- ///
- /// [`center_x`]: Self::center_x
- /// [`width`]: Self::width
- pub fn fill_x(self) -> Self {
- self.width(Length::Fill)
- }
-
- /// Sets the [`Container`] to fill the available space in the vetical axis.
- ///
- /// This can be useful to quickly position content when chained with
- /// alignment functions—like [`center_y`].
- ///
- /// Calling this method is equivalent to calling [`height`] with a
- /// [`Length::Fill`].
- ///
- /// [`center_y`]: Self::center_x
- /// [`height`]: Self::height
- pub fn fill_y(self) -> Self {
- self.height(Length::Fill)
- }
-
- /// Sets the [`Container`] to fill all the available space.
- ///
- /// Calling this method is equivalent to chaining [`fill_x`] and
- /// [`fill_y`].
- ///
- /// [`center`]: Self::center
- /// [`fill_x`]: Self::fill_x
- /// [`fill_y`]: Self::fill_y
- pub fn fill(self) -> Self {
- self.width(Length::Fill).height(Length::Fill)
- }
-
/// Sets the maximum width of the [`Container`].
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
self.max_width = max_width.into().0;
@@ -144,18 +105,6 @@ where
self
}
- /// Sets the content alignment for the horizontal axis of the [`Container`].
- pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self {
- self.horizontal_alignment = alignment;
- self
- }
-
- /// Sets the content alignment for the vertical axis of the [`Container`].
- pub fn align_y(mut self, alignment: alignment::Vertical) -> Self {
- self.vertical_alignment = alignment;
- self
- }
-
/// Sets the width of the [`Container`] and centers its contents horizontally.
pub fn center_x(self, width: impl Into<Length>) -> Self {
self.width(width).align_x(alignment::Horizontal::Center)
@@ -179,6 +128,44 @@ where
self.center_x(length).center_y(length)
}
+ /// Aligns the contents of the [`Container`] to the left.
+ pub fn align_left(self, width: impl Into<Length>) -> Self {
+ self.width(width).align_x(alignment::Horizontal::Left)
+ }
+
+ /// Aligns the contents of the [`Container`] to the right.
+ pub fn align_right(self, width: impl Into<Length>) -> Self {
+ self.width(width).align_x(alignment::Horizontal::Right)
+ }
+
+ /// Aligns the contents of the [`Container`] to the top.
+ pub fn align_top(self, height: impl Into<Length>) -> Self {
+ self.height(height).align_y(alignment::Vertical::Top)
+ }
+
+ /// Aligns the contents of the [`Container`] to the bottom.
+ pub fn align_bottom(self, height: impl Into<Length>) -> Self {
+ self.height(height).align_y(alignment::Vertical::Bottom)
+ }
+
+ /// Sets the content alignment for the horizontal axis of the [`Container`].
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.horizontal_alignment = alignment.into();
+ self
+ }
+
+ /// Sets the content alignment for the vertical axis of the [`Container`].
+ pub fn align_y(
+ mut self,
+ alignment: impl Into<alignment::Vertical>,
+ ) -> Self {
+ self.vertical_alignment = alignment.into();
+ self
+ }
+
/// Sets whether the contents of the [`Container`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
@@ -197,7 +184,6 @@ where
}
/// Sets the style class of the [`Container`].
- #[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
@@ -258,7 +244,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(
self.id.as_ref().map(|id| &id.0),
@@ -473,6 +459,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
_state: &mut dyn widget::operation::Scrollable,
_id: Option<&widget::Id>,
bounds: Rectangle,
+ _content_bounds: Rectangle,
translation: Vector,
) {
match self.scrollables.last() {
@@ -538,7 +525,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
}
}
- Task::widget(VisibleBounds {
+ task::widget(VisibleBounds {
target: id.into(),
depth: 0,
scrollables: Vec::new(),
@@ -560,46 +547,54 @@ pub struct Style {
}
impl Style {
- /// Updates the border of the [`Style`] with the given [`Color`] and `width`.
- pub fn with_border(
- self,
- color: impl Into<Color>,
- width: impl Into<Pixels>,
- ) -> Self {
+ /// Updates the text color of the [`Style`].
+ pub fn color(self, color: impl Into<Color>) -> Self {
Self {
- border: Border {
- color: color.into(),
- width: width.into().0,
- ..Border::default()
- },
+ text_color: Some(color.into()),
+ ..self
+ }
+ }
+
+ /// Updates the border of the [`Style`].
+ pub fn border(self, border: impl Into<Border>) -> Self {
+ Self {
+ border: border.into(),
..self
}
}
/// Updates the background of the [`Style`].
- pub fn with_background(self, background: impl Into<Background>) -> Self {
+ pub fn background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
+
+ /// Updates the shadow of the [`Style`].
+ pub fn shadow(self, shadow: impl Into<Shadow>) -> Self {
+ Self {
+ shadow: shadow.into(),
+ ..self
+ }
+ }
}
impl From<Color> for Style {
fn from(color: Color) -> Self {
- Self::default().with_background(color)
+ Self::default().background(color)
}
}
impl From<Gradient> for Style {
fn from(gradient: Gradient) -> Self {
- Self::default().with_background(gradient)
+ Self::default().background(gradient)
}
}
impl From<gradient::Linear> for Style {
fn from(gradient: gradient::Linear) -> Self {
- Self::default().with_background(gradient)
+ Self::default().background(gradient)
}
}
@@ -618,6 +613,12 @@ pub trait Catalog {
/// A styling function for a [`Container`].
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
+impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
+ fn from(style: Style) -> Self {
+ Box::new(move |_theme| style)
+ }
+}
+
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
@@ -635,13 +636,18 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style {
Style::default()
}
+/// A [`Container`] with the given [`Background`].
+pub fn background(background: impl Into<Background>) -> Style {
+ Style::default().background(background)
+}
+
/// A rounded [`Container`] with a background.
pub fn rounded_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
Style {
background: Some(palette.background.weak.color.into()),
- border: Border::rounded(2),
+ border: border::rounded(2),
..Style::default()
}
}
@@ -660,3 +666,13 @@ pub fn bordered_box(theme: &Theme) -> Style {
..Style::default()
}
}
+
+/// A [`Container`] with a dark background and white text.
+pub fn dark(_theme: &Theme) -> Style {
+ Style {
+ background: Some(color!(0x111111).into()),
+ text_color: Some(Color::WHITE),
+ border: border::rounded(2),
+ ..Style::default()
+ }
+}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 62343a55..51978823 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -4,7 +4,8 @@ use crate::checkbox::{self, Checkbox};
use crate::combo_box::{self, ComboBox};
use crate::container::{self, Container};
use crate::core;
-use crate::core::widget::operation;
+use crate::core::widget::operation::{self, Operation};
+use crate::core::window;
use crate::core::{Element, Length, Pixels, Widget};
use crate::keyed;
use crate::overlay;
@@ -12,7 +13,8 @@ use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar};
use crate::radio::{self, Radio};
use crate::rule::{self, Rule};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider};
use crate::text::{self, Text};
@@ -111,6 +113,19 @@ macro_rules! text {
};
}
+/// Creates some [`Rich`] text with the given spans.
+///
+/// [`Rich`]: text::Rich
+#[macro_export]
+macro_rules! rich_text {
+ () => (
+ $crate::Column::new()
+ );
+ ($($x:expr),+ $(,)?) => (
+ $crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+])
+ );
+}
+
/// Creates a new [`Container`] with the provided content.
///
/// [`Container`]: crate::Container
@@ -275,7 +290,7 @@ where
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn operation::Operation<()>,
+ operation: &mut dyn operation::Operation,
) {
self.content
.as_widget()
@@ -383,6 +398,7 @@ where
struct Hover<'a, Message, Theme, Renderer> {
base: Element<'a, Message, Theme, Renderer>,
top: Element<'a, Message, Theme, Renderer>,
+ is_top_focused: bool,
is_top_overlay_active: bool,
}
@@ -458,7 +474,9 @@ where
viewport,
);
- if cursor.is_over(layout.bounds()) || self.is_top_overlay_active
+ if cursor.is_over(layout.bounds())
+ || self.is_top_focused
+ || self.is_top_overlay_active
{
let (top_layout, top_tree) = children.next().unwrap();
@@ -477,7 +495,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn operation::Operation<()>,
+ operation: &mut dyn operation::Operation,
) {
let children = [&self.base, &self.top]
.into_iter()
@@ -501,6 +519,24 @@ where
) -> event::Status {
let mut children = layout.children().zip(&mut tree.children);
let (base_layout, base_tree) = children.next().unwrap();
+ let (top_layout, top_tree) = children.next().unwrap();
+
+ if matches!(event, Event::Window(window::Event::RedrawRequested(_)))
+ {
+ let mut count_focused = operation::focusable::count();
+
+ self.top.as_widget_mut().operate(
+ top_tree,
+ top_layout,
+ renderer,
+ &mut operation::black_box(&mut count_focused),
+ );
+
+ self.is_top_focused = match count_focused.finish() {
+ operation::Outcome::Some(count) => count.focused.is_some(),
+ _ => false,
+ };
+ }
let top_status = if matches!(
event,
@@ -509,9 +545,9 @@ where
| mouse::Event::ButtonReleased(_)
)
) || cursor.is_over(layout.bounds())
+ || self.is_top_focused
+ || self.is_top_overlay_active
{
- let (top_layout, top_tree) = children.next().unwrap();
-
self.top.as_widget_mut().on_event(
top_tree,
event.clone(),
@@ -597,6 +633,7 @@ where
Element::new(Hover {
base: base.into(),
top: top.into(),
+ is_top_focused: false,
is_top_overlay_active: false,
})
}
@@ -645,8 +682,6 @@ where
}
/// Creates a new [`Text`] widget with the provided content.
-///
-/// [`Text`]: core::widget::Text
pub fn text<'a, Theme, Renderer>(
text: impl text::IntoFragment<'a>,
) -> Text<'a, Theme, Renderer>
@@ -658,8 +693,6 @@ where
}
/// 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>
@@ -670,6 +703,34 @@ where
Text::new(value.to_string())
}
+/// Creates a new [`Rich`] text widget with the provided spans.
+///
+/// [`Rich`]: text::Rich
+pub fn rich_text<'a, Link, Theme, Renderer>(
+ spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a,
+) -> text::Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: text::Catalog + 'a,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ text::Rich::with_spans(spans)
+}
+
+/// Creates a new [`Span`] of text with the provided content.
+///
+/// [`Span`]: text::Span
+pub fn span<'a, Link, Font>(
+ text: impl text::IntoFragment<'a>,
+) -> text::Span<'a, Link, Font> {
+ text::Span::new(text)
+}
+
+#[cfg(feature = "markdown")]
+#[doc(inline)]
+pub use crate::markdown::view as markdown;
+
/// Creates a new [`Checkbox`].
///
/// [`Checkbox`]: crate::Checkbox
@@ -706,15 +767,13 @@ where
///
/// [`Toggler`]: crate::Toggler
pub fn toggler<'a, Message, Theme, Renderer>(
- label: impl Into<Option<String>>,
is_checked: bool,
- f: impl Fn(bool) -> Message + 'a,
) -> Toggler<'a, Message, Theme, Renderer>
where
Theme: toggler::Catalog + 'a,
Renderer: core::text::Renderer,
{
- Toggler::new(label, is_checked, f)
+ Toggler::new(is_checked)
}
/// Creates a new [`TextInput`].
@@ -889,6 +948,41 @@ where
crate::Svg::new(handle)
}
+/// Creates an [`Element`] that displays the iced logo with the given `text_size`.
+///
+/// Useful for showing some love to your favorite GUI library in your "About" screen,
+/// for instance.
+#[cfg(feature = "svg")]
+pub fn iced<'a, Message, Theme, Renderer>(
+ text_size: impl Into<Pixels>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Renderer: core::Renderer
+ + core::text::Renderer<Font = core::Font>
+ + core::svg::Renderer
+ + 'a,
+ Theme: text::Catalog + crate::svg::Catalog + 'a,
+{
+ use crate::core::{Alignment, Font};
+ use crate::svg;
+ use once_cell::sync::Lazy;
+
+ static LOGO: Lazy<svg::Handle> = Lazy::new(|| {
+ svg::Handle::from_memory(include_bytes!("../assets/iced-logo.svg"))
+ });
+
+ let text_size = text_size.into();
+
+ row![
+ svg(LOGO.clone()).width(text_size * 1.3),
+ text("iced").size(text_size).font(Font::MONOSPACE)
+ ]
+ .spacing(text_size.0 / 3.0)
+ .align_y(Alignment::Center)
+ .into()
+}
+
/// Creates a new [`Canvas`].
///
/// [`Canvas`]: crate::Canvas
@@ -930,12 +1024,12 @@ where
/// Focuses the previous focusable widget.
pub fn focus_previous<T>() -> Task<T> {
- Task::effect(Action::widget(operation::focusable::focus_previous()))
+ task::effect(Action::widget(operation::focusable::focus_previous()))
}
/// Focuses the next focusable widget.
pub fn focus_next<T>() -> Task<T> {
- Task::effect(Action::widget(operation::focusable::focus_next()))
+ task::effect(Action::widget(operation::focusable::focus_next()))
}
/// A container intercepting mouse events.
diff --git a/widget/src/image.rs b/widget/src/image.rs
index 80e17263..e04f2d6f 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -43,7 +43,7 @@ pub struct Image<Handle> {
impl<Handle> Image<Handle> {
/// Creates a new [`Image`] with the given path.
- pub fn new<T: Into<Handle>>(handle: T) -> Self {
+ pub fn new(handle: impl Into<Handle>) -> Self {
Image {
handle: handle.into(),
width: Length::Shrink,
@@ -181,11 +181,14 @@ pub fn draw<Renderer, Handle>(
let render = |renderer: &mut Renderer| {
renderer.draw_image(
- handle.clone(),
- filter_method,
+ image::Image {
+ handle: handle.clone(),
+ filter_method,
+ rotation: rotation.radians(),
+ opacity,
+ snap: true,
+ },
drawing_bounds,
- rotation.radians(),
- opacity,
);
};
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index b8b69b60..b1aad22c 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -6,8 +6,8 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians,
- Rectangle, Shell, Size, Vector, Widget,
+ Clipboard, ContentFit, Element, Image, Layout, Length, Pixels, Point,
+ Radians, Rectangle, Shell, Size, Vector, Widget,
};
/// A frame that displays an image with the ability to zoom in/out and pan.
@@ -349,11 +349,14 @@ where
let render = |renderer: &mut Renderer| {
renderer.with_translation(translation, |renderer| {
renderer.draw_image(
- self.handle.clone(),
- self.filter_method,
+ Image {
+ handle: self.handle.clone(),
+ filter_method: self.filter_method,
+ rotation: Radians(0.0),
+ opacity: 1.0,
+ snap: true,
+ },
drawing_bounds,
- Radians(0.0),
- 1.0,
);
});
};
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index 69991d1f..2c56c605 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -265,7 +265,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index 606da22d..221f9de3 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -4,6 +4,7 @@ pub(crate) mod helpers;
pub mod component;
pub mod responsive;
+#[allow(deprecated)]
pub use component::Component;
pub use responsive::Responsive;
@@ -29,6 +30,7 @@ use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
/// A widget that only rebuilds its contents when necessary.
+#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> {
dependency: Dependency,
@@ -182,7 +184,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.with_element(|element| {
element.as_widget().operate(
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index f079c0df..659bc476 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -1,4 +1,5 @@
//! Build and reuse custom widgets using The Elm Architecture.
+#![allow(deprecated)]
use crate::core::event;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -30,6 +31,12 @@ use std::rc::Rc;
///
/// Additionally, a [`Component`] is capable of producing a `Message` to notify
/// the parent application of any relevant interactions.
+#[cfg(feature = "lazy")]
+#[deprecated(
+ since = "0.13.0",
+ note = "components introduce encapsulated state and hamper the use of a single source of truth. \
+ Instead, leverage the Elm Architecture directly, or implement a custom widget"
+)]
pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
/// The internal state of this [`Component`].
type State: Default;
@@ -59,7 +66,7 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> {
fn operate(
&self,
_state: &mut Self::State,
- _operation: &mut dyn widget::Operation<()>,
+ _operation: &mut dyn widget::Operation,
) {
}
@@ -172,7 +179,7 @@ where
fn rebuild_element_with_operation(
&self,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let heads = self.state.borrow_mut().take().unwrap().into_heads();
@@ -358,7 +365,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
self.rebuild_element_with_operation(operation);
diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs
index 4d0776ca..52e690ff 100644
--- a/widget/src/lazy/helpers.rs
+++ b/widget/src/lazy/helpers.rs
@@ -1,9 +1,11 @@
use crate::core::{self, Element, Size};
-use crate::lazy::component::{self, Component};
-use crate::lazy::{Lazy, Responsive};
+use crate::lazy::component;
use std::hash::Hash;
+#[allow(deprecated)]
+pub use crate::lazy::{Component, Lazy, Responsive};
+
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
/// closure that can turn this data into a widget tree.
#[cfg(feature = "lazy")]
@@ -21,6 +23,12 @@ where
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
#[cfg(feature = "lazy")]
+#[deprecated(
+ since = "0.13.0",
+ note = "components introduce encapsulated state and hamper the use of a single source of truth. \
+ Instead, leverage the Elm Architecture directly, or implement a custom widget"
+)]
+#[allow(deprecated)]
pub fn component<'a, C, Message, Theme, Renderer>(
component: C,
) -> Element<'a, Message, Theme, Renderer>
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index 27f52617..dbf281f3 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -21,6 +21,7 @@ use std::ops::Deref;
///
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
+#[cfg(feature = "lazy")]
#[allow(missing_debug_implementations)]
pub struct Responsive<
'a,
@@ -161,7 +162,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 00e9aaa4..a68720d6 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -43,9 +43,6 @@ pub use helpers::*;
mod lazy;
#[cfg(feature = "lazy")]
-pub use crate::lazy::{Component, Lazy, Responsive};
-
-#[cfg(feature = "lazy")]
pub use crate::lazy::helpers::*;
#[doc(no_inline)]
@@ -130,5 +127,8 @@ pub mod qr_code;
#[doc(no_inline)]
pub use qr_code::QRCode;
+#[cfg(feature = "markdown")]
+pub mod markdown;
+
pub use crate::core::theme::{self, Theme};
pub use renderer::Renderer;
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs
new file mode 100644
index 00000000..fa4ee6bf
--- /dev/null
+++ b/widget/src/markdown.rs
@@ -0,0 +1,587 @@
+//! Parse and display Markdown.
+//!
+//! You can enable the `highlighter` feature for syntax highligting
+//! in code blocks.
+//!
+//! Only the variants of [`Item`] are currently supported.
+use crate::core::border;
+use crate::core::font::{self, Font};
+use crate::core::padding;
+use crate::core::theme;
+use crate::core::{
+ self, color, Color, Element, Length, Padding, Pixels, Theme,
+};
+use crate::{column, container, rich_text, row, scrollable, span, text};
+
+use std::cell::{Cell, RefCell};
+use std::rc::Rc;
+
+pub use core::text::Highlight;
+pub use pulldown_cmark::HeadingLevel;
+pub use url::Url;
+
+/// A Markdown item.
+#[derive(Debug, Clone)]
+pub enum Item {
+ /// A heading.
+ Heading(pulldown_cmark::HeadingLevel, Text),
+ /// A paragraph.
+ Paragraph(Text),
+ /// A code block.
+ ///
+ /// You can enable the `highlighter` feature for syntax highligting.
+ CodeBlock(Text),
+ /// A list.
+ List {
+ /// The first number of the list, if it is ordered.
+ start: Option<u64>,
+ /// The items of the list.
+ items: Vec<Vec<Item>>,
+ },
+}
+
+/// A bunch of parsed Markdown text.
+#[derive(Debug, Clone)]
+pub struct Text {
+ spans: Vec<Span>,
+ last_style: Cell<Option<Style>>,
+ last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>,
+}
+
+impl Text {
+ fn new(spans: Vec<Span>) -> Self {
+ Self {
+ spans,
+ last_style: Cell::default(),
+ last_styled_spans: RefCell::default(),
+ }
+ }
+
+ /// Returns the [`rich_text()`] spans ready to be used for the given style.
+ ///
+ /// This method performs caching for you. It will only reallocate if the [`Style`]
+ /// provided changes.
+ pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> {
+ if Some(style) != self.last_style.get() {
+ *self.last_styled_spans.borrow_mut() =
+ self.spans.iter().map(|span| span.view(&style)).collect();
+
+ self.last_style.set(Some(style));
+ }
+
+ self.last_styled_spans.borrow().clone()
+ }
+}
+
+#[derive(Debug, Clone)]
+enum Span {
+ Standard {
+ text: String,
+ strikethrough: bool,
+ link: Option<Url>,
+ strong: bool,
+ emphasis: bool,
+ code: bool,
+ },
+ #[cfg(feature = "highlighter")]
+ Highlight {
+ text: String,
+ color: Option<Color>,
+ font: Option<Font>,
+ },
+}
+
+impl Span {
+ fn view(&self, style: &Style) -> text::Span<'static, Url> {
+ match self {
+ Span::Standard {
+ text,
+ strikethrough,
+ link,
+ strong,
+ emphasis,
+ code,
+ } => {
+ let span = span(text.clone()).strikethrough(*strikethrough);
+
+ let span = if *code {
+ span.font(Font::MONOSPACE)
+ .color(style.inline_code_color)
+ .background(style.inline_code_highlight.background)
+ .border(style.inline_code_highlight.border)
+ .padding(style.inline_code_padding)
+ } else if *strong || *emphasis {
+ span.font(Font {
+ weight: if *strong {
+ font::Weight::Bold
+ } else {
+ font::Weight::Normal
+ },
+ style: if *emphasis {
+ font::Style::Italic
+ } else {
+ font::Style::Normal
+ },
+ ..Font::default()
+ })
+ } else {
+ span
+ };
+
+ let span = if let Some(link) = link.as_ref() {
+ span.color(style.link_color).link(link.clone())
+ } else {
+ span
+ };
+
+ span
+ }
+ #[cfg(feature = "highlighter")]
+ Span::Highlight { text, color, font } => {
+ span(text.clone()).color_maybe(*color).font_maybe(*font)
+ }
+ }
+ }
+}
+
+/// Parse the given Markdown content.
+pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
+ struct List {
+ start: Option<u64>,
+ items: Vec<Vec<Item>>,
+ }
+
+ let mut spans = Vec::new();
+ let mut strong = false;
+ let mut emphasis = false;
+ let mut strikethrough = false;
+ let mut metadata = false;
+ let mut table = false;
+ let mut link = None;
+ let mut lists = Vec::new();
+
+ #[cfg(feature = "highlighter")]
+ let mut highlighter = None;
+
+ let parser = pulldown_cmark::Parser::new_ext(
+ markdown,
+ pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
+ | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
+ | pulldown_cmark::Options::ENABLE_TABLES
+ | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
+ );
+
+ let produce = |lists: &mut Vec<List>, item| {
+ if lists.is_empty() {
+ Some(item)
+ } else {
+ lists
+ .last_mut()
+ .expect("list context")
+ .items
+ .last_mut()
+ .expect("item context")
+ .push(item);
+
+ None
+ }
+ };
+
+ // We want to keep the `spans` capacity
+ #[allow(clippy::drain_collect)]
+ parser.filter_map(move |event| match event {
+ pulldown_cmark::Event::Start(tag) => match tag {
+ pulldown_cmark::Tag::Strong if !metadata && !table => {
+ strong = true;
+ None
+ }
+ pulldown_cmark::Tag::Emphasis if !metadata && !table => {
+ emphasis = true;
+ None
+ }
+ pulldown_cmark::Tag::Strikethrough if !metadata && !table => {
+ strikethrough = true;
+ None
+ }
+ pulldown_cmark::Tag::Link { dest_url, .. }
+ if !metadata && !table =>
+ {
+ match Url::parse(&dest_url) {
+ Ok(url)
+ if url.scheme() == "http"
+ || url.scheme() == "https" =>
+ {
+ link = Some(url);
+ }
+ _ => {}
+ }
+
+ None
+ }
+ pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
+ lists.push(List {
+ start: first_item,
+ items: Vec::new(),
+ });
+
+ None
+ }
+ pulldown_cmark::Tag::Item => {
+ lists
+ .last_mut()
+ .expect("list context")
+ .items
+ .push(Vec::new());
+ None
+ }
+ pulldown_cmark::Tag::CodeBlock(
+ pulldown_cmark::CodeBlockKind::Fenced(_language),
+ ) if !metadata && !table => {
+ #[cfg(feature = "highlighter")]
+ {
+ use iced_highlighter::{self, Highlighter};
+ use text::Highlighter as _;
+
+ highlighter =
+ Some(Highlighter::new(&iced_highlighter::Settings {
+ theme: iced_highlighter::Theme::Base16Ocean,
+ token: _language.to_string(),
+ }));
+ }
+
+ None
+ }
+ pulldown_cmark::Tag::MetadataBlock(_) => {
+ metadata = true;
+ None
+ }
+ pulldown_cmark::Tag::Table(_) => {
+ table = true;
+ None
+ }
+ _ => None,
+ },
+ pulldown_cmark::Event::End(tag) => match tag {
+ pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
+ produce(
+ &mut lists,
+ Item::Heading(level, Text::new(spans.drain(..).collect())),
+ )
+ }
+ pulldown_cmark::TagEnd::Strong if !metadata && !table => {
+ strong = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Emphasis if !metadata && !table => {
+ emphasis = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => {
+ strikethrough = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Link if !metadata && !table => {
+ link = None;
+ None
+ }
+ pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
+ produce(
+ &mut lists,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ )
+ }
+ pulldown_cmark::TagEnd::Item if !metadata && !table => {
+ if spans.is_empty() {
+ None
+ } else {
+ produce(
+ &mut lists,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ )
+ }
+ }
+ pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
+ let list = lists.pop().expect("list context");
+
+ produce(
+ &mut lists,
+ Item::List {
+ start: list.start,
+ items: list.items,
+ },
+ )
+ }
+ pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
+ #[cfg(feature = "highlighter")]
+ {
+ highlighter = None;
+ }
+
+ produce(
+ &mut lists,
+ Item::CodeBlock(Text::new(spans.drain(..).collect())),
+ )
+ }
+ pulldown_cmark::TagEnd::MetadataBlock(_) => {
+ metadata = false;
+ None
+ }
+ pulldown_cmark::TagEnd::Table => {
+ table = false;
+ None
+ }
+ _ => None,
+ },
+ pulldown_cmark::Event::Text(text) if !metadata && !table => {
+ #[cfg(feature = "highlighter")]
+ if let Some(highlighter) = &mut highlighter {
+ use text::Highlighter as _;
+
+ for (range, highlight) in
+ highlighter.highlight_line(text.as_ref())
+ {
+ let span = Span::Highlight {
+ text: text[range].to_owned(),
+ color: highlight.color(),
+ font: highlight.font(),
+ };
+
+ spans.push(span);
+ }
+
+ return None;
+ }
+
+ let span = Span::Standard {
+ text: text.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: false,
+ };
+
+ spans.push(span);
+
+ None
+ }
+ pulldown_cmark::Event::Code(code) if !metadata && !table => {
+ let span = Span::Standard {
+ text: code.into_string(),
+ strong,
+ emphasis,
+ strikethrough,
+ link: link.clone(),
+ code: true,
+ };
+
+ spans.push(span);
+ None
+ }
+ pulldown_cmark::Event::SoftBreak if !metadata && !table => {
+ spans.push(Span::Standard {
+ text: String::from(" "),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
+ None
+ }
+ pulldown_cmark::Event::HardBreak if !metadata && !table => {
+ spans.push(Span::Standard {
+ text: String::from("\n"),
+ strikethrough,
+ strong,
+ emphasis,
+ link: link.clone(),
+ code: false,
+ });
+ None
+ }
+ _ => None,
+ })
+}
+
+/// Configuration controlling Markdown rendering in [`view`].
+#[derive(Debug, Clone, Copy)]
+pub struct Settings {
+ /// The base text size.
+ pub text_size: Pixels,
+ /// The text size of level 1 heading.
+ pub h1_size: Pixels,
+ /// The text size of level 2 heading.
+ pub h2_size: Pixels,
+ /// The text size of level 3 heading.
+ pub h3_size: Pixels,
+ /// The text size of level 4 heading.
+ pub h4_size: Pixels,
+ /// The text size of level 5 heading.
+ pub h5_size: Pixels,
+ /// The text size of level 6 heading.
+ pub h6_size: Pixels,
+ /// The text size used in code blocks.
+ pub code_size: Pixels,
+}
+
+impl Settings {
+ /// Creates new [`Settings`] with the given base text size in [`Pixels`].
+ ///
+ /// Heading levels will be adjusted automatically. Specifically,
+ /// the first level will be twice the base size, and then every level
+ /// after that will be 25% smaller.
+ pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
+ let text_size = text_size.into();
+
+ Self {
+ text_size,
+ h1_size: text_size * 2.0,
+ h2_size: text_size * 1.75,
+ h3_size: text_size * 1.5,
+ h4_size: text_size * 1.25,
+ h5_size: text_size,
+ h6_size: text_size,
+ code_size: text_size * 0.75,
+ }
+ }
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self::with_text_size(16)
+ }
+}
+
+/// The text styling of some Markdown rendering in [`view`].
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Style {
+ /// The [`Highlight`] to be applied to the background of inline code.
+ pub inline_code_highlight: Highlight,
+ /// The [`Padding`] to be applied to the background of inline code.
+ pub inline_code_padding: Padding,
+ /// The [`Color`] to be applied to inline code.
+ pub inline_code_color: Color,
+ /// The [`Color`] to be applied to links.
+ pub link_color: Color,
+}
+
+impl Style {
+ /// Creates a new [`Style`] from the given [`theme::Palette`].
+ pub fn from_palette(palette: theme::Palette) -> Self {
+ Self {
+ inline_code_padding: padding::left(1).right(1),
+ inline_code_highlight: Highlight {
+ background: color!(0x111).into(),
+ border: border::rounded(2),
+ },
+ inline_code_color: Color::WHITE,
+ link_color: palette.primary,
+ }
+ }
+}
+
+/// Display a bunch of Markdown items.
+///
+/// You can obtain the items with [`parse`].
+pub fn view<'a, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: Settings,
+ style: Style,
+) -> Element<'a, Url, Theme, Renderer>
+where
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ let Settings {
+ text_size,
+ h1_size,
+ h2_size,
+ h3_size,
+ h4_size,
+ h5_size,
+ h6_size,
+ code_size,
+ } = settings;
+
+ let spacing = text_size * 0.625;
+
+ let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
+ Item::Heading(level, heading) => {
+ container(rich_text(heading.spans(style)).size(match level {
+ pulldown_cmark::HeadingLevel::H1 => h1_size,
+ pulldown_cmark::HeadingLevel::H2 => h2_size,
+ pulldown_cmark::HeadingLevel::H3 => h3_size,
+ pulldown_cmark::HeadingLevel::H4 => h4_size,
+ pulldown_cmark::HeadingLevel::H5 => h5_size,
+ pulldown_cmark::HeadingLevel::H6 => h6_size,
+ }))
+ .padding(padding::top(if i > 0 {
+ text_size / 2.0
+ } else {
+ Pixels::ZERO
+ }))
+ .into()
+ }
+ Item::Paragraph(paragraph) => {
+ rich_text(paragraph.spans(style)).size(text_size).into()
+ }
+ Item::List { start: None, items } => {
+ column(items.iter().map(|items| {
+ row![text("•").size(text_size), view(items, settings, style)]
+ .spacing(spacing)
+ .into()
+ }))
+ .spacing(spacing)
+ .into()
+ }
+ Item::List {
+ start: Some(start),
+ items,
+ } => column(items.iter().enumerate().map(|(i, items)| {
+ row![
+ text!("{}.", i as u64 + *start).size(text_size),
+ view(items, settings, style)
+ ]
+ .spacing(spacing)
+ .into()
+ }))
+ .spacing(spacing)
+ .into(),
+ Item::CodeBlock(code) => container(
+ scrollable(
+ container(
+ rich_text(code.spans(style))
+ .font(Font::MONOSPACE)
+ .size(code_size),
+ )
+ .padding(spacing.0 / 2.0),
+ )
+ .direction(scrollable::Direction::Horizontal(
+ scrollable::Scrollbar::default()
+ .width(spacing.0 / 2.0)
+ .scroller_width(spacing.0 / 2.0),
+ )),
+ )
+ .width(Length::Fill)
+ .padding(spacing.0 / 2.0)
+ .class(Theme::code_block())
+ .into(),
+ });
+
+ Element::new(column(blocks).width(Length::Fill).spacing(text_size))
+}
+
+/// The theme catalog of Markdown items.
+pub trait Catalog:
+ container::Catalog + scrollable::Catalog + text::Catalog
+{
+ /// The styling class of a Markdown code block.
+ fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>;
+}
+
+impl Catalog for Theme {
+ fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> {
+ Box::new(container::dark)
+ }
+}
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 17cae53b..d255ac99 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -1,7 +1,4 @@
//! A container for capturing mouse events.
-
-use iced_renderer::core::Point;
-
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -10,7 +7,8 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{tree, Operation, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
+ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector,
+ Widget,
};
/// Emit messages on mouse events.
@@ -28,8 +26,9 @@ pub struct MouseArea<
on_right_release: Option<Message>,
on_middle_press: Option<Message>,
on_middle_release: Option<Message>,
+ on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
on_enter: Option<Message>,
- on_move: Option<Box<dyn Fn(Point) -> Message>>,
+ on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
on_exit: Option<Message>,
interaction: Option<mouse::Interaction>,
}
@@ -77,6 +76,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
self
}
+ /// The message to emit when scroll wheel is used
+ #[must_use]
+ pub fn on_scroll(
+ mut self,
+ on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a,
+ ) -> Self {
+ self.on_scroll = Some(Box::new(on_scroll));
+ self
+ }
+
/// The message to emit when the mouse enters the area.
#[must_use]
pub fn on_enter(mut self, message: Message) -> Self {
@@ -86,11 +95,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
/// The message to emit when the mouse moves in the area.
#[must_use]
- pub fn on_move<F>(mut self, build_message: F) -> Self
- where
- F: Fn(Point) -> Message + 'static,
- {
- self.on_move = Some(Box::new(build_message));
+ pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self {
+ self.on_move = Some(Box::new(on_move));
self
}
@@ -113,6 +119,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
#[derive(Default)]
struct State {
is_hovered: bool,
+ bounds: Rectangle,
+ cursor_position: Option<Point>,
}
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
@@ -128,6 +136,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
on_right_release: None,
on_middle_press: None,
on_middle_release: None,
+ on_scroll: None,
on_enter: None,
on_move: None,
on_exit: None,
@@ -178,7 +187,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
self.content.as_widget().operate(
&mut tree.children[0],
@@ -302,13 +311,17 @@ fn update<Message: Clone, Theme, Renderer>(
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- if let Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) = event
- {
- let state: &mut State = tree.state.downcast_mut();
+ let state: &mut State = tree.state.downcast_mut();
+ let cursor_position = cursor.position();
+ let bounds = layout.bounds();
+
+ if state.cursor_position != cursor_position && state.bounds != bounds {
let was_hovered = state.is_hovered;
+
state.is_hovered = cursor.is_over(layout.bounds());
+ state.cursor_position = cursor_position;
+ state.bounds = bounds;
match (
widget.on_enter.as_ref(),
@@ -397,5 +410,13 @@ fn update<Message: Clone, Theme, Renderer>(
}
}
+ if let Some(on_scroll) = widget.on_scroll.as_ref() {
+ if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
+ shell.publish(on_scroll(delta));
+
+ return event::Status::Captured;
+ }
+ }
+
event::Status::Ignored
}
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index 98efe305..f05ae40a 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -1,5 +1,6 @@
//! Build and show dropdown menus.
use crate::core::alignment;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -9,8 +10,8 @@ use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::Tree;
use crate::core::{
- Background, Border, Clipboard, Color, Length, Padding, Pixels, Point,
- Rectangle, Size, Theme, Vector,
+ Background, Clipboard, Color, Length, Padding, Pixels, Point, Rectangle,
+ Size, Theme, Vector,
};
use crate::core::{Element, Shell, Widget};
use crate::scrollable::{self, Scrollable};
@@ -200,21 +201,18 @@ where
class,
} = menu;
- let list = Scrollable::with_direction(
- List {
- options,
- hovered_option,
- on_selected,
- on_option_hovered,
- font,
- text_size,
- text_line_height,
- text_shaping,
- padding,
- class,
- },
- scrollable::Direction::default(),
- );
+ let list = Scrollable::new(List {
+ options,
+ hovered_option,
+ on_selected,
+ on_option_hovered,
+ font,
+ text_size,
+ text_line_height,
+ text_shaping,
+ padding,
+ class,
+ });
state.tree.diff(&list as &dyn Widget<_, _, _>);
@@ -517,7 +515,7 @@ where
width: bounds.width - style.border.width * 2.0,
..bounds
},
- border: Border::rounded(style.border.radius),
+ border: border::rounded(style.border.radius),
..renderer::Quad::default()
},
style.selected_background,
@@ -534,6 +532,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index c3da3879..710a5443 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -10,6 +10,7 @@
mod axis;
mod configuration;
mod content;
+mod controls;
mod direction;
mod draggable;
mod node;
@@ -22,6 +23,7 @@ pub mod state;
pub use axis::Axis;
pub use configuration::Configuration;
pub use content::Content;
+pub use controls::Controls;
pub use direction::Direction;
pub use draggable::Draggable;
pub use node::Node;
@@ -324,7 +326,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.contents
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index d45fc0cd..ec0676b1 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -214,7 +214,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let body_layout = if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs
new file mode 100644
index 00000000..13b57acb
--- /dev/null
+++ b/widget/src/pane_grid/controls.rs
@@ -0,0 +1,59 @@
+use crate::container;
+use crate::core::{self, Element};
+
+/// The controls of a [`Pane`].
+///
+/// [`Pane`]: super::Pane
+#[allow(missing_debug_implementations)]
+pub struct Controls<
+ 'a,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ pub(super) full: Element<'a, Message, Theme, Renderer>,
+ pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>,
+}
+
+impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ /// Creates a new [`Controls`] with the given content.
+ pub fn new(
+ content: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ full: content.into(),
+ compact: None,
+ }
+ }
+
+ /// Creates a new [`Controls`] with a full and compact variant.
+ /// If there is not enough room to show the full variant without overlap,
+ /// then the compact variant will be shown instead.
+ pub fn dynamic(
+ full: impl Into<Element<'a, Message, Theme, Renderer>>,
+ compact: impl Into<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
+ full: full.into(),
+ compact: Some(compact.into()),
+ }
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>>
+ for Controls<'a, Message, Theme, Renderer>
+where
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
+{
+ fn from(value: Element<'a, Message, Theme, Renderer>) -> Self {
+ Self::new(value)
+ }
+}
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index c05f1252..5002b4f7 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -9,6 +9,7 @@ use crate::core::{
self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
Vector,
};
+use crate::pane_grid::controls::Controls;
/// The title bar of a [`Pane`].
///
@@ -24,7 +25,7 @@ pub struct TitleBar<
Renderer: core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
- controls: Option<Element<'a, Message, Theme, Renderer>>,
+ controls: Option<Controls<'a, Message, Theme, Renderer>>,
padding: Padding,
always_show_controls: bool,
class: Theme::Class<'a>,
@@ -51,7 +52,7 @@ where
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
- controls: impl Into<Element<'a, Message, Theme, Renderer>>,
+ controls: impl Into<Controls<'a, Message, Theme, Renderer>>,
) -> Self {
self.controls = Some(controls.into());
self
@@ -104,10 +105,22 @@ where
Renderer: core::Renderer,
{
pub(super) fn state(&self) -> Tree {
- let children = if let Some(controls) = self.controls.as_ref() {
- vec![Tree::new(&self.content), Tree::new(controls)]
- } else {
- vec![Tree::new(&self.content), Tree::empty()]
+ let children = match self.controls.as_ref() {
+ Some(controls) => match controls.compact.as_ref() {
+ Some(compact) => vec![
+ Tree::new(&self.content),
+ Tree::new(&controls.full),
+ Tree::new(compact),
+ ],
+ None => vec![
+ Tree::new(&self.content),
+ Tree::new(&controls.full),
+ Tree::empty(),
+ ],
+ },
+ None => {
+ vec![Tree::new(&self.content), Tree::empty(), Tree::empty()]
+ }
};
Tree {
@@ -117,9 +130,13 @@ where
}
pub(super) fn diff(&self, tree: &mut Tree) {
- if tree.children.len() == 2 {
+ if tree.children.len() == 3 {
if let Some(controls) = self.controls.as_ref() {
- tree.children[1].diff(controls);
+ if let Some(compact) = controls.compact.as_ref() {
+ tree.children[2].diff(compact);
+ }
+
+ tree.children[1].diff(&controls.full);
}
tree.children[0].diff(&self.content);
@@ -164,18 +181,42 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
+
+ compact.as_widget().draw(
+ &tree.children[2],
+ renderer,
+ theme,
+ &inherited_style,
+ compact_layout,
+ cursor,
+ viewport,
+ );
+ } else {
+ show_title = false;
+
+ controls.full.as_widget().draw(
+ &tree.children[1],
+ renderer,
+ theme,
+ &inherited_style,
+ controls_layout,
+ cursor,
+ viewport,
+ );
+ }
+ } else {
+ controls.full.as_widget().draw(
+ &tree.children[1],
+ renderer,
+ theme,
+ &inherited_style,
+ controls_layout,
+ cursor,
+ viewport,
+ );
}
-
- controls.as_widget().draw(
- &tree.children[1],
- renderer,
- theme,
- &inherited_style,
- controls_layout,
- cursor,
- viewport,
- );
}
}
@@ -207,13 +248,20 @@ where
let mut children = padded.children();
let title_layout = children.next().unwrap();
- if self.controls.is_some() {
+ if let Some(controls) = self.controls.as_ref() {
let controls_layout = children.next().unwrap();
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- !controls_layout.bounds().contains(cursor_position)
+ if controls.compact.is_some() {
+ let compact_layout = children.next().unwrap();
+
+ !compact_layout.bounds().contains(cursor_position)
+ && !title_layout.bounds().contains(cursor_position)
+ } else {
+ !controls_layout.bounds().contains(cursor_position)
+ }
} else {
!controls_layout.bounds().contains(cursor_position)
&& !title_layout.bounds().contains(cursor_position)
@@ -244,25 +292,73 @@ where
let title_size = title_layout.size();
let node = if let Some(controls) = &self.controls {
- let controls_layout = controls.as_widget().layout(
+ let controls_layout = controls.full.as_widget().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
);
- let controls_size = controls_layout.size();
- let space_before_controls = max_size.width - controls_size.width;
-
- let height = title_size.height.max(controls_size.height);
-
- layout::Node::with_children(
- Size::new(max_size.width, height),
- vec![
- title_layout,
- controls_layout
- .move_to(Point::new(space_before_controls, 0.0)),
- ],
- )
+ if title_layout.bounds().width + controls_layout.bounds().width
+ > max_size.width
+ {
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = compact.as_widget().layout(
+ &mut tree.children[2],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
+
+ let compact_size = compact_layout.size();
+ let space_before_controls =
+ max_size.width - compact_size.width;
+
+ let height = title_size.height.max(compact_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout,
+ compact_layout.move_to(Point::new(
+ space_before_controls,
+ 0.0,
+ )),
+ ],
+ )
+ } else {
+ let controls_size = controls_layout.size();
+ let space_before_controls =
+ max_size.width - controls_size.width;
+
+ let height = title_size.height.max(controls_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout.move_to(Point::new(
+ space_before_controls,
+ 0.0,
+ )),
+ ],
+ )
+ }
+ } else {
+ let controls_size = controls_layout.size();
+ let space_before_controls =
+ max_size.width - controls_size.width;
+
+ let height = title_size.height.max(controls_size.height);
+
+ layout::Node::with_children(
+ Size::new(max_size.width, height),
+ vec![
+ title_layout,
+ controls_layout
+ .move_to(Point::new(space_before_controls, 0.0)),
+ ],
+ )
+ }
} else {
layout::Node::with_children(
Size::new(max_size.width, title_size.height),
@@ -278,7 +374,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn widget::Operation<()>,
+ operation: &mut dyn widget::Operation,
) {
let mut children = layout.children();
let padded = children.next().unwrap();
@@ -293,15 +389,33 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
- }
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
- controls.as_widget().operate(
- &mut tree.children[1],
- controls_layout,
- renderer,
- operation,
- );
+ compact.as_widget().operate(
+ &mut tree.children[2],
+ compact_layout,
+ renderer,
+ operation,
+ );
+ } else {
+ show_title = false;
+
+ controls.full.as_widget().operate(
+ &mut tree.children[1],
+ controls_layout,
+ renderer,
+ operation,
+ );
+ }
+ } else {
+ controls.full.as_widget().operate(
+ &mut tree.children[1],
+ controls_layout,
+ renderer,
+ operation,
+ );
+ }
};
if show_title {
@@ -337,19 +451,45 @@ where
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- show_title = false;
- }
+ if let Some(compact) = controls.compact.as_mut() {
+ let compact_layout = children.next().unwrap();
+
+ compact.as_widget_mut().on_event(
+ &mut tree.children[2],
+ event.clone(),
+ compact_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ } else {
+ show_title = false;
- controls.as_widget_mut().on_event(
- &mut tree.children[1],
- event.clone(),
- controls_layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
- )
+ controls.full.as_widget_mut().on_event(
+ &mut tree.children[1],
+ event.clone(),
+ controls_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
+ } else {
+ controls.full.as_widget_mut().on_event(
+ &mut tree.children[1],
+ event.clone(),
+ controls_layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ }
} else {
event::Status::Ignored
};
@@ -396,18 +536,33 @@ where
if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
- let controls_interaction = controls.as_widget().mouse_interaction(
- &tree.children[1],
- controls_layout,
- cursor,
- viewport,
- renderer,
- );
+ let controls_interaction =
+ controls.full.as_widget().mouse_interaction(
+ &tree.children[1],
+ controls_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
if title_layout.bounds().width + controls_layout.bounds().width
> padded.bounds().width
{
- controls_interaction
+ if let Some(compact) = controls.compact.as_ref() {
+ let compact_layout = children.next().unwrap();
+ let compact_interaction =
+ compact.as_widget().mouse_interaction(
+ &tree.children[2],
+ compact_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
+
+ compact_interaction.max(title_interaction)
+ } else {
+ controls_interaction
+ }
} else {
controls_interaction.max(title_interaction)
}
@@ -444,12 +599,36 @@ where
controls.as_mut().and_then(|controls| {
let controls_layout = children.next()?;
- controls.as_widget_mut().overlay(
- controls_state,
- controls_layout,
- renderer,
- translation,
- )
+ if title_layout.bounds().width
+ + controls_layout.bounds().width
+ > padded.bounds().width
+ {
+ if let Some(compact) = controls.compact.as_mut() {
+ let compact_state = states.next().unwrap();
+ let compact_layout = children.next()?;
+
+ compact.as_widget_mut().overlay(
+ compact_state,
+ compact_layout,
+ renderer,
+ translation,
+ )
+ } else {
+ controls.full.as_widget_mut().overlay(
+ controls_state,
+ controls_layout,
+ renderer,
+ translation,
+ )
+ }
+ } else {
+ controls.full.as_widget_mut().overlay(
+ controls_state,
+ controls_layout,
+ renderer,
+ translation,
+ )
+ }
})
})
}
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 97de5b48..1fc9951e 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -6,7 +6,8 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
-use crate::core::text::{self, Paragraph as _, Text};
+use crate::core::text::paragraph;
+use crate::core::text::{self, Text};
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
@@ -80,7 +81,7 @@ where
padding: crate::button::DEFAULT_PADDING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
font: None,
handle: Handle::default(),
class: <Theme as Catalog>::default(),
@@ -249,6 +250,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
@@ -514,6 +516,7 @@ where
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(
bounds.x + bounds.width - self.padding.right,
@@ -543,6 +546,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
+ wrapping: text::Wrapping::default(),
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
@@ -622,8 +626,8 @@ struct State<P: text::Paragraph> {
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
- options: Vec<P>,
- placeholder: P,
+ options: Vec<paragraph::Plain<P>>,
+ placeholder: paragraph::Plain<P>,
}
impl<P: text::Paragraph> State<P> {
@@ -635,7 +639,7 @@ impl<P: text::Paragraph> State<P> {
is_open: bool::default(),
hovered_option: Option::default(),
options: Vec::new(),
- placeholder: P::default(),
+ placeholder: paragraph::Plain::default(),
}
}
}
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index e7821b43..a10feea6 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -1,10 +1,11 @@
//! Provide progress feedback to your users.
+use crate::core::border::{self, Border};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
- self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme,
+ self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme,
Widget,
};
@@ -151,7 +152,10 @@ where
width: active_progress_width,
..bounds
},
- border: Border::rounded(style.border.radius),
+ border: Border {
+ color: Color::TRANSPARENT,
+ ..style.border
+ },
..renderer::Quad::default()
},
style.bar,
@@ -255,6 +259,6 @@ fn styled(
Style {
background: background.into(),
bar: bar.into(),
- border: Border::rounded(2),
+ border: border::rounded(2),
}
}
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 6b22961d..cfa961f3 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -1,5 +1,6 @@
//! Create choices using radio buttons.
use crate::core::alignment;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -9,8 +10,8 @@ use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
- Rectangle, Shell, Size, Theme, Widget,
+ Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle,
+ Shell, Size, Theme, Widget,
};
/// A circular button representing a choice.
@@ -81,6 +82,7 @@ where
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
}
@@ -104,7 +106,7 @@ where
/// * the label of the [`Radio`] button
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
- /// receives the value of the radio and must produce a `Message`.
+ /// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>(
label: impl Into<String>,
value: V,
@@ -121,10 +123,11 @@ where
label: label.into(),
width: Length::Shrink,
size: Self::DEFAULT_SIZE,
- spacing: Self::DEFAULT_SPACING, //15
+ spacing: Self::DEFAULT_SPACING,
text_size: None,
text_line_height: text::LineHeight::default(),
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
font: None,
class: Theme::default(),
}
@@ -169,6 +172,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button.
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the text font of the [`Radio`] button.
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
@@ -244,6 +253,7 @@ where
alignment::Horizontal::Left,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
},
)
@@ -342,7 +352,7 @@ where
width: bounds.width - dot_size,
height: bounds.height - dot_size,
},
- border: Border::rounded(dot_size / 2.0),
+ border: border::rounded(dot_size / 2.0),
..renderer::Quad::default()
},
style.dot_color,
@@ -352,12 +362,14 @@ where
{
let label_layout = children.next().unwrap();
+ let state: &widget::text::State<Renderer::Paragraph> =
+ tree.state.downcast_ref();
crate::text::draw(
renderer,
defaults,
label_layout,
- tree.state.downcast_ref(),
+ state.0.raw(),
crate::text::Style {
color: style.text_color,
},
diff --git a/widget/src/row.rs b/widget/src/row.rs
index c8fcdb61..85af912f 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -1,4 +1,5 @@
//! Distribute content horizontally.
+use crate::core::alignment::{self, Alignment};
use crate::core::event::{self, Event};
use crate::core::layout::{self, Layout};
use crate::core::mouse;
@@ -6,8 +7,8 @@ use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell,
- Size, Vector, Widget,
+ Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size,
+ Vector, Widget,
};
/// A container that distributes its contents horizontally.
@@ -17,7 +18,7 @@ pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
padding: Padding,
width: Length,
height: Length,
- align_items: Alignment,
+ align: Alignment,
clip: bool,
children: Vec<Element<'a, Message, Theme, Renderer>>,
}
@@ -60,7 +61,7 @@ where
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
- align_items: Alignment::Start,
+ align: Alignment::Start,
clip: false,
children,
}
@@ -95,8 +96,8 @@ where
}
/// Sets the vertical alignment of the contents of the [`Row`] .
- pub fn align_items(mut self, align: Alignment) -> Self {
- self.align_items = align;
+ pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
+ self.align = Alignment::from(align.into());
self
}
@@ -141,6 +142,13 @@ where
) -> Self {
children.into_iter().fold(self, Self::push)
}
+
+ /// Turns the [`Row`] into a [`Wrapping`] row.
+ ///
+ /// The original alignment of the [`Row`] is preserved per row wrapped.
+ pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
+ Wrapping { row: self }
+ }
}
impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer>
@@ -199,7 +207,7 @@ where
self.height,
self.padding,
self.spacing,
- self.align_items,
+ self.align,
&self.children,
&mut tree.children,
)
@@ -210,7 +218,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@@ -338,3 +346,196 @@ where
Self::new(row)
}
}
+
+/// A [`Row`] that wraps its contents.
+///
+/// Create a [`Row`] first, and then call [`Row::wrap`] to
+/// obtain a [`Row`] that wraps its contents.
+///
+/// The original alignment of the [`Row`] is preserved per row wrapped.
+#[allow(missing_debug_implementations)]
+pub struct Wrapping<
+ 'a,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> {
+ row: Row<'a, Message, Theme, Renderer>,
+}
+
+impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Wrapping<'a, Message, Theme, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
+ fn children(&self) -> Vec<Tree> {
+ self.row.children()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ self.row.diff(tree);
+ }
+
+ fn size(&self) -> Size<Length> {
+ self.row.size()
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits
+ .width(self.row.width)
+ .height(self.row.height)
+ .shrink(self.row.padding);
+
+ let spacing = self.row.spacing;
+ let max_width = limits.max().width;
+
+ let mut children: Vec<layout::Node> = Vec::new();
+ let mut intrinsic_size = Size::ZERO;
+ let mut row_start = 0;
+ let mut row_height = 0.0;
+ let mut x = 0.0;
+ let mut y = 0.0;
+
+ let align_factor = match self.row.align {
+ Alignment::Start => 0.0,
+ Alignment::Center => 2.0,
+ Alignment::End => 1.0,
+ };
+
+ let align = |row_start: std::ops::Range<usize>,
+ row_height: f32,
+ children: &mut Vec<layout::Node>| {
+ if align_factor != 0.0 {
+ for node in &mut children[row_start] {
+ let height = node.size().height;
+
+ node.translate_mut(Vector::new(
+ 0.0,
+ (row_height - height) / align_factor,
+ ));
+ }
+ }
+ };
+
+ for (i, child) in self.row.children.iter().enumerate() {
+ let node = child.as_widget().layout(
+ &mut tree.children[i],
+ renderer,
+ &limits,
+ );
+
+ let child_size = node.size();
+
+ if x != 0.0 && x + child_size.width > max_width {
+ intrinsic_size.width = intrinsic_size.width.max(x - spacing);
+
+ align(row_start..i, row_height, &mut children);
+
+ y += row_height + spacing;
+ x = 0.0;
+ row_start = i;
+ row_height = 0.0;
+ }
+
+ row_height = row_height.max(child_size.height);
+
+ children.push(node.move_to((
+ x + self.row.padding.left,
+ y + self.row.padding.top,
+ )));
+
+ x += child_size.width + spacing;
+ }
+
+ if x != 0.0 {
+ intrinsic_size.width = intrinsic_size.width.max(x - spacing);
+ }
+
+ intrinsic_size.height = (y - spacing).max(0.0) + row_height;
+ align(row_start..children.len(), row_height, &mut children);
+
+ let size =
+ limits.resolve(self.row.width, self.row.height, intrinsic_size);
+
+ layout::Node::with_children(size.expand(self.row.padding), children)
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn Operation,
+ ) {
+ self.row.operate(tree, layout, renderer, operation);
+ }
+
+ fn 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.row.on_event(
+ tree, event, layout, cursor, renderer, clipboard, shell, viewport,
+ )
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.row
+ .mouse_interaction(tree, layout, cursor, viewport, renderer)
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ self.row
+ .draw(tree, renderer, theme, style, layout, cursor, viewport);
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ translation: Vector,
+ ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
+ self.row.overlay(tree, layout, renderer, translation)
+ }
+}
+
+impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
+ for Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: 'a,
+ Renderer: crate::core::Renderer + 'a,
+{
+ fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self {
+ Self::new(row)
+ }
+}
diff --git a/widget/src/rule.rs b/widget/src/rule.rs
index 1a536d2f..bbcd577e 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -1,6 +1,6 @@
//! Display a horizontal or vertical rule for dividing content.
use crate::core;
-use crate::core::border::{self, Border};
+use crate::core::border;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
@@ -132,7 +132,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border: Border::rounded(style.radius),
+ border: border::rounded(style.radius),
..renderer::Quad::default()
},
style.color,
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index c3d08223..af6a3945 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -1,21 +1,24 @@
//! Navigate an endless amount of content with a scrollbar.
-// use crate::container;
use crate::container;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
+use crate::core::window;
use crate::core::{
- self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ self, Background, Clipboard, Color, Element, Layout, Length, Padding,
Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
@@ -52,34 +55,51 @@ where
Self::with_direction(content, Direction::default())
}
- /// Creates a new [`Scrollable`] with the given [`Direction`].
+ /// Creates a new vertical [`Scrollable`].
pub fn with_direction(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- direction: Direction,
+ direction: impl Into<Direction>,
) -> Self {
- let content = content.into();
+ Scrollable {
+ id: None,
+ width: Length::Shrink,
+ height: Length::Shrink,
+ direction: direction.into(),
+ content: content.into(),
+ on_scroll: None,
+ class: Theme::default(),
+ }
+ .validate()
+ }
+
+ fn validate(mut self) -> Self {
+ let size_hint = self.content.as_widget().size_hint();
debug_assert!(
- direction.vertical().is_none()
- || !content.as_widget().size_hint().height.is_fill(),
+ self.direction.vertical().is_none() || !size_hint.height.is_fill(),
"scrollable content must not fill its vertical scrolling axis"
);
debug_assert!(
- direction.horizontal().is_none()
- || !content.as_widget().size_hint().width.is_fill(),
+ self.direction.horizontal().is_none() || !size_hint.width.is_fill(),
"scrollable content must not fill its horizontal scrolling axis"
);
- Scrollable {
- id: None,
- width: Length::Shrink,
- height: Length::Shrink,
- direction,
- content,
- on_scroll: None,
- class: Theme::default(),
+ if self.direction.horizontal().is_none() {
+ self.width = self.width.enclose(size_hint.width);
}
+
+ if self.direction.vertical().is_none() {
+ self.height = self.height.enclose(size_hint.height);
+ }
+
+ self
+ }
+
+ /// Creates a new [`Scrollable`] with the given [`Direction`].
+ pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
+ self.direction = direction.into();
+ self.validate()
}
/// Sets the [`Id`] of the [`Scrollable`].
@@ -108,6 +128,69 @@ where
self
}
+ /// Anchors the vertical [`Scrollable`] direction to the top.
+ pub fn anchor_top(self) -> Self {
+ self.anchor_y(Anchor::Start)
+ }
+
+ /// Anchors the vertical [`Scrollable`] direction to the bottom.
+ pub fn anchor_bottom(self) -> Self {
+ self.anchor_y(Anchor::End)
+ }
+
+ /// Anchors the horizontal [`Scrollable`] direction to the left.
+ pub fn anchor_left(self) -> Self {
+ self.anchor_x(Anchor::Start)
+ }
+
+ /// Anchors the horizontal [`Scrollable`] direction to the right.
+ pub fn anchor_right(self) -> Self {
+ self.anchor_x(Anchor::End)
+ }
+
+ /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable.
+ pub fn anchor_x(mut self, alignment: Anchor) -> Self {
+ match &mut self.direction {
+ Direction::Horizontal(horizontal)
+ | Direction::Both { horizontal, .. } => {
+ horizontal.alignment = alignment;
+ }
+ Direction::Vertical { .. } => {}
+ }
+
+ self
+ }
+
+ /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable.
+ pub fn anchor_y(mut self, alignment: Anchor) -> Self {
+ match &mut self.direction {
+ Direction::Vertical(vertical)
+ | Direction::Both { vertical, .. } => {
+ vertical.alignment = alignment;
+ }
+ Direction::Horizontal { .. } => {}
+ }
+
+ self
+ }
+
+ /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the
+ /// content.
+ ///
+ /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents
+ /// of the [`Scrollable`].
+ pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self {
+ match &mut self.direction {
+ Direction::Horizontal(scrollbar)
+ | Direction::Vertical(scrollbar) => {
+ scrollbar.spacing = Some(new_spacing.into().0);
+ }
+ Direction::Both { .. } => {}
+ }
+
+ self
+ }
+
/// Sets the style of this [`Scrollable`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -131,102 +214,133 @@ where
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
/// Vertical scrolling
- Vertical(Properties),
+ Vertical(Scrollbar),
/// Horizontal scrolling
- Horizontal(Properties),
+ Horizontal(Scrollbar),
/// Both vertical and horizontal scrolling
Both {
/// The properties of the vertical scrollbar.
- vertical: Properties,
+ vertical: Scrollbar,
/// The properties of the horizontal scrollbar.
- horizontal: Properties,
+ horizontal: Scrollbar,
},
}
impl Direction {
- /// Returns the [`Properties`] of the horizontal scrollbar, if any.
- pub fn horizontal(&self) -> Option<&Properties> {
+ /// Returns the horizontal [`Scrollbar`], if any.
+ pub fn horizontal(&self) -> Option<&Scrollbar> {
match self {
- Self::Horizontal(properties) => Some(properties),
+ Self::Horizontal(scrollbar) => Some(scrollbar),
Self::Both { horizontal, .. } => Some(horizontal),
Self::Vertical(_) => None,
}
}
- /// Returns the [`Properties`] of the vertical scrollbar, if any.
- pub fn vertical(&self) -> Option<&Properties> {
+ /// Returns the vertical [`Scrollbar`], if any.
+ pub fn vertical(&self) -> Option<&Scrollbar> {
match self {
- Self::Vertical(properties) => Some(properties),
+ Self::Vertical(scrollbar) => Some(scrollbar),
Self::Both { vertical, .. } => Some(vertical),
Self::Horizontal(_) => None,
}
}
+
+ fn align(&self, delta: Vector) -> Vector {
+ let horizontal_alignment =
+ self.horizontal().map(|p| p.alignment).unwrap_or_default();
+
+ let vertical_alignment =
+ self.vertical().map(|p| p.alignment).unwrap_or_default();
+
+ let align = |alignment: Anchor, delta: f32| match alignment {
+ Anchor::Start => delta,
+ Anchor::End => -delta,
+ };
+
+ Vector::new(
+ align(horizontal_alignment, delta.x),
+ align(vertical_alignment, delta.y),
+ )
+ }
}
impl Default for Direction {
fn default() -> Self {
- Self::Vertical(Properties::default())
+ Self::Vertical(Scrollbar::default())
}
}
-/// Properties of a scrollbar within a [`Scrollable`].
+/// A scrollbar within a [`Scrollable`].
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Properties {
+pub struct Scrollbar {
width: f32,
margin: f32,
scroller_width: f32,
- alignment: Alignment,
+ alignment: Anchor,
+ spacing: Option<f32>,
}
-impl Default for Properties {
+impl Default for Scrollbar {
fn default() -> Self {
Self {
width: 10.0,
margin: 0.0,
scroller_width: 10.0,
- alignment: Alignment::Start,
+ alignment: Anchor::Start,
+ spacing: None,
}
}
}
-impl Properties {
- /// Creates new [`Properties`] for use in a [`Scrollable`].
+impl Scrollbar {
+ /// Creates new [`Scrollbar`] for use in a [`Scrollable`].
pub fn new() -> Self {
Self::default()
}
- /// Sets the scrollbar width of the [`Scrollable`] .
+ /// Sets the scrollbar width of the [`Scrollbar`] .
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = width.into().0.max(0.0);
self
}
- /// Sets the scrollbar margin of the [`Scrollable`] .
+ /// Sets the scrollbar margin of the [`Scrollbar`] .
pub fn margin(mut self, margin: impl Into<Pixels>) -> Self {
self.margin = margin.into().0;
self
}
- /// Sets the scroller width of the [`Scrollable`] .
+ /// Sets the scroller width of the [`Scrollbar`] .
pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
self.scroller_width = scroller_width.into().0.max(0.0);
self
}
- /// Sets the alignment of the [`Scrollable`] .
- pub fn alignment(mut self, alignment: Alignment) -> Self {
+ /// Sets the [`Anchor`] of the [`Scrollbar`] .
+ pub fn anchor(mut self, alignment: Anchor) -> Self {
self.alignment = alignment;
self
}
+
+ /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using
+ /// the given spacing between itself and the contents.
+ ///
+ /// An embedded [`Scrollbar`] will always be displayed, will take layout space,
+ /// and will not float over the contents.
+ pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
+ self.spacing = Some(spacing.into().0);
+ self
+ }
}
-/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction.
+/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`]
+/// on a given axis.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
-pub enum Alignment {
- /// Content is aligned to the start of the [`Viewport`].
+pub enum Anchor {
+ /// Scroller is anchoer to the start of the [`Viewport`].
#[default]
Start,
- /// Content is aligned to the end of the [`Viewport`]
+ /// Content is aligned to the end of the [`Viewport`].
End,
}
@@ -265,29 +379,55 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout::contained(limits, self.width, self.height, |limits| {
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, limits.min().height),
- Size::new(
- if self.direction.horizontal().is_some() {
- f32::INFINITY
- } else {
- limits.max().width
- },
- if self.direction.vertical().is_some() {
- f32::MAX
- } else {
- limits.max().height
- },
- ),
- );
+ let (right_padding, bottom_padding) = match self.direction {
+ Direction::Vertical(Scrollbar {
+ width,
+ margin,
+ spacing: Some(spacing),
+ ..
+ }) => (width + margin * 2.0 + spacing, 0.0),
+ Direction::Horizontal(Scrollbar {
+ width,
+ margin,
+ spacing: Some(spacing),
+ ..
+ }) => (0.0, width + margin * 2.0 + spacing),
+ _ => (0.0, 0.0),
+ };
- self.content.as_widget().layout(
- &mut tree.children[0],
- renderer,
- &child_limits,
- )
- })
+ layout::padded(
+ limits,
+ self.width,
+ self.height,
+ Padding {
+ right: right_padding,
+ bottom: bottom_padding,
+ ..Padding::ZERO
+ },
+ |limits| {
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, limits.min().height),
+ Size::new(
+ if self.direction.horizontal().is_some() {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ if self.direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
+ ),
+ );
+
+ self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &child_limits,
+ )
+ },
+ )
}
fn operate(
@@ -295,7 +435,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
let state = tree.state.downcast_mut::<State>();
@@ -309,6 +449,7 @@ where
state,
self.id.as_ref().map(|id| &id.0),
bounds,
+ content_bounds,
translation,
);
@@ -350,6 +491,24 @@ where
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
scrollbars.is_mouse_over(cursor);
+ if let Some(last_scrolled) = state.last_scrolled {
+ let clear_transaction = match event {
+ Event::Mouse(
+ mouse::Event::ButtonPressed(_)
+ | mouse::Event::ButtonReleased(_)
+ | mouse::Event::CursorLeft,
+ ) => true,
+ Event::Mouse(mouse::Event::CursorMoved { .. }) => {
+ last_scrolled.elapsed() > Duration::from_millis(100)
+ }
+ _ => last_scrolled.elapsed() > Duration::from_millis(1500),
+ };
+
+ if clear_transaction {
+ state.last_scrolled = None;
+ }
+ }
+
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
match event {
Event::Mouse(mouse::Event::CursorMoved { .. })
@@ -368,7 +527,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -406,7 +565,7 @@ where
state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -439,7 +598,7 @@ where
content_bounds,
);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -477,7 +636,7 @@ where
state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -492,7 +651,11 @@ where
}
}
- let mut event_status = {
+ let content_status = if state.last_scrolled.is_some()
+ && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
+ {
+ event::Status::Ignored
+ } else {
let cursor = match cursor_over_scrollable {
Some(cursor_position)
if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
@@ -540,10 +703,10 @@ where
state.x_scroller_grabbed_at = None;
state.y_scroller_grabbed_at = None;
- return event_status;
+ return content_status;
}
- if let event::Status::Captured = event_status {
+ if let event::Status::Captured = content_status {
return event::Status::Captured;
}
@@ -563,23 +726,41 @@ where
let delta = match delta {
mouse::ScrollDelta::Lines { x, y } => {
- // TODO: Configurable speed/friction (?)
- let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed
- && state.keyboard_modifiers.shift()
- {
- Vector::new(y, x)
- } else {
+ let is_shift_pressed = state.keyboard_modifiers.shift();
+
+ // macOS automatically inverts the axes when Shift is pressed
+ let (x, y) =
+ if cfg!(target_os = "macos") && is_shift_pressed {
+ (y, x)
+ } else {
+ (x, y)
+ };
+
+ let is_vertical = match self.direction {
+ Direction::Vertical(_) => true,
+ Direction::Horizontal(_) => false,
+ Direction::Both { .. } => !is_shift_pressed,
+ };
+
+ let movement = if is_vertical {
Vector::new(x, y)
+ } else {
+ Vector::new(y, x)
};
- movement * 60.0
+ // TODO: Configurable speed/friction (?)
+ -movement * 60.0
}
mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
};
- state.scroll(delta, self.direction, bounds, content_bounds);
+ state.scroll(
+ self.direction.align(delta),
+ bounds,
+ content_bounds,
+ );
- event_status = if notify_on_scroll(
+ if notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -589,7 +770,7 @@ where
event::Status::Captured
} else {
event::Status::Ignored
- };
+ }
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
@@ -613,13 +794,12 @@ where
};
let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
+ scroll_box_touched_at.x - cursor_position.x,
+ scroll_box_touched_at.y - cursor_position.y,
);
state.scroll(
- delta,
- self.direction,
+ self.direction.align(delta),
bounds,
content_bounds,
);
@@ -628,7 +808,7 @@ where
Some(cursor_position);
// TODO: bubble up touch movements if not consumed.
- let _ = notify_on_scroll(
+ let _ = notify_scroll(
state,
&self.on_scroll,
bounds,
@@ -640,12 +820,21 @@ where
_ => {}
}
- event_status = event::Status::Captured;
+ event::Status::Captured
}
- _ => {}
- }
+ Event::Window(window::Event::RedrawRequested(_)) => {
+ let _ = notify_viewport(
+ state,
+ &self.on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
- event_status
+ event::Status::Ignored
+ }
+ _ => event::Status::Ignored,
+ }
}
fn draw(
@@ -736,7 +925,7 @@ where
let draw_scrollbar =
|renderer: &mut Renderer,
- style: Scrollbar,
+ style: Rail,
scrollbar: &internals::Scrollbar| {
if scrollbar.bounds.width > 0.0
&& scrollbar.bounds.height > 0.0
@@ -756,21 +945,23 @@ where
);
}
- if scrollbar.scroller.bounds.width > 0.0
- && scrollbar.scroller.bounds.height > 0.0
- && (style.scroller.color != Color::TRANSPARENT
- || (style.scroller.border.color
- != Color::TRANSPARENT
- && style.scroller.border.width > 0.0))
- {
- renderer.fill_quad(
- renderer::Quad {
- bounds: scrollbar.scroller.bounds,
- border: style.scroller.border,
- ..renderer::Quad::default()
- },
- style.scroller.color,
- );
+ if let Some(scroller) = scrollbar.scroller {
+ if scroller.bounds.width > 0.0
+ && scroller.bounds.height > 0.0
+ && (style.scroller.color != Color::TRANSPARENT
+ || (style.scroller.border.color
+ != Color::TRANSPARENT
+ && style.scroller.border.width > 0.0))
+ {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: scroller.bounds,
+ border: style.scroller.border,
+ ..renderer::Quad::default()
+ },
+ style.scroller.color,
+ );
+ }
}
};
@@ -784,7 +975,7 @@ where
if let Some(scrollbar) = scrollbars.y {
draw_scrollbar(
renderer,
- style.vertical_scrollbar,
+ style.vertical_rail,
&scrollbar,
);
}
@@ -792,7 +983,7 @@ where
if let Some(scrollbar) = scrollbars.x {
draw_scrollbar(
renderer,
- style.horizontal_scrollbar,
+ style.horizontal_rail,
&scrollbar,
);
}
@@ -953,21 +1144,44 @@ impl From<Id> for widget::Id {
}
/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`]
-/// to the provided `percentage` along the x & y axis.
+/// to the provided [`RelativeOffset`].
pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> {
- Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
+ task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset)))
}
/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
-/// to the provided [`AbsoluteOffset`] along the x & y axis.
+/// to the provided [`AbsoluteOffset`].
pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
- Task::effect(Action::widget(operation::scrollable::scroll_to(
+ task::effect(Action::widget(operation::scrollable::scroll_to(
id.0, offset,
)))
}
-/// Returns [`true`] if the viewport actually changed.
-fn notify_on_scroll<Message>(
+/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`]
+/// by the provided [`AbsoluteOffset`].
+pub fn scroll_by<T>(id: Id, offset: AbsoluteOffset) -> Task<T> {
+ task::effect(Action::widget(operation::scrollable::scroll_by(
+ id.0, offset,
+ )))
+}
+
+fn notify_scroll<Message>(
+ state: &mut State,
+ on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ shell: &mut Shell<'_, Message>,
+) -> bool {
+ if notify_viewport(state, on_scroll, bounds, content_bounds, shell) {
+ state.last_scrolled = Some(Instant::now());
+
+ true
+ } else {
+ false
+ }
+}
+
+fn notify_viewport<Message>(
state: &mut State,
on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
bounds: Rectangle,
@@ -980,6 +1194,11 @@ fn notify_on_scroll<Message>(
return false;
}
+ let Some(on_scroll) = on_scroll else {
+ state.last_notified = None;
+ return false;
+ };
+
let viewport = Viewport {
offset_x: state.offset_x,
offset_y: state.offset_y,
@@ -999,7 +1218,9 @@ fn notify_on_scroll<Message>(
(a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
};
- if unchanged(last_relative_offset.x, current_relative_offset.x)
+ if last_notified.bounds == bounds
+ && last_notified.content_bounds == content_bounds
+ && unchanged(last_relative_offset.x, current_relative_offset.x)
&& unchanged(last_relative_offset.y, current_relative_offset.y)
&& unchanged(last_absolute_offset.x, current_absolute_offset.x)
&& unchanged(last_absolute_offset.y, current_absolute_offset.y)
@@ -1008,9 +1229,7 @@ fn notify_on_scroll<Message>(
}
}
- if let Some(on_scroll) = on_scroll {
- shell.publish(on_scroll(viewport));
- }
+ shell.publish(on_scroll(viewport));
state.last_notified = Some(viewport);
true
@@ -1025,6 +1244,7 @@ struct State {
x_scroller_grabbed_at: Option<f32>,
keyboard_modifiers: keyboard::Modifiers,
last_notified: Option<Viewport>,
+ last_scrolled: Option<Instant>,
}
impl Default for State {
@@ -1037,6 +1257,7 @@ impl Default for State {
x_scroller_grabbed_at: None,
keyboard_modifiers: keyboard::Modifiers::default(),
last_notified: None,
+ last_scrolled: None,
}
}
}
@@ -1049,6 +1270,15 @@ impl operation::Scrollable for State {
fn scroll_to(&mut self, offset: AbsoluteOffset) {
State::scroll_to(self, offset);
}
+
+ fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ State::scroll_by(self, offset, bounds, content_bounds);
+ }
}
#[derive(Debug, Clone, Copy)]
@@ -1073,13 +1303,13 @@ impl Offset {
self,
viewport: f32,
content: f32,
- alignment: Alignment,
+ alignment: Anchor,
) -> f32 {
let offset = self.absolute(viewport, content);
match alignment {
- Alignment::Start => offset,
- Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0),
+ Anchor::Start => offset,
+ Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0),
}
}
}
@@ -1152,34 +1382,13 @@ impl State {
pub fn scroll(
&mut self,
delta: Vector<f32>,
- direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) {
- let horizontal_alignment = direction
- .horizontal()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let vertical_alignment = direction
- .vertical()
- .map(|p| p.alignment)
- .unwrap_or_default();
-
- let align = |alignment: Alignment, delta: f32| match alignment {
- Alignment::Start => delta,
- Alignment::End => -delta,
- };
-
- let delta = Vector::new(
- align(horizontal_alignment, delta.x),
- align(vertical_alignment, delta.y),
- );
-
if bounds.height < content_bounds.height {
self.offset_y = Offset::Absolute(
(self.offset_y.absolute(bounds.height, content_bounds.height)
- - delta.y)
+ + delta.y)
.clamp(0.0, content_bounds.height - bounds.height),
);
}
@@ -1187,7 +1396,7 @@ impl State {
if bounds.width < content_bounds.width {
self.offset_x = Offset::Absolute(
(self.offset_x.absolute(bounds.width, content_bounds.width)
- - delta.x)
+ + delta.x)
.clamp(0.0, content_bounds.width - bounds.width),
);
}
@@ -1233,6 +1442,16 @@ impl State {
self.offset_y = Offset::Absolute(offset.y.max(0.0));
}
+ /// Scroll by the provided [`AbsoluteOffset`].
+ pub fn scroll_by(
+ &mut self,
+ offset: AbsoluteOffset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ ) {
+ self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds);
+ }
+
/// Unsnaps the current scroll position, if snapped, given the bounds of the
/// [`Scrollable`] and its contents.
pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
@@ -1298,16 +1517,16 @@ impl Scrollbars {
) -> Self {
let translation = state.translation(direction, bounds, content_bounds);
- let show_scrollbar_x = direction
- .horizontal()
- .filter(|_| content_bounds.width > bounds.width);
+ let show_scrollbar_x = direction.horizontal().filter(|scrollbar| {
+ scrollbar.spacing.is_some() || content_bounds.width > bounds.width
+ });
- let show_scrollbar_y = direction
- .vertical()
- .filter(|_| content_bounds.height > bounds.height);
+ let show_scrollbar_y = direction.vertical().filter(|scrollbar| {
+ scrollbar.spacing.is_some() || content_bounds.height > bounds.height
+ });
let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
- let Properties {
+ let Scrollbar {
width,
margin,
scroller_width,
@@ -1341,26 +1560,35 @@ impl Scrollbars {
};
let ratio = bounds.height / content_bounds.height;
- // min height for easier grabbing with super tall content
- let scroller_height = (scrollbar_bounds.height * ratio).max(2.0);
- let scroller_offset =
- translation.y * ratio * scrollbar_bounds.height / bounds.height;
- let scroller_bounds = Rectangle {
- x: bounds.x + bounds.width
- - total_scrollbar_width / 2.0
- - scroller_width / 2.0,
- y: (scrollbar_bounds.y + scroller_offset).max(0.0),
- width: scroller_width,
- height: scroller_height,
+ let scroller = if ratio >= 1.0 {
+ None
+ } else {
+ // min height for easier grabbing with super tall content
+ let scroller_height =
+ (scrollbar_bounds.height * ratio).max(2.0);
+ let scroller_offset =
+ translation.y * ratio * scrollbar_bounds.height
+ / bounds.height;
+
+ let scroller_bounds = Rectangle {
+ x: bounds.x + bounds.width
+ - total_scrollbar_width / 2.0
+ - scroller_width / 2.0,
+ y: (scrollbar_bounds.y + scroller_offset).max(0.0),
+ width: scroller_width,
+ height: scroller_height,
+ };
+
+ Some(internals::Scroller {
+ bounds: scroller_bounds,
+ })
};
Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: internals::Scroller {
- bounds: scroller_bounds,
- },
+ scroller,
alignment: vertical.alignment,
})
} else {
@@ -1368,7 +1596,7 @@ impl Scrollbars {
};
let x_scrollbar = if let Some(horizontal) = show_scrollbar_x {
- let Properties {
+ let Scrollbar {
width,
margin,
scroller_width,
@@ -1402,26 +1630,34 @@ impl Scrollbars {
};
let ratio = bounds.width / content_bounds.width;
- // min width for easier grabbing with extra wide content
- let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
- let scroller_offset =
- translation.x * ratio * scrollbar_bounds.width / bounds.width;
- let scroller_bounds = Rectangle {
- x: (scrollbar_bounds.x + scroller_offset).max(0.0),
- y: bounds.y + bounds.height
- - total_scrollbar_height / 2.0
- - scroller_width / 2.0,
- width: scroller_length,
- height: scroller_width,
+ let scroller = if ratio >= 1.0 {
+ None
+ } else {
+ // min width for easier grabbing with extra wide content
+ let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
+ let scroller_offset =
+ translation.x * ratio * scrollbar_bounds.width
+ / bounds.width;
+
+ let scroller_bounds = Rectangle {
+ x: (scrollbar_bounds.x + scroller_offset).max(0.0),
+ y: bounds.y + bounds.height
+ - total_scrollbar_height / 2.0
+ - scroller_width / 2.0,
+ width: scroller_length,
+ height: scroller_width,
+ };
+
+ Some(internals::Scroller {
+ bounds: scroller_bounds,
+ })
};
Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: internals::Scroller {
- bounds: scroller_bounds,
- },
+ scroller,
alignment: horizontal.alignment,
})
} else {
@@ -1452,33 +1688,33 @@ impl Scrollbars {
}
fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
- self.y.and_then(|scrollbar| {
- if scrollbar.total_bounds.contains(cursor_position) {
- Some(if scrollbar.scroller.bounds.contains(cursor_position) {
- (cursor_position.y - scrollbar.scroller.bounds.y)
- / scrollbar.scroller.bounds.height
- } else {
- 0.5
- })
+ let scrollbar = self.y?;
+ let scroller = scrollbar.scroller?;
+
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scroller.bounds.contains(cursor_position) {
+ (cursor_position.y - scroller.bounds.y) / scroller.bounds.height
} else {
- None
- }
- })
+ 0.5
+ })
+ } else {
+ None
+ }
}
fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> {
- self.x.and_then(|scrollbar| {
- if scrollbar.total_bounds.contains(cursor_position) {
- Some(if scrollbar.scroller.bounds.contains(cursor_position) {
- (cursor_position.x - scrollbar.scroller.bounds.x)
- / scrollbar.scroller.bounds.width
- } else {
- 0.5
- })
+ let scrollbar = self.x?;
+ let scroller = scrollbar.scroller?;
+
+ if scrollbar.total_bounds.contains(cursor_position) {
+ Some(if scroller.bounds.contains(cursor_position) {
+ (cursor_position.x - scroller.bounds.x) / scroller.bounds.width
} else {
- None
- }
- })
+ 0.5
+ })
+ } else {
+ None
+ }
}
fn active(&self) -> bool {
@@ -1489,14 +1725,14 @@ impl Scrollbars {
pub(super) mod internals {
use crate::core::{Point, Rectangle};
- use super::Alignment;
+ use super::Anchor;
#[derive(Debug, Copy, Clone)]
pub struct Scrollbar {
pub total_bounds: Rectangle,
pub bounds: Rectangle,
- pub scroller: Scroller,
- pub alignment: Alignment,
+ pub scroller: Option<Scroller>,
+ pub alignment: Anchor,
}
impl Scrollbar {
@@ -1511,14 +1747,18 @@ pub(super) mod internals {
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- let percentage = (cursor_position.y
- - self.bounds.y
- - self.scroller.bounds.height * grabbed_at)
- / (self.bounds.height - self.scroller.bounds.height);
-
- match self.alignment {
- Alignment::Start => percentage,
- Alignment::End => 1.0 - percentage,
+ if let Some(scroller) = self.scroller {
+ let percentage = (cursor_position.y
+ - self.bounds.y
+ - scroller.bounds.height * grabbed_at)
+ / (self.bounds.height - scroller.bounds.height);
+
+ match self.alignment {
+ Anchor::Start => percentage,
+ Anchor::End => 1.0 - percentage,
+ }
+ } else {
+ 0.0
}
}
@@ -1528,14 +1768,18 @@ pub(super) mod internals {
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
- let percentage = (cursor_position.x
- - self.bounds.x
- - self.scroller.bounds.width * grabbed_at)
- / (self.bounds.width - self.scroller.bounds.width);
-
- match self.alignment {
- Alignment::Start => percentage,
- Alignment::End => 1.0 - percentage,
+ if let Some(scroller) = self.scroller {
+ let percentage = (cursor_position.x
+ - self.bounds.x
+ - scroller.bounds.width * grabbed_at)
+ / (self.bounds.width - scroller.bounds.width);
+
+ match self.alignment {
+ Anchor::Start => percentage,
+ Anchor::End => 1.0 - percentage,
+ }
+ } else {
+ 0.0
}
}
}
@@ -1569,22 +1813,22 @@ pub enum Status {
},
}
-/// The appearance of a scrolable.
+/// The appearance of a scrollable.
#[derive(Debug, Clone, Copy)]
pub struct Style {
/// The [`container::Style`] of a scrollable.
pub container: container::Style,
- /// The vertical [`Scrollbar`] appearance.
- pub vertical_scrollbar: Scrollbar,
- /// The horizontal [`Scrollbar`] appearance.
- pub horizontal_scrollbar: Scrollbar,
+ /// The vertical [`Rail`] appearance.
+ pub vertical_rail: Rail,
+ /// The horizontal [`Rail`] appearance.
+ pub horizontal_rail: Rail,
/// The [`Background`] of the gap between a horizontal and vertical scrollbar.
pub gap: Option<Background>,
}
/// The appearance of the scrollbar of a scrollable.
#[derive(Debug, Clone, Copy)]
-pub struct Scrollbar {
+pub struct Rail {
/// The [`Background`] of a scrollbar.
pub background: Option<Background>,
/// The [`Border`] of a scrollbar.
@@ -1633,27 +1877,27 @@ impl Catalog for Theme {
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let scrollbar = Scrollbar {
+ let scrollbar = Rail {
background: Some(palette.background.weak.color.into()),
- border: Border::rounded(2),
+ border: border::rounded(2),
scroller: Scroller {
color: palette.background.strong.color,
- border: Border::rounded(2),
+ border: border::rounded(2),
},
};
match status {
Status::Active => Style {
container: container::Style::default(),
- vertical_scrollbar: scrollbar,
- horizontal_scrollbar: scrollbar,
+ vertical_rail: scrollbar,
+ horizontal_rail: scrollbar,
gap: None,
},
Status::Hovered {
is_horizontal_scrollbar_hovered,
is_vertical_scrollbar_hovered,
} => {
- let hovered_scrollbar = Scrollbar {
+ let hovered_scrollbar = Rail {
scroller: Scroller {
color: palette.primary.strong.color,
..scrollbar.scroller
@@ -1663,12 +1907,12 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
container: container::Style::default(),
- vertical_scrollbar: if is_vertical_scrollbar_hovered {
+ vertical_rail: if is_vertical_scrollbar_hovered {
hovered_scrollbar
} else {
scrollbar
},
- horizontal_scrollbar: if is_horizontal_scrollbar_hovered {
+ horizontal_rail: if is_horizontal_scrollbar_hovered {
hovered_scrollbar
} else {
scrollbar
@@ -1680,7 +1924,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
is_horizontal_scrollbar_dragged,
is_vertical_scrollbar_dragged,
} => {
- let dragged_scrollbar = Scrollbar {
+ let dragged_scrollbar = Rail {
scroller: Scroller {
color: palette.primary.base.color,
..scrollbar.scroller
@@ -1690,12 +1934,12 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
container: container::Style::default(),
- vertical_scrollbar: if is_vertical_scrollbar_dragged {
+ vertical_rail: if is_vertical_scrollbar_dragged {
dragged_scrollbar
} else {
scrollbar
},
- horizontal_scrollbar: if is_horizontal_scrollbar_dragged {
+ horizontal_rail: if is_horizontal_scrollbar_dragged {
dragged_scrollbar
} else {
scrollbar
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index a8f1d192..aebf68e2 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -1,5 +1,5 @@
//! Display an interactive selector of a single value from a range of values.
-use crate::core::border;
+use crate::core::border::{self, Border};
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
@@ -9,7 +9,7 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
+ self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Rectangle, Shell, Size, Theme, Widget,
};
@@ -70,8 +70,8 @@ where
/// * an inclusive range of possible values
/// * the current value of the [`Slider`]
/// * a function that will be called when the [`Slider`] is dragged.
- /// It receives the new value of the [`Slider`] and must produce a
- /// `Message`.
+ /// It receives the new value of the [`Slider`] and must produce a
+ /// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
F: 'a + Fn(T) -> Message,
@@ -237,7 +237,7 @@ where
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
- T::from_f64(value)
+ T::from_f64(value.min(end))
};
new_value
@@ -408,10 +408,10 @@ where
width: offset + handle_width / 2.0,
height: style.rail.width,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.0,
+ style.rail.backgrounds.0,
);
renderer.fill_quad(
@@ -422,10 +422,10 @@ where
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.1,
+ style.rail.backgrounds.1,
);
renderer.fill_quad(
@@ -443,7 +443,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}
@@ -524,12 +524,12 @@ impl Style {
/// 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 backgrounds of the rail of the slider.
+ pub backgrounds: (Background, Background),
/// The width of the stroke of a slider rail.
pub width: f32,
- /// The border radius of the corners of the rail.
- pub border_radius: border::Radius,
+ /// The border of the rail.
+ pub border: Border,
}
/// The appearance of the handle of a slider.
@@ -537,8 +537,8 @@ pub struct Rail {
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
- /// The [`Color`] of the handle.
- pub color: Color,
+ /// The [`Background`] of the handle.
+ pub background: Background,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
@@ -601,13 +601,17 @@ pub fn default(theme: &Theme, status: Status) -> Style {
Style {
rail: Rail {
- colors: (color, palette.secondary.base.color),
+ backgrounds: (color.into(), palette.secondary.base.color.into()),
width: 4.0,
- border_radius: 2.0.into(),
+ border: Border {
+ radius: 2.0.into(),
+ width: 0.0,
+ color: Color::TRANSPARENT,
+ },
},
handle: Handle {
shape: HandleShape::Circle { radius: 7.0 },
- color,
+ background: color.into(),
border_color: Color::TRANSPARENT,
border_width: 0.0,
},
diff --git a/widget/src/stack.rs b/widget/src/stack.rs
index efa9711d..9ccaa274 100644
--- a/widget/src/stack.rs
+++ b/widget/src/stack.rs
@@ -189,7 +189,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.children
@@ -209,19 +209,23 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor: mouse::Cursor,
+ mut cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
+ let is_over_scroll =
+ matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. }))
+ && cursor.is_over(layout.bounds());
+
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(
+ let status = child.as_widget_mut().on_event(
state,
event.clone(),
layout,
@@ -230,7 +234,19 @@ where
clipboard,
shell,
viewport,
- )
+ );
+
+ if is_over_scroll && cursor != mouse::Cursor::Unavailable {
+ let interaction = child.as_widget().mouse_interaction(
+ state, layout, cursor, viewport, renderer,
+ );
+
+ if interaction != mouse::Interaction::None {
+ cursor = mouse::Cursor::Unavailable;
+ }
+ }
+
+ status
})
.find(|&status| status == event::Status::Captured)
.unwrap_or(event::Status::Ignored)
@@ -269,15 +285,53 @@ where
viewport: &Rectangle,
) {
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
- for (i, ((layer, state), layout)) in self
+ let layers_below = if cursor.is_over(layout.bounds()) {
+ self.children
+ .iter()
+ .rev()
+ .zip(tree.children.iter().rev())
+ .zip(layout.children().rev())
+ .position(|((layer, state), layout)| {
+ let interaction = layer.as_widget().mouse_interaction(
+ state, layout, cursor, viewport, renderer,
+ );
+
+ interaction != mouse::Interaction::None
+ })
+ .map(|i| self.children.len() - i - 1)
+ .unwrap_or_default()
+ } else {
+ 0
+ };
+
+ let mut layers = self
.children
.iter()
.zip(&tree.children)
.zip(layout.children())
- .enumerate()
- {
- if i > 0 {
- renderer.with_layer(clipped_viewport, |renderer| {
+ .enumerate();
+
+ let layers = layers.by_ref();
+
+ let mut draw_layer =
+ |i,
+ layer: &Element<'a, Message, Theme, Renderer>,
+ state,
+ layout,
+ cursor| {
+ if i > 0 {
+ renderer.with_layer(clipped_viewport, |renderer| {
+ layer.as_widget().draw(
+ state,
+ renderer,
+ theme,
+ style,
+ layout,
+ cursor,
+ &clipped_viewport,
+ );
+ });
+ } else {
layer.as_widget().draw(
state,
renderer,
@@ -287,18 +341,15 @@ where
cursor,
&clipped_viewport,
);
- });
- } else {
- layer.as_widget().draw(
- state,
- renderer,
- theme,
- style,
- layout,
- cursor,
- &clipped_viewport,
- );
- }
+ }
+ };
+
+ for (i, ((layer, state), layout)) in layers.take(layers_below) {
+ draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable);
+ }
+
+ for (i, ((layer, state), layout)) in layers {
+ draw_layer(i, layer, state, layout, cursor);
}
}
}
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index 4551bcad..bec0090f 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -211,11 +211,13 @@ where
let render = |renderer: &mut Renderer| {
renderer.draw_svg(
- self.handle.clone(),
- style.color,
+ svg::Svg {
+ handle: self.handle.clone(),
+ color: style.color,
+ rotation: self.rotation.radians(),
+ opacity: self.opacity,
+ },
drawing_bounds,
- self.rotation.radians(),
- self.opacity,
);
};
diff --git a/widget/src/text.rs b/widget/src/text.rs
index 0d689295..9bf7fce4 100644
--- a/widget/src/text.rs
+++ b/widget/src/text.rs
@@ -1,5 +1,9 @@
//! Draw and interact with text.
+mod rich;
+
+pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span};
pub use crate::core::widget::text::*;
+pub use rich::Rich;
/// A paragraph.
pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> =
diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs
new file mode 100644
index 00000000..921c55a5
--- /dev/null
+++ b/widget/src/text/rich.rs
@@ -0,0 +1,538 @@
+use crate::core::alignment;
+use crate::core::event;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text::{Paragraph, Span};
+use crate::core::widget::text::{
+ self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
+};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
+ Rectangle, Shell, Size, Vector, Widget,
+};
+
+/// A bunch of [`Rich`] text.
+#[allow(missing_debug_implementations)]
+pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+{
+ spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
+ size: Option<Pixels>,
+ line_height: LineHeight,
+ width: Length,
+ height: Length,
+ font: Option<Renderer::Font>,
+ align_x: alignment::Horizontal,
+ align_y: alignment::Vertical,
+ wrapping: Wrapping,
+ class: Theme::Class<'a>,
+}
+
+impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ /// Creates a new empty [`Rich`] text.
+ pub fn new() -> Self {
+ Self {
+ spans: Box::new([]),
+ size: None,
+ line_height: LineHeight::default(),
+ width: Length::Shrink,
+ height: Length::Shrink,
+ font: None,
+ align_x: alignment::Horizontal::Left,
+ align_y: alignment::Vertical::Top,
+ wrapping: Wrapping::default(),
+ class: Theme::default(),
+ }
+ }
+
+ /// Creates a new [`Rich`] text with the given text spans.
+ pub fn with_spans(
+ spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
+ ) -> Self {
+ Self {
+ spans: Box::new(spans),
+ ..Self::new()
+ }
+ }
+
+ /// Sets the default size of the [`Rich`] text.
+ pub fn size(mut self, size: impl Into<Pixels>) -> Self {
+ self.size = Some(size.into());
+ self
+ }
+
+ /// Sets the defualt [`LineHeight`] of the [`Rich`] text.
+ pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
+ self.line_height = line_height.into();
+ self
+ }
+
+ /// Sets the default font of the [`Rich`] text.
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the width of the [`Rich`] text boundaries.
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Rich`] text boundaries.
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Centers the [`Rich`] text, both horizontally and vertically.
+ pub fn center(self) -> Self {
+ self.align_x(alignment::Horizontal::Center)
+ .align_y(alignment::Vertical::Center)
+ }
+
+ /// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.align_x = alignment.into();
+ self
+ }
+
+ /// Sets the [`alignment::Vertical`] of the [`Rich`] text.
+ pub fn align_y(
+ mut self,
+ alignment: impl Into<alignment::Vertical>,
+ ) -> Self {
+ self.align_y = alignment.into();
+ self
+ }
+
+ /// Sets the [`Wrapping`] strategy of the [`Rich`] text.
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
+ /// Sets the default style of the [`Rich`] text.
+ #[must_use]
+ pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
+ self
+ }
+
+ /// Sets the default [`Color`] of the [`Rich`] text.
+ pub fn color(self, color: impl Into<Color>) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ self.color_maybe(Some(color))
+ }
+
+ /// Sets the default [`Color`] of the [`Rich`] text, if `Some`.
+ pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ let color = color.map(Into::into);
+
+ self.style(move |_theme| Style { color })
+ }
+
+ /// Sets the default style class of the [`Rich`] text.
+ #[cfg(feature = "advanced")]
+ #[must_use]
+ pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
+ self.class = class.into();
+ self
+ }
+}
+
+impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+struct State<Link, P: Paragraph> {
+ spans: Vec<Span<'static, Link, P::Font>>,
+ span_pressed: Option<usize>,
+ paragraph: P,
+}
+
+impl<'a, Link, Theme, Renderer> Widget<Link, Theme, Renderer>
+ for Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State<Link, Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::<Link, _> {
+ spans: Vec::new(),
+ span_pressed: None,
+ paragraph: Renderer::Paragraph::default(),
+ })
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ layout(
+ tree.state
+ .downcast_mut::<State<Link, Renderer::Paragraph>>(),
+ renderer,
+ limits,
+ self.width,
+ self.height,
+ self.spans.as_ref().as_ref(),
+ self.line_height,
+ self.size,
+ self.font,
+ self.align_x,
+ self.align_y,
+ self.wrapping,
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ defaults: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ let state = tree
+ .state
+ .downcast_ref::<State<Link, Renderer::Paragraph>>();
+
+ let style = theme.style(&self.class);
+
+ let hovered_span = cursor
+ .position_in(layout.bounds())
+ .and_then(|position| state.paragraph.hit_span(position));
+
+ for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
+ let is_hovered_link =
+ span.link.is_some() && Some(index) == hovered_span;
+
+ if span.highlight.is_some()
+ || span.underline
+ || span.strikethrough
+ || is_hovered_link
+ {
+ let translation = layout.position() - Point::ORIGIN;
+ let regions = state.paragraph.span_bounds(index);
+
+ if let Some(highlight) = span.highlight {
+ for bounds in &regions {
+ let bounds = Rectangle::new(
+ bounds.position()
+ - Vector::new(
+ span.padding.left,
+ span.padding.top,
+ ),
+ bounds.size()
+ + Size::new(
+ span.padding.horizontal(),
+ span.padding.vertical(),
+ ),
+ );
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: bounds + translation,
+ border: highlight.border,
+ ..Default::default()
+ },
+ highlight.background,
+ );
+ }
+ }
+
+ if span.underline || span.strikethrough || is_hovered_link {
+ let size = span
+ .size
+ .or(self.size)
+ .unwrap_or(renderer.default_size());
+
+ let line_height = span
+ .line_height
+ .unwrap_or(self.line_height)
+ .to_absolute(size);
+
+ let color = span
+ .color
+ .or(style.color)
+ .unwrap_or(defaults.text_color);
+
+ let baseline = translation
+ + Vector::new(
+ 0.0,
+ size.0 + (line_height.0 - size.0) / 2.0,
+ );
+
+ if span.underline || is_hovered_link {
+ for bounds in &regions {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle::new(
+ bounds.position() + baseline
+ - Vector::new(0.0, size.0 * 0.08),
+ Size::new(bounds.width, 1.0),
+ ),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ }
+
+ if span.strikethrough {
+ for bounds in &regions {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle::new(
+ bounds.position() + baseline
+ - Vector::new(0.0, size.0 / 2.0),
+ Size::new(bounds.width, 1.0),
+ ),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ text::draw(
+ renderer,
+ defaults,
+ layout,
+ &state.paragraph,
+ style,
+ viewport,
+ );
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Link>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ if let Some(position) = cursor.position_in(layout.bounds()) {
+ let state = tree
+ .state
+ .downcast_mut::<State<Link, Renderer::Paragraph>>();
+
+ if let Some(span) = state.paragraph.hit_span(position) {
+ state.span_pressed = Some(span);
+
+ return event::Status::Captured;
+ }
+ }
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
+ let state = tree
+ .state
+ .downcast_mut::<State<Link, Renderer::Paragraph>>();
+
+ if let Some(span_pressed) = state.span_pressed {
+ state.span_pressed = None;
+
+ if let Some(position) = cursor.position_in(layout.bounds())
+ {
+ match state.paragraph.hit_span(position) {
+ Some(span) if span == span_pressed => {
+ if let Some(link) = self
+ .spans
+ .as_ref()
+ .as_ref()
+ .get(span)
+ .and_then(|span| span.link.clone())
+ {
+ shell.publish(link);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+
+ event::Status::Ignored
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ if let Some(position) = cursor.position_in(layout.bounds()) {
+ let state = tree
+ .state
+ .downcast_ref::<State<Link, Renderer::Paragraph>>();
+
+ if let Some(span) = state
+ .paragraph
+ .hit_span(position)
+ .and_then(|span| self.spans.as_ref().as_ref().get(span))
+ {
+ if span.link.is_some() {
+ return mouse::Interaction::Pointer;
+ }
+ }
+ }
+
+ mouse::Interaction::None
+ }
+}
+
+fn layout<Link, Renderer>(
+ state: &mut State<Link, Renderer::Paragraph>,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ width: Length,
+ height: Length,
+ spans: &[Span<'_, Link, Renderer::Font>],
+ line_height: LineHeight,
+ size: Option<Pixels>,
+ font: Option<Renderer::Font>,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ wrapping: Wrapping,
+) -> layout::Node
+where
+ Link: Clone,
+ Renderer: core::text::Renderer,
+{
+ layout::sized(limits, width, height, |limits| {
+ let bounds = limits.max();
+
+ let size = size.unwrap_or_else(|| renderer.default_size());
+ let font = font.unwrap_or_else(|| renderer.default_font());
+
+ let text_with_spans = || core::Text {
+ content: spans,
+ bounds,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping: Shaping::Advanced,
+ wrapping,
+ };
+
+ if state.spans != spans {
+ state.paragraph =
+ Renderer::Paragraph::with_spans(text_with_spans());
+ state.spans = spans.iter().cloned().map(Span::to_static).collect();
+ } else {
+ match state.paragraph.compare(core::Text {
+ content: (),
+ bounds,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping: Shaping::Advanced,
+ wrapping,
+ }) {
+ core::text::Difference::None => {}
+ core::text::Difference::Bounds => {
+ state.paragraph.resize(bounds);
+ }
+ core::text::Difference::Shape => {
+ state.paragraph =
+ Renderer::Paragraph::with_spans(text_with_spans());
+ }
+ }
+ }
+
+ state.paragraph.min_bounds()
+ })
+}
+
+impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
+ for Rich<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
+ spans: T,
+ ) -> Self {
+ Self::with_spans(spans.into_iter().collect::<Vec<_>>())
+ }
+}
+
+impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>>
+ for Element<'a, Link, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer + 'a,
+{
+ fn from(
+ text: Rich<'a, Link, Theme, Renderer>,
+ ) -> Element<'a, Link, Theme, Renderer> {
+ Element::new(text)
+ }
+}
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index fc2ade43..e0102656 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -1,4 +1,5 @@
//! Display a multi-line text input for text editing.
+use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
use crate::core::event::{self, Event};
use crate::core::keyboard;
@@ -8,11 +9,14 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::editor::{Cursor, Editor as _};
use crate::core::text::highlighter::{self, Highlighter};
-use crate::core::text::{self, LineHeight};
+use crate::core::text::{self, LineHeight, Text, Wrapping};
+use crate::core::time::{Duration, Instant};
+use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
+use crate::core::window;
use crate::core::{
- Background, Border, Color, Element, Length, Padding, Pixels, Rectangle,
- Shell, Size, Theme, Vector,
+ Background, Border, Color, Element, Length, Padding, Pixels, Point,
+ Rectangle, Shell, Size, SmolStr, Theme, Vector,
};
use std::cell::RefCell;
@@ -36,13 +40,16 @@ pub struct TextEditor<
Renderer: text::Renderer,
{
content: &'a Content<Renderer>,
+ placeholder: Option<text::Fragment<'a>>,
font: Option<Renderer::Font>,
text_size: Option<Pixels>,
line_height: LineHeight,
width: Length,
height: Length,
padding: Padding,
+ wrapping: Wrapping,
class: Theme::Class<'a>,
+ key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings,
highlighter_format: fn(
@@ -61,13 +68,16 @@ where
pub fn new(content: &'a Content<Renderer>) -> Self {
Self {
content,
+ placeholder: None,
font: None,
text_size: None,
line_height: LineHeight::default(),
width: Length::Fill,
height: Length::Shrink,
padding: Padding::new(5.0),
+ wrapping: Wrapping::default(),
class: Theme::default(),
+ key_binding: None,
on_edit: None,
highlighter_settings: (),
highlighter_format: |_highlight, _theme| {
@@ -84,6 +94,15 @@ where
Theme: Catalog,
Renderer: text::Renderer,
{
+ /// Sets the placeholder of the [`TextEditor`].
+ pub fn placeholder(
+ mut self,
+ placeholder: impl text::IntoFragment<'a>,
+ ) -> Self {
+ self.placeholder = Some(placeholder.into_fragment());
+ self
+ }
+
/// Sets the height of the [`TextEditor`].
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
@@ -131,9 +150,34 @@ where
self
}
+ /// Sets the [`Wrapping`] strategy of the [`TextEditor`].
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
+ /// Highlights the [`TextEditor`] using the given syntax and theme.
+ #[cfg(feature = "highlighter")]
+ pub fn highlight(
+ self,
+ syntax: &str,
+ theme: iced_highlighter::Theme,
+ ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer>
+ where
+ Renderer: text::Renderer<Font = crate::core::Font>,
+ {
+ self.highlight_with::<iced_highlighter::Highlighter>(
+ iced_highlighter::Settings {
+ theme,
+ token: syntax.to_owned(),
+ },
+ |highlight, _theme| highlight.to_format(),
+ )
+ }
+
/// Highlights the [`TextEditor`] with the given [`Highlighter`] and
/// a strategy to turn its highlights into some text format.
- pub fn highlight<H: text::Highlighter>(
+ pub fn highlight_with<H: text::Highlighter>(
self,
settings: H::Settings,
to_format: fn(
@@ -143,19 +187,33 @@ where
) -> TextEditor<'a, H, Message, Theme, Renderer> {
TextEditor {
content: self.content,
+ placeholder: self.placeholder,
font: self.font,
text_size: self.text_size,
line_height: self.line_height,
width: self.width,
height: self.height,
padding: self.padding,
+ wrapping: self.wrapping,
class: self.class,
+ key_binding: self.key_binding,
on_edit: self.on_edit,
highlighter_settings: settings,
highlighter_format: to_format,
}
}
+ /// Sets the closure to produce key bindings on key presses.
+ ///
+ /// See [`Binding`] for the list of available bindings.
+ pub fn key_binding(
+ mut self,
+ key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
+ ) -> Self {
+ self.key_binding = Some(Box::new(key_binding));
+ self
+ }
+
/// Sets the style of the [`TextEditor`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -322,7 +380,7 @@ where
/// The state of a [`TextEditor`].
#[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> {
- is_focused: bool,
+ focus: Option<Focus>,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
partial_scroll: f32,
@@ -331,10 +389,55 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}
+#[derive(Debug, Clone, Copy)]
+struct Focus {
+ updated_at: Instant,
+ now: Instant,
+ is_window_focused: bool,
+}
+
+impl Focus {
+ const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
+
+ fn now() -> Self {
+ let now = Instant::now();
+
+ Self {
+ updated_at: now,
+ now,
+ is_window_focused: true,
+ }
+ }
+
+ fn is_cursor_visible(&self) -> bool {
+ self.is_window_focused
+ && ((self.now - self.updated_at).as_millis()
+ / Self::CURSOR_BLINK_INTERVAL_MILLIS)
+ % 2
+ == 0
+ }
+}
+
impl<Highlighter: text::Highlighter> State<Highlighter> {
/// Returns whether the [`TextEditor`] is currently focused or not.
pub fn is_focused(&self) -> bool {
- self.is_focused
+ self.focus.is_some()
+ }
+}
+
+impl<Highlighter: text::Highlighter> operation::Focusable
+ for State<Highlighter>
+{
+ fn is_focused(&self) -> bool {
+ self.focus.is_some()
+ }
+
+ fn focus(&mut self) {
+ self.focus = Some(Focus::now());
+ }
+
+ fn unfocus(&mut self) {
+ self.focus = None;
}
}
@@ -351,7 +454,7 @@ where
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
- is_focused: false,
+ focus: None,
last_click: None,
drag_click: None,
partial_scroll: 0.0,
@@ -402,6 +505,7 @@ where
self.font.unwrap_or_else(|| renderer.default_font()),
self.text_size.unwrap_or_else(|| renderer.default_size()),
self.line_height,
+ self.wrapping,
state.highlighter.borrow_mut().deref_mut(),
);
@@ -439,12 +543,48 @@ where
let state = tree.state.downcast_mut::<State<Highlighter>>();
+ match event {
+ Event::Window(window::Event::Unfocused) => {
+ if let Some(focus) = &mut state.focus {
+ focus.is_window_focused = false;
+ }
+ }
+ Event::Window(window::Event::Focused) => {
+ if let Some(focus) = &mut state.focus {
+ focus.is_window_focused = true;
+ focus.updated_at = Instant::now();
+
+ shell.request_redraw(window::RedrawRequest::NextFrame);
+ }
+ }
+ Event::Window(window::Event::RedrawRequested(now)) => {
+ if let Some(focus) = &mut state.focus {
+ if focus.is_window_focused {
+ focus.now = now;
+
+ let millis_until_redraw =
+ Focus::CURSOR_BLINK_INTERVAL_MILLIS
+ - (now - focus.updated_at).as_millis()
+ % Focus::CURSOR_BLINK_INTERVAL_MILLIS;
+
+ shell.request_redraw(window::RedrawRequest::At(
+ now + Duration::from_millis(
+ millis_until_redraw as u64,
+ ),
+ ));
+ }
+ }
+ }
+ _ => {}
+ }
+
let Some(update) = Update::from_event(
event,
state,
layout.bounds(),
self.padding,
cursor,
+ self.key_binding.as_deref(),
) else {
return event::Status::Ignored;
};
@@ -459,12 +599,18 @@ where
mouse::click::Kind::Triple => Action::SelectLine,
};
- state.is_focused = true;
+ state.focus = Some(Focus::now());
state.last_click = Some(click);
state.drag_click = Some(click.kind());
shell.publish(on_edit(action));
}
+ Update::Drag(position) => {
+ shell.publish(on_edit(Action::Drag(position)));
+ }
+ Update::Release => {
+ state.drag_click = None;
+ }
Update::Scroll(lines) => {
let bounds = self.content.0.borrow().editor.bounds();
@@ -479,34 +625,105 @@ where
lines: lines as i32,
}));
}
- Update::Unfocus => {
- state.is_focused = false;
- state.drag_click = None;
- }
- Update::Release => {
- state.drag_click = None;
- }
- Update::Action(action) => {
- shell.publish(on_edit(action));
- }
- Update::Copy => {
- if let Some(selection) = self.content.selection() {
- clipboard.write(clipboard::Kind::Standard, selection);
- }
- }
- Update::Cut => {
- if let Some(selection) = self.content.selection() {
- clipboard.write(clipboard::Kind::Standard, selection);
- shell.publish(on_edit(Action::Edit(Edit::Delete)));
+ Update::Binding(binding) => {
+ fn apply_binding<
+ H: text::Highlighter,
+ R: text::Renderer,
+ Message,
+ >(
+ binding: Binding<Message>,
+ content: &Content<R>,
+ state: &mut State<H>,
+ on_edit: &dyn Fn(Action) -> Message,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) {
+ let mut publish = |action| shell.publish(on_edit(action));
+
+ match binding {
+ Binding::Unfocus => {
+ state.focus = None;
+ state.drag_click = None;
+ }
+ Binding::Copy => {
+ if let Some(selection) = content.selection() {
+ clipboard.write(
+ clipboard::Kind::Standard,
+ selection,
+ );
+ }
+ }
+ Binding::Cut => {
+ if let Some(selection) = content.selection() {
+ clipboard.write(
+ clipboard::Kind::Standard,
+ selection,
+ );
+
+ publish(Action::Edit(Edit::Delete));
+ }
+ }
+ Binding::Paste => {
+ if let Some(contents) =
+ clipboard.read(clipboard::Kind::Standard)
+ {
+ publish(Action::Edit(Edit::Paste(Arc::new(
+ contents,
+ ))));
+ }
+ }
+ Binding::Move(motion) => {
+ publish(Action::Move(motion));
+ }
+ Binding::Select(motion) => {
+ publish(Action::Select(motion));
+ }
+ Binding::SelectWord => {
+ publish(Action::SelectWord);
+ }
+ Binding::SelectLine => {
+ publish(Action::SelectLine);
+ }
+ Binding::SelectAll => {
+ publish(Action::SelectAll);
+ }
+ Binding::Insert(c) => {
+ publish(Action::Edit(Edit::Insert(c)));
+ }
+ Binding::Enter => {
+ publish(Action::Edit(Edit::Enter));
+ }
+ Binding::Backspace => {
+ publish(Action::Edit(Edit::Backspace));
+ }
+ Binding::Delete => {
+ publish(Action::Edit(Edit::Delete));
+ }
+ Binding::Sequence(sequence) => {
+ for binding in sequence {
+ apply_binding(
+ binding, content, state, on_edit,
+ clipboard, shell,
+ );
+ }
+ }
+ Binding::Custom(message) => {
+ shell.publish(message);
+ }
+ }
}
- }
- Update::Paste => {
- if let Some(contents) =
- clipboard.read(clipboard::Kind::Standard)
- {
- shell.publish(on_edit(Action::Edit(Edit::Paste(
- Arc::new(contents),
- ))));
+
+ apply_binding(
+ binding,
+ self.content,
+ state,
+ on_edit,
+ clipboard,
+ shell,
+ );
+
+ if let Some(focus) = &mut state.focus {
+ focus.updated_at = Instant::now();
}
}
}
@@ -522,15 +739,17 @@ where
defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- viewport: &Rectangle,
+ _viewport: &Rectangle,
) {
let bounds = layout.bounds();
let mut internal = self.content.0.borrow_mut();
let state = tree.state.downcast_ref::<State<Highlighter>>();
+ let font = self.font.unwrap_or_else(|| renderer.default_font());
+
internal.editor.highlight(
- self.font.unwrap_or_else(|| renderer.default_font()),
+ font,
state.highlighter.borrow_mut().deref_mut(),
|highlight| (self.highlighter_format)(highlight, theme),
);
@@ -540,7 +759,7 @@ where
let status = if is_disabled {
Status::Disabled
- } else if state.is_focused {
+ } else if state.focus.is_some() {
Status::Focused
} else if is_mouse_over {
Status::Hovered
@@ -559,22 +778,43 @@ where
style.background,
);
- renderer.fill_editor(
- &internal.editor,
- bounds.position()
- + Vector::new(self.padding.left, self.padding.top),
- defaults.text_color,
- *viewport,
- );
+ let text_bounds = bounds.shrink(self.padding);
+
+ if internal.editor.is_empty() {
+ if let Some(placeholder) = self.placeholder.clone() {
+ renderer.fill_text(
+ Text {
+ content: placeholder.into_owned(),
+ bounds: text_bounds.size(),
+ size: self
+ .text_size
+ .unwrap_or_else(|| renderer.default_size()),
+ line_height: self.line_height,
+ font,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: text::Shaping::Advanced,
+ wrapping: self.wrapping,
+ },
+ text_bounds.position(),
+ style.placeholder,
+ text_bounds,
+ );
+ }
+ } else {
+ renderer.fill_editor(
+ &internal.editor,
+ text_bounds.position(),
+ defaults.text_color,
+ text_bounds,
+ );
+ }
- let translation = Vector::new(
- bounds.x + self.padding.left,
- bounds.y + self.padding.top,
- );
+ let translation = text_bounds.position() - Point::ORIGIN;
- if state.is_focused {
+ if let Some(focus) = state.focus.as_ref() {
match internal.editor.cursor() {
- Cursor::Caret(position) => {
+ Cursor::Caret(position) if focus.is_cursor_visible() => {
let cursor =
Rectangle::new(
position + translation,
@@ -588,15 +828,12 @@ where
),
);
- if let Some(clipped_cursor) = bounds.intersection(&cursor) {
+ if let Some(clipped_cursor) =
+ text_bounds.intersection(&cursor)
+ {
renderer.fill_quad(
renderer::Quad {
- bounds: Rectangle {
- x: clipped_cursor.x.floor(),
- y: clipped_cursor.y,
- width: clipped_cursor.width,
- height: clipped_cursor.height,
- },
+ bounds: clipped_cursor,
..renderer::Quad::default()
},
style.value,
@@ -605,7 +842,7 @@ where
}
Cursor::Selection(ranges) => {
for range in ranges.into_iter().filter_map(|range| {
- bounds.intersection(&(range + translation))
+ text_bounds.intersection(&(range + translation))
}) {
renderer.fill_quad(
renderer::Quad {
@@ -616,6 +853,7 @@ where
);
}
}
+ Cursor::Caret(_) => {}
}
}
}
@@ -640,6 +878,18 @@ where
mouse::Interaction::default()
}
}
+
+ fn operate(
+ &self,
+ tree: &mut widget::Tree,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ operation: &mut dyn widget::Operation,
+ ) {
+ let state = tree.state.downcast_mut::<State<Highlighter>>();
+
+ operation.focusable(state, None);
+ }
}
impl<'a, Highlighter, Message, Theme, Renderer>
@@ -658,27 +908,144 @@ where
}
}
-enum Update {
- Click(mouse::Click),
- Scroll(f32),
+/// A binding to an action in the [`TextEditor`].
+#[derive(Debug, Clone, PartialEq)]
+pub enum Binding<Message> {
+ /// Unfocus the [`TextEditor`].
Unfocus,
- Release,
- Action(Action),
+ /// Copy the selection of the [`TextEditor`].
Copy,
+ /// Cut the selection of the [`TextEditor`].
Cut,
+ /// Paste the clipboard contents in the [`TextEditor`].
Paste,
+ /// Apply a [`Motion`].
+ Move(Motion),
+ /// Select text with a given [`Motion`].
+ Select(Motion),
+ /// Select the word at the current cursor.
+ SelectWord,
+ /// Select the line at the current cursor.
+ SelectLine,
+ /// Select the entire buffer.
+ SelectAll,
+ /// Insert the given character.
+ Insert(char),
+ /// Break the current line.
+ Enter,
+ /// Delete the previous character.
+ Backspace,
+ /// Delete the next character.
+ Delete,
+ /// A sequence of bindings to execute.
+ Sequence(Vec<Self>),
+ /// Produce the given message.
+ Custom(Message),
+}
+
+/// A key press.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct KeyPress {
+ /// The key pressed.
+ pub key: keyboard::Key,
+ /// The state of the keyboard modifiers.
+ pub modifiers: keyboard::Modifiers,
+ /// The text produced by the key press.
+ pub text: Option<SmolStr>,
+ /// The current [`Status`] of the [`TextEditor`].
+ pub status: Status,
+}
+
+impl<Message> Binding<Message> {
+ /// Returns the default [`Binding`] for the given key press.
+ pub fn from_key_press(event: KeyPress) -> Option<Self> {
+ let KeyPress {
+ key,
+ modifiers,
+ text,
+ status,
+ } = event;
+
+ if status != Status::Focused {
+ return None;
+ }
+
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter),
+ keyboard::Key::Named(key::Named::Backspace) => {
+ Some(Self::Backspace)
+ }
+ keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete),
+ keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus),
+ keyboard::Key::Character("c") if modifiers.command() => {
+ Some(Self::Copy)
+ }
+ keyboard::Key::Character("x") if modifiers.command() => {
+ Some(Self::Cut)
+ }
+ keyboard::Key::Character("v")
+ if modifiers.command() && !modifiers.alt() =>
+ {
+ Some(Self::Paste)
+ }
+ keyboard::Key::Character("a") if modifiers.command() => {
+ Some(Self::SelectAll)
+ }
+ _ => {
+ if let Some(text) = text {
+ let c = text.chars().find(|c| !c.is_control())?;
+
+ Some(Self::Insert(c))
+ } else if let keyboard::Key::Named(named_key) = key.as_ref() {
+ let motion = motion(named_key)?;
+
+ let motion = if modifiers.macos_command() {
+ match motion {
+ Motion::Left => Motion::Home,
+ Motion::Right => Motion::End,
+ _ => motion,
+ }
+ } else {
+ motion
+ };
+
+ let motion = if modifiers.jump() {
+ motion.widen()
+ } else {
+ motion
+ };
+
+ Some(if modifiers.shift() {
+ Self::Select(motion)
+ } else {
+ Self::Move(motion)
+ })
+ } else {
+ None
+ }
+ }
+ }
+ }
+}
+
+enum Update<Message> {
+ Click(mouse::Click),
+ Drag(Point),
+ Release,
+ Scroll(f32),
+ Binding(Binding<Message>),
}
-impl Update {
+impl<Message> Update<Message> {
fn from_event<H: Highlighter>(
event: Event,
state: &State<H>,
bounds: Rectangle,
padding: Padding,
cursor: mouse::Cursor,
+ key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>,
) -> Option<Self> {
- let action = |action| Some(Update::Action(action));
- let edit = |edit| action(Action::Edit(edit));
+ let binding = |binding| Some(Update::Binding(binding));
match event {
Event::Mouse(event) => match event {
@@ -689,12 +1056,13 @@ impl Update {
let click = mouse::Click::new(
cursor_position,
+ mouse::Button::Left,
state.last_click,
);
Some(Update::Click(click))
- } else if state.is_focused {
- Some(Update::Unfocus)
+ } else if state.focus.is_some() {
+ binding(Binding::Unfocus)
} else {
None
}
@@ -707,7 +1075,7 @@ impl Update {
let cursor_position = cursor.position_in(bounds)?
- Vector::new(padding.top, padding.left);
- action(Action::Drag(cursor_position))
+ Some(Update::Drag(cursor_position))
}
_ => None,
},
@@ -727,81 +1095,32 @@ impl Update {
}
_ => None,
},
- Event::Keyboard(event) => match event {
- keyboard::Event::KeyPressed {
+ Event::Keyboard(keyboard::Event::KeyPressed {
+ key,
+ modifiers,
+ text,
+ ..
+ }) => {
+ let status = if state.focus.is_some() {
+ Status::Focused
+ } else {
+ Status::Active
+ };
+
+ let key_press = KeyPress {
key,
modifiers,
text,
- ..
- } if state.is_focused => {
- match key.as_ref() {
- keyboard::Key::Named(key::Named::Enter) => {
- return edit(Edit::Enter);
- }
- keyboard::Key::Named(key::Named::Backspace) => {
- return edit(Edit::Backspace);
- }
- keyboard::Key::Named(key::Named::Delete) => {
- return edit(Edit::Delete);
- }
- keyboard::Key::Named(key::Named::Escape) => {
- return Some(Self::Unfocus);
- }
- keyboard::Key::Character("c")
- if modifiers.command() =>
- {
- return Some(Self::Copy);
- }
- keyboard::Key::Character("x")
- if modifiers.command() =>
- {
- return Some(Self::Cut);
- }
- keyboard::Key::Character("v")
- if modifiers.command() && !modifiers.alt() =>
- {
- return Some(Self::Paste);
- }
- _ => {}
- }
-
- if let Some(text) = text {
- if let Some(c) = text.chars().find(|c| !c.is_control())
- {
- return edit(Edit::Insert(c));
- }
- }
-
- if let keyboard::Key::Named(named_key) = key.as_ref() {
- if let Some(motion) = motion(named_key) {
- let motion = if modifiers.macos_command() {
- match motion {
- Motion::Left => Motion::Home,
- Motion::Right => Motion::End,
- _ => motion,
- }
- } else {
- motion
- };
-
- let motion = if modifiers.jump() {
- motion.widen()
- } else {
- motion
- };
-
- return action(if modifiers.shift() {
- Action::Select(motion)
- } else {
- Action::Move(motion)
- });
- }
- }
+ status,
+ };
- None
+ if let Some(key_binding) = key_binding {
+ key_binding(key_press)
+ } else {
+ Binding::from_key_press(key_press)
}
- _ => None,
- },
+ .map(Self::Binding)
+ }
_ => None,
}
}
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 4e89236b..d5ede524 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -19,7 +19,8 @@ use crate::core::keyboard::key;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
-use crate::core::text::{self, Paragraph as _, Text};
+use crate::core::text::paragraph::{self, Paragraph as _};
+use crate::core::text::{self, Text};
use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
@@ -30,7 +31,8 @@ use crate::core::{
Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point,
Rectangle, Shell, Size, Theme, Vector, Widget,
};
-use crate::runtime::{Action, Task};
+use crate::runtime::task::{self, Task};
+use crate::runtime::Action;
/// A field that can be filled with text.
///
@@ -72,6 +74,7 @@ pub struct TextInput<
padding: Padding,
size: Option<Pixels>,
line_height: text::LineHeight,
+ alignment: alignment::Horizontal,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>,
@@ -101,6 +104,7 @@ where
padding: DEFAULT_PADDING,
size: None,
line_height: text::LineHeight::default(),
+ alignment: alignment::Horizontal::Left,
on_input: None,
on_paste: None,
on_submit: None,
@@ -125,11 +129,23 @@ where
/// the [`TextInput`].
///
/// If this method is not called, the [`TextInput`] will be disabled.
- pub fn on_input<F>(mut self, callback: F) -> Self
- where
- F: 'a + Fn(String) -> Message,
- {
- self.on_input = Some(Box::new(callback));
+ pub fn on_input(
+ mut self,
+ on_input: impl Fn(String) -> Message + 'a,
+ ) -> Self {
+ self.on_input = Some(Box::new(on_input));
+ self
+ }
+
+ /// Sets the message that should be produced when some text is typed into
+ /// the [`TextInput`], if `Some`.
+ ///
+ /// If `None`, the [`TextInput`] will be disabled.
+ pub fn on_input_maybe(
+ mut self,
+ on_input: Option<impl Fn(String) -> Message + 'a>,
+ ) -> Self {
+ self.on_input = on_input.map(|f| Box::new(f) as _);
self
}
@@ -140,6 +156,13 @@ where
self
}
+ /// Sets the message that should be produced when the [`TextInput`] is
+ /// focused and the enter key is pressed, if `Some`.
+ pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self {
+ self.on_submit = on_submit;
+ self
+ }
+
/// Sets the message that should be produced when some text is pasted into
/// the [`TextInput`].
pub fn on_paste(
@@ -150,6 +173,16 @@ where
self
}
+ /// Sets the message that should be produced when some text is pasted into
+ /// the [`TextInput`], if `Some`.
+ pub fn on_paste_maybe(
+ mut self,
+ on_paste: Option<impl Fn(String) -> Message + 'a>,
+ ) -> Self {
+ self.on_paste = on_paste.map(|f| Box::new(f) as _);
+ self
+ }
+
/// Sets the [`Font`] of the [`TextInput`].
///
/// [`Font`]: text::Renderer::Font
@@ -191,6 +224,15 @@ where
self
}
+ /// Sets the horizontal alignment of the [`TextInput`].
+ pub fn align_x(
+ mut self,
+ alignment: impl Into<alignment::Horizontal>,
+ ) -> Self {
+ self.alignment = alignment.into();
+ self
+ }
+
/// Sets the style of the [`TextInput`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
@@ -238,6 +280,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
};
state.placeholder.update(placeholder_text);
@@ -262,6 +305,7 @@ where
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
};
state.icon.update(icon_text);
@@ -359,7 +403,7 @@ where
let icon_layout = children_layout.next().unwrap();
renderer.fill_paragraph(
- &state.icon,
+ state.icon.raw(),
icon_layout.bounds().center(),
style.icon,
*viewport,
@@ -377,16 +421,16 @@ where
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
position,
);
- let is_cursor_visible = ((focus.now - focus.updated_at)
- .as_millis()
- / CURSOR_BLINK_INTERVAL_MILLIS)
- % 2
- == 0;
+ let is_cursor_visible = !is_disabled
+ && ((focus.now - focus.updated_at).as_millis()
+ / CURSOR_BLINK_INTERVAL_MILLIS)
+ % 2
+ == 0;
let cursor = if is_cursor_visible {
Some((
@@ -414,14 +458,14 @@ where
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
left,
);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
right,
);
@@ -455,9 +499,21 @@ where
};
let draw = |renderer: &mut Renderer, viewport| {
+ let paragraph = if text.is_empty() {
+ state.placeholder.raw()
+ } else {
+ state.value.raw()
+ };
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ paragraph.min_width(),
+ self.alignment,
+ );
+
if let Some((cursor, color)) = cursor {
renderer.with_translation(
- Vector::new(-offset, 0.0),
+ Vector::new(alignment_offset - offset, 0.0),
|renderer| {
renderer.fill_quad(cursor, color);
},
@@ -467,13 +523,9 @@ where
}
renderer.fill_paragraph(
- if text.is_empty() {
- &state.placeholder
- } else {
- &state.value
- },
+ paragraph,
Point::new(text_bounds.x, text_bounds.center_y())
- - Vector::new(offset, 0.0),
+ + Vector::new(alignment_offset - offset, 0.0),
if text.is_empty() {
style.placeholder
} else {
@@ -510,12 +562,9 @@ where
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
- // Unfocus text input if it becomes disabled
+ // Stop pasting if input becomes disabled
if self.on_input.is_none() {
- state.last_click = None;
- state.is_focused = None;
state.is_pasting = None;
- state.is_dragging = false;
}
}
@@ -540,7 +589,7 @@ where
tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
@@ -576,11 +625,7 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state::<Renderer>(tree);
- let click_position = if self.on_input.is_some() {
- cursor.position_over(layout.bounds())
- } else {
- None
- };
+ let click_position = cursor.position_over(layout.bounds());
state.is_focused = if click_position.is_some() {
state.is_focused.or_else(|| {
@@ -598,10 +643,24 @@ where
if let Some(cursor_position) = click_position {
let text_layout = layout.children().next().unwrap();
- let target = cursor_position.x - text_layout.bounds().x;
- let click =
- mouse::Click::new(cursor_position, state.last_click);
+ let target = {
+ let text_bounds = text_layout.bounds();
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ state.value.raw().min_width(),
+ self.alignment,
+ );
+
+ cursor_position.x - text_bounds.x - alignment_offset
+ };
+
+ let click = mouse::Click::new(
+ cursor_position,
+ mouse::Button::Left,
+ state.last_click,
+ );
match click.kind() {
click::Kind::Single => {
@@ -675,7 +734,18 @@ where
if state.is_dragging {
let text_layout = layout.children().next().unwrap();
- let target = position.x - text_layout.bounds().x;
+
+ let target = {
+ let text_bounds = text_layout.bounds();
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ state.value.raw().min_width(),
+ self.alignment,
+ );
+
+ position.x - text_bounds.x - alignment_offset
+ };
let value = if self.is_secure {
self.value.secure()
@@ -704,10 +774,6 @@ where
let state = state::<Renderer>(tree);
if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = &self.on_input else {
- return event::Status::Ignored;
- };
-
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
@@ -731,6 +797,10 @@ where
if state.keyboard_modifiers.command()
&& !self.is_secure =>
{
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if let Some((start, end)) =
state.cursor.selection(&self.value)
{
@@ -755,6 +825,10 @@ where
if state.keyboard_modifiers.command()
&& !state.keyboard_modifiers.alt() =>
{
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
let content = match state.is_pasting.take() {
Some(content) => content,
None => {
@@ -798,6 +872,10 @@ where
}
if let Some(text) = text {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
state.is_pasting = None;
if let Some(c) =
@@ -826,6 +904,10 @@ where
}
}
keyboard::Key::Named(key::Named::Backspace) => {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if modifiers.jump()
&& state.cursor.selection(&self.value).is_none()
{
@@ -850,6 +932,10 @@ where
update_cache(state, &self.value);
}
keyboard::Key::Named(key::Named::Delete) => {
+ let Some(on_input) = &self.on_input else {
+ return event::Status::Ignored;
+ };
+
if modifiers.jump()
&& state.cursor.selection(&self.value).is_none()
{
@@ -1068,7 +1154,7 @@ where
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
if self.on_input.is_none() {
- mouse::Interaction::NotAllowed
+ mouse::Interaction::Idle
} else {
mouse::Interaction::Text
}
@@ -1142,13 +1228,13 @@ impl From<Id> for widget::Id {
/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`].
pub fn focus<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::focusable::focus(id.0)))
+ task::effect(Action::widget(operation::focusable::focus(id.0)))
}
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// end.
pub fn move_cursor_to_end<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::move_cursor_to_end(
+ task::effect(Action::widget(operation::text_input::move_cursor_to_end(
id.0,
)))
}
@@ -1156,7 +1242,7 @@ pub fn move_cursor_to_end<T>(id: Id) -> Task<T> {
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// front.
pub fn move_cursor_to_front<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::move_cursor_to_front(
+ task::effect(Action::widget(operation::text_input::move_cursor_to_front(
id.0,
)))
}
@@ -1164,22 +1250,22 @@ pub fn move_cursor_to_front<T>(id: Id) -> Task<T> {
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// provided position.
pub fn move_cursor_to<T>(id: Id, position: usize) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::move_cursor_to(
+ task::effect(Action::widget(operation::text_input::move_cursor_to(
id.0, position,
)))
}
/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`].
pub fn select_all<T>(id: Id) -> Task<T> {
- Task::effect(Action::widget(operation::text_input::select_all(id.0)))
+ task::effect(Action::widget(operation::text_input::select_all(id.0)))
}
/// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)]
pub struct State<P: text::Paragraph> {
- value: P,
- placeholder: P,
- icon: P,
+ value: paragraph::Plain<P>,
+ placeholder: paragraph::Plain<P>,
+ icon: paragraph::Plain<P>,
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
@@ -1208,21 +1294,6 @@ impl<P: text::Paragraph> State<P> {
Self::default()
}
- /// Creates a new [`State`], representing a focused [`TextInput`].
- pub fn focused() -> Self {
- Self {
- value: P::default(),
- placeholder: P::default(),
- icon: P::default(),
- is_focused: None,
- is_dragging: false,
- is_pasting: None,
- last_click: None,
- cursor: Cursor::default(),
- keyboard_modifiers: keyboard::Modifiers::default(),
- }
- }
-
/// Returns whether the [`TextInput`] is currently focused or not.
pub fn is_focused(&self) -> bool {
self.is_focused.is_some()
@@ -1318,7 +1389,7 @@ fn offset<P: text::Paragraph>(
};
let (_, offset) = measure_cursor_and_scroll_offset(
- &state.value,
+ state.value.raw(),
text_bounds,
focus_position,
);
@@ -1356,6 +1427,7 @@ fn find_cursor_position<P: text::Paragraph>(
let char_offset = state
.value
+ .raw()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
@@ -1385,7 +1457,7 @@ fn replace_paragraph<Renderer>(
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
- state.value = Renderer::Paragraph::with_text(Text {
+ state.value = paragraph::Plain::new(Text {
font,
line_height,
content: &value.to_string(),
@@ -1394,6 +1466,7 @@ fn replace_paragraph<Renderer>(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
+ wrapping: text::Wrapping::default(),
});
}
@@ -1498,3 +1571,21 @@ pub fn default(theme: &Theme, status: Status) -> Style {
},
}
}
+
+fn alignment_offset(
+ text_bounds_width: f32,
+ text_min_width: f32,
+ alignment: alignment::Horizontal,
+) -> f32 {
+ if text_min_width > text_bounds_width {
+ 0.0
+ } else {
+ match alignment {
+ alignment::Horizontal::Left => 0.0,
+ alignment::Horizontal::Center => {
+ (text_bounds_width - text_min_width) / 2.0
+ }
+ alignment::Horizontal::Right => text_bounds_width - text_min_width,
+ }
+ }
+}
diff --git a/widget/src/themer.rs b/widget/src/themer.rs
index 9eb47d84..499a9fe8 100644
--- a/widget/src/themer.rs
+++ b/widget/src/themer.rs
@@ -104,7 +104,7 @@ where
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
self.content
.as_widget()
@@ -236,7 +236,7 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn Operation<()>,
+ operation: &mut dyn Operation,
) {
self.content.operate(layout, renderer, operation);
}
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index ca6e37b0..1c425dc1 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -26,7 +26,9 @@ use crate::core::{
///
/// let is_toggled = true;
///
-/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
+/// Toggler::new(is_toggled)
+/// .label("Toggle me!")
+/// .on_toggle(Message::TogglerToggled);
/// ```
#[allow(missing_debug_implementations)]
pub struct Toggler<
@@ -39,14 +41,15 @@ pub struct Toggler<
Renderer: text::Renderer,
{
is_toggled: bool,
- on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
- label: Option<String>,
+ on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
+ label: Option<text::Fragment<'a>>,
width: Length,
size: f32,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal,
text_shaping: text::Shaping,
+ text_wrapping: text::Wrapping,
spacing: f32,
font: Option<Renderer::Font>,
class: Theme::Class<'a>,
@@ -68,30 +71,54 @@ where
/// * a function that will be called when the [`Toggler`] is toggled. It
/// will receive the new state of the [`Toggler`] and must produce a
/// `Message`.
- pub fn new<F>(
- label: impl Into<Option<String>>,
- is_toggled: bool,
- f: F,
- ) -> Self
- where
- F: 'a + Fn(bool) -> Message,
- {
+ pub fn new(is_toggled: bool) -> Self {
Toggler {
is_toggled,
- on_toggle: Box::new(f),
- label: label.into(),
- width: Length::Fill,
+ on_toggle: None,
+ label: None,
+ width: Length::Shrink,
size: Self::DEFAULT_SIZE,
text_size: None,
text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left,
- text_shaping: text::Shaping::Basic,
+ text_shaping: text::Shaping::default(),
+ text_wrapping: text::Wrapping::default(),
spacing: Self::DEFAULT_SIZE / 2.0,
font: None,
class: Theme::default(),
}
}
+ /// Sets the label of the [`Toggler`].
+ pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
+ self.label = Some(label.into_fragment());
+ self
+ }
+
+ /// Sets the message that should be produced when a user toggles
+ /// the [`Toggler`].
+ ///
+ /// If this method is not called, the [`Toggler`] will be disabled.
+ pub fn on_toggle(
+ mut self,
+ on_toggle: impl Fn(bool) -> Message + 'a,
+ ) -> Self {
+ self.on_toggle = Some(Box::new(on_toggle));
+ self
+ }
+
+ /// Sets the message that should be produced when a user toggles
+ /// the [`Toggler`], if `Some`.
+ ///
+ /// If `None`, the [`Toggler`] will be disabled.
+ pub fn on_toggle_maybe(
+ mut self,
+ on_toggle: Option<impl Fn(bool) -> Message + 'a>,
+ ) -> Self {
+ self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
+ self
+ }
+
/// Sets the size of the [`Toggler`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = size.into().0;
@@ -131,6 +158,12 @@ where
self
}
+ /// Sets the [`text::Wrapping`] strategy of the [`Toggler`].
+ pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
+ self.text_wrapping = wrapping;
+ self
+ }
+
/// Sets the spacing between the [`Toggler`] and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
@@ -216,6 +249,7 @@ where
self.text_alignment,
alignment::Vertical::Top,
self.text_shaping,
+ self.text_wrapping,
)
} else {
layout::Node::new(Size::ZERO)
@@ -235,13 +269,17 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
+ let Some(on_toggle) = &self.on_toggle else {
+ return event::Status::Ignored;
+ };
+
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = cursor.is_over(layout.bounds());
if mouse_over {
- shell.publish((self.on_toggle)(!self.is_toggled));
+ shell.publish(on_toggle(!self.is_toggled));
event::Status::Captured
} else {
@@ -261,7 +299,11 @@ where
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
- mouse::Interaction::Pointer
+ if self.on_toggle.is_some() {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::NotAllowed
+ }
} else {
mouse::Interaction::default()
}
@@ -289,12 +331,14 @@ where
if self.label.is_some() {
let label_layout = children.next().unwrap();
+ let state: &widget::text::State<Renderer::Paragraph> =
+ tree.state.downcast_ref();
crate::text::draw(
renderer,
style,
label_layout,
- tree.state.downcast_ref(),
+ state.0.raw(),
crate::text::Style::default(),
viewport,
);
@@ -303,7 +347,9 @@ where
let bounds = toggler_layout.bounds();
let is_mouse_over = cursor.is_over(layout.bounds());
- let status = if is_mouse_over {
+ let status = if self.on_toggle.is_none() {
+ Status::Disabled
+ } else if is_mouse_over {
Status::Hovered {
is_toggled: self.is_toggled,
}
@@ -392,6 +438,8 @@ pub enum Status {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
+ /// The [`Toggler`] is disabled.
+ Disabled,
}
/// The appearance of a toggler.
@@ -452,6 +500,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.strong.color
}
}
+ Status::Disabled => palette.background.weak.color,
};
let foreground = match status {
@@ -472,6 +521,7 @@ pub fn default(theme: &Theme, status: Status) -> Style {
palette.background.weak.color
}
}
+ Status::Disabled => palette.background.base.color,
};
Style {
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index defb442f..03ec374c 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -5,6 +5,7 @@ pub use crate::slider::{
default, Catalog, Handle, HandleShape, Status, Style, StyleFn,
};
+use crate::core::border::Border;
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key};
@@ -14,8 +15,8 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell,
- Size, Widget,
+ self, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size,
+ Widget,
};
/// An vertical bar and a handle that selects a single value from a range of
@@ -71,8 +72,8 @@ where
/// * an inclusive range of possible values
/// * the current value of the [`VerticalSlider`]
/// * a function that will be called when the [`VerticalSlider`] is dragged.
- /// It receives the new value of the [`VerticalSlider`] and must produce a
- /// `Message`.
+ /// It receives the new value of the [`VerticalSlider`] and must produce a
+ /// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
F: 'a + Fn(T) -> Message,
@@ -239,7 +240,7 @@ where
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
- T::from_f64(value)
+ T::from_f64(value.min(end))
};
new_value
@@ -412,10 +413,10 @@ where
width: style.rail.width,
height: offset + handle_width / 2.0,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.1,
+ style.rail.backgrounds.1,
);
renderer.fill_quad(
@@ -426,10 +427,10 @@ where
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
- border: Border::rounded(style.rail.border_radius),
+ border: style.rail.border,
..renderer::Quad::default()
},
- style.rail.colors.0,
+ style.rail.backgrounds.0,
);
renderer.fill_quad(
@@ -447,7 +448,7 @@ where
},
..renderer::Quad::default()
},
- style.handle.color,
+ style.handle.background,
);
}