diff options
Diffstat (limited to 'widget/src')
-rw-r--r-- | widget/src/checkbox.rs | 12 | ||||
-rw-r--r-- | widget/src/combo_box.rs | 27 | ||||
-rw-r--r-- | widget/src/container.rs | 13 | ||||
-rw-r--r-- | widget/src/helpers.rs | 9 | ||||
-rw-r--r-- | widget/src/lazy.rs | 2 | ||||
-rw-r--r-- | widget/src/lazy/component.rs | 7 | ||||
-rw-r--r-- | widget/src/lazy/helpers.rs | 12 | ||||
-rw-r--r-- | widget/src/lazy/responsive.rs | 1 | ||||
-rw-r--r-- | widget/src/lib.rs | 3 | ||||
-rw-r--r-- | widget/src/markdown.rs | 275 | ||||
-rw-r--r-- | widget/src/mouse_area.rs | 49 | ||||
-rw-r--r-- | widget/src/overlay/menu.rs | 1 | ||||
-rw-r--r-- | widget/src/pane_grid.rs | 2 | ||||
-rw-r--r-- | widget/src/pane_grid/controls.rs | 59 | ||||
-rw-r--r-- | widget/src/pane_grid/title_bar.rs | 317 | ||||
-rw-r--r-- | widget/src/pick_list.rs | 5 | ||||
-rw-r--r-- | widget/src/progress_bar.rs | 8 | ||||
-rw-r--r-- | widget/src/radio.rs | 13 | ||||
-rw-r--r-- | widget/src/scrollable.rs | 211 | ||||
-rw-r--r-- | widget/src/slider.rs | 36 | ||||
-rw-r--r-- | widget/src/text/rich.rs | 49 | ||||
-rw-r--r-- | widget/src/text_editor.rs | 26 | ||||
-rw-r--r-- | widget/src/text_input.rs | 175 | ||||
-rw-r--r-- | widget/src/toggler.rs | 82 | ||||
-rw-r--r-- | widget/src/vertical_slider.rs | 12 |
25 files changed, 1080 insertions, 326 deletions
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index e5abfbb4..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, diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 62785b2c..a51701ca 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -41,6 +41,7 @@ pub struct ComboBox< selection: text_input::Value, on_selected: Box<dyn Fn(T) -> Message>, on_option_hovered: Option<Box<dyn Fn(T) -> Message>>, + on_open: Option<Message>, on_close: Option<Message>, on_input: Option<Box<dyn Fn(String) -> Message>>, menu_class: <Theme as menu::Catalog>::Class<'a>, @@ -77,6 +78,7 @@ where on_selected: Box::new(on_selected), on_option_hovered: None, on_input: None, + on_open: None, on_close: None, menu_class: <Theme as Catalog>::default_menu(), padding: text_input::DEFAULT_PADDING, @@ -104,6 +106,13 @@ where self } + /// Sets the message that will be produced when the [`ComboBox`] is + /// opened. + pub fn on_open(mut self, message: Message) -> Self { + self.on_open = Some(message); + self + } + /// Sets the message that will be produced when the outside area /// of the [`ComboBox`] is pressed. pub fn on_close(mut self, message: Message) -> Self { @@ -632,15 +641,19 @@ where text_input_state.is_focused() }; - if started_focused && !is_focused && !published_message_to_shell { - if let Some(message) = self.on_close.take() { - shell.publish(message); - } - } - - // Focus changed, invalidate widget tree to force a fresh `view` if started_focused != is_focused { + // Focus changed, invalidate widget tree to force a fresh `view` shell.invalidate_widgets(); + + if !published_message_to_shell { + if is_focused { + if let Some(on_open) = self.on_open.take() { + shell.publish(on_open); + } + } else if let Some(on_close) = self.on_close.take() { + shell.publish(on_close); + } + } } event_status diff --git a/widget/src/container.rs b/widget/src/container.rs index 54043ad0..3b794099 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -184,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(); @@ -460,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() { @@ -613,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>; @@ -630,6 +636,11 @@ 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(); diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index c3ffea45..51978823 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -25,7 +25,7 @@ use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; use crate::{Column, MouseArea, Row, Space, Stack, Themer}; -use std::borrow::{Borrow, Cow}; +use std::borrow::Borrow; use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. @@ -707,12 +707,13 @@ where /// /// [`Rich`]: text::Rich pub fn rich_text<'a, Link, Theme, Renderer>( - spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>, + 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) } @@ -766,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`]. diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 4bcf8628..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, diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 1bf04195..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; 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 2e24f2b3..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, diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 115a29e5..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)] diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 23e36435..fa4ee6bf 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -7,10 +7,16 @@ use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; -use crate::core::theme::{self, Theme}; -use crate::core::{self, color, Color, Element, Length, Pixels}; +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; @@ -18,13 +24,13 @@ pub use url::Url; #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(pulldown_cmark::HeadingLevel, Vec<text::Span<'static, Url>>), + Heading(pulldown_cmark::HeadingLevel, Text), /// A paragraph. - Paragraph(Vec<text::Span<'static, Url>>), + Paragraph(Text), /// A code block. /// /// You can enable the `highlighter` feature for syntax highligting. - CodeBlock(Vec<text::Span<'static, Url>>), + CodeBlock(Text), /// A list. List { /// The first number of the list, if it is ordered. @@ -34,11 +40,112 @@ pub enum 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, - palette: theme::Palette, -) -> impl Iterator<Item = Item> + '_ { +pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ { struct List { start: Option<u64>, items: Vec<Vec<Item>>, @@ -158,7 +265,7 @@ pub fn parse( pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { produce( &mut lists, - Item::Heading(level, spans.drain(..).collect()), + Item::Heading(level, Text::new(spans.drain(..).collect())), ) } pulldown_cmark::TagEnd::Strong if !metadata && !table => { @@ -178,7 +285,10 @@ pub fn parse( None } pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { - produce(&mut lists, Item::Paragraph(spans.drain(..).collect())) + produce( + &mut lists, + Item::Paragraph(Text::new(spans.drain(..).collect())), + ) } pulldown_cmark::TagEnd::Item if !metadata && !table => { if spans.is_empty() { @@ -186,7 +296,7 @@ pub fn parse( } else { produce( &mut lists, - Item::Paragraph(spans.drain(..).collect()), + Item::Paragraph(Text::new(spans.drain(..).collect())), ) } } @@ -207,7 +317,10 @@ pub fn parse( highlighter = None; } - produce(&mut lists, Item::CodeBlock(spans.drain(..).collect())) + produce( + &mut lists, + Item::CodeBlock(Text::new(spans.drain(..).collect())), + ) } pulldown_cmark::TagEnd::MetadataBlock(_) => { metadata = false; @@ -227,9 +340,11 @@ pub fn parse( for (range, highlight) in highlighter.highlight_line(text.as_ref()) { - let span = span(text[range].to_owned()) - .color_maybe(highlight.color()) - .font_maybe(highlight.font()); + let span = Span::Highlight { + text: text[range].to_owned(), + color: highlight.color(), + font: highlight.font(), + }; spans.push(span); } @@ -237,30 +352,13 @@ pub fn parse( return None; } - let span = span(text.into_string()).strikethrough(strikethrough); - - let span = 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(palette.primary).link(link.clone()) - } else { - span + let span = Span::Standard { + text: text.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: false, }; spans.push(span); @@ -268,29 +366,38 @@ pub fn parse( None } pulldown_cmark::Event::Code(code) if !metadata && !table => { - let span = span(code.into_string()) - .font(Font::MONOSPACE) - .color(Color::WHITE) - .background(color!(0x111111)) - .border(border::rounded(2)) - .padding(padding::left(2).right(2)) - .strikethrough(strikethrough); - - let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) - } else { - span + 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(" ").strikethrough(strikethrough)); + 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("\n")); + spans.push(Span::Standard { + text: String::from("\n"), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); None } _ => None, @@ -346,14 +453,44 @@ impl Default for Settings { } } +/// 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, Renderer>( +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 { @@ -371,7 +508,7 @@ where let blocks = items.into_iter().enumerate().map(|(i, item)| match item { Item::Heading(level, heading) => { - container(rich_text(heading).size(match level { + 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, @@ -387,11 +524,11 @@ where .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).size(text_size).into() + 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)] + row![text("•").size(text_size), view(items, settings, style)] .spacing(spacing) .into() })) @@ -404,7 +541,7 @@ where } => column(items.iter().enumerate().map(|(i, items)| { row![ text!("{}.", i as u64 + *start).size(text_size), - view(items, settings) + view(items, settings, style) ] .spacing(spacing) .into() @@ -414,7 +551,9 @@ where Item::CodeBlock(code) => container( scrollable( container( - rich_text(code).font(Font::MONOSPACE).size(code_size), + rich_text(code.spans(style)) + .font(Font::MONOSPACE) + .size(code_size), ) .padding(spacing.0 / 2.0), ) @@ -426,9 +565,23 @@ where ) .width(Length::Fill) .padding(spacing.0 / 2.0) - .style(container::dark) + .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 366335f4..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, @@ -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 73d1cc8c..f05ae40a 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -532,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 0aab1ab5..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; 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 791fab4a..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), @@ -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 f7f7b65b..1fc9951e 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -81,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(), @@ -250,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()) @@ -515,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, @@ -544,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 { diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 88d1850a..a10feea6 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -5,7 +5,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - self, Background, Element, Layout, Length, Rectangle, Size, Theme, Widget, + self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme, + Widget, }; use std::ops::RangeInclusive; @@ -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, diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 1b02f8ca..cfa961f3 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -82,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>, } @@ -122,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(), } @@ -170,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()); @@ -245,6 +253,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cf504eda..af6a3945 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -7,10 +7,12 @@ 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, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, @@ -242,6 +244,24 @@ impl Direction { 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 { @@ -429,6 +449,7 @@ where state, self.id.as_ref().map(|id| &id.0), bounds, + content_bounds, translation, ); @@ -470,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 { .. }) @@ -488,7 +527,7 @@ where content_bounds, ); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -526,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, @@ -559,7 +598,7 @@ where content_bounds, ); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -597,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, @@ -612,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) => @@ -660,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; } @@ -683,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, @@ -709,7 +770,7 @@ where event::Status::Captured } else { event::Status::Ignored - }; + } } Event::Touch(event) if state.scroll_area_touched_at.is_some() @@ -733,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, ); @@ -748,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, @@ -760,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( @@ -1075,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))) } /// 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( 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, @@ -1102,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, @@ -1121,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) @@ -1130,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 @@ -1147,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 { @@ -1159,6 +1257,7 @@ impl Default for State { x_scroller_grabbed_at: None, keyboard_modifiers: keyboard::Modifiers::default(), last_notified: None, + last_scrolled: None, } } } @@ -1171,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)] @@ -1274,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: Anchor, delta: f32| match alignment { - Anchor::Start => delta, - Anchor::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), ); } @@ -1309,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), ); } @@ -1355,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) { diff --git a/widget/src/slider.rs b/widget/src/slider.rs index e586684a..aebf68e2 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -9,8 +9,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Size, Theme, Widget, + self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Theme, Widget, }; use std::ops::RangeInclusive; @@ -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/text/rich.rs b/widget/src/text/rich.rs index c6aa1e14..921c55a5 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -5,7 +5,7 @@ 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, + self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping, }; use crate::core::widget::tree::{self, Tree}; use crate::core::{ @@ -13,8 +13,6 @@ use crate::core::{ Rectangle, Shell, Size, Vector, Widget, }; -use std::borrow::Cow; - /// A bunch of [`Rich`] text. #[allow(missing_debug_implementations)] pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer> @@ -23,7 +21,7 @@ where Theme: Catalog, Renderer: core::text::Renderer, { - spans: Cow<'a, [Span<'a, Link, Renderer::Font>]>, + spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -31,6 +29,7 @@ where font: Option<Renderer::Font>, align_x: alignment::Horizontal, align_y: alignment::Vertical, + wrapping: Wrapping, class: Theme::Class<'a>, } @@ -39,11 +38,12 @@ 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: Cow::default(), + spans: Box::new([]), size: None, line_height: LineHeight::default(), width: Length::Shrink, @@ -51,16 +51,17 @@ where 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 Into<Cow<'a, [Span<'a, Link, Renderer::Font>]>>, + spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a, ) -> Self { Self { - spans: spans.into(), + spans: Box::new(spans), ..Self::new() } } @@ -119,6 +120,12 @@ where 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 @@ -154,15 +161,6 @@ where self.class = class.into(); self } - - /// Adds a new text [`Span`] to the [`Rich`] text. - pub fn push( - mut self, - span: impl Into<Span<'a, Link, Renderer::Font>>, - ) -> Self { - self.spans.to_mut().push(span.into()); - self - } } impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer> @@ -170,6 +168,7 @@ where Link: Clone + 'a, Theme: Catalog, Renderer: core::text::Renderer, + Renderer::Font: 'a, { fn default() -> Self { Self::new() @@ -221,12 +220,13 @@ where limits, self.width, self.height, - self.spans.as_ref(), + self.spans.as_ref().as_ref(), self.line_height, self.size, self.font, self.align_x, self.align_y, + self.wrapping, ) } @@ -250,7 +250,7 @@ where .position_in(layout.bounds()) .and_then(|position| state.paragraph.hit_span(position)); - for (index, span) in self.spans.iter().enumerate() { + for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() { let is_hovered_link = span.link.is_some() && Some(index) == hovered_span; @@ -394,6 +394,8 @@ where Some(span) if span == span_pressed => { if let Some(link) = self .spans + .as_ref() + .as_ref() .get(span) .and_then(|span| span.link.clone()) { @@ -427,7 +429,7 @@ where if let Some(span) = state .paragraph .hit_span(position) - .and_then(|span| self.spans.get(span)) + .and_then(|span| self.spans.as_ref().as_ref().get(span)) { if span.link.is_some() { return mouse::Interaction::Pointer; @@ -451,6 +453,7 @@ fn layout<Link, Renderer>( font: Option<Renderer::Font>, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, + wrapping: Wrapping, ) -> layout::Node where Link: Clone, @@ -471,6 +474,7 @@ where horizontal_alignment, vertical_alignment, shaping: Shaping::Advanced, + wrapping, }; if state.spans != spans { @@ -487,6 +491,7 @@ where horizontal_alignment, vertical_alignment, shaping: Shaping::Advanced, + wrapping, }) { core::text::Difference::None => {} core::text::Difference::Bounds => { @@ -509,14 +514,12 @@ 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 { - spans: spans.into_iter().collect(), - ..Self::new() - } + Self::with_spans(spans.into_iter().collect::<Vec<_>>()) } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 745e3ae8..1df97962 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -9,7 +9,7 @@ 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, Text}; +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}; @@ -47,6 +47,7 @@ pub struct TextEditor< 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>>, @@ -74,6 +75,7 @@ where width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), + wrapping: Wrapping::default(), class: Theme::default(), key_binding: None, on_edit: None, @@ -107,6 +109,12 @@ where self } + /// Sets the width of the [`TextEditor`]. + pub fn width(mut self, width: impl Into<Pixels>) -> Self { + self.width = Length::from(width.into()); + self + } + /// Sets the message that should be produced when some action is performed in /// the [`TextEditor`]. /// @@ -148,6 +156,12 @@ 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( @@ -186,6 +200,7 @@ where 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, @@ -489,13 +504,14 @@ where state.highlighter_settings = self.highlighter_settings.clone(); } - let limits = limits.height(self.height); + let limits = limits.width(self.width).height(self.height); internal.editor.update( limits.shrink(self.padding).max(), 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(), ); @@ -784,6 +800,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrapping: self.wrapping, }, text_bounds.position(), style.placeholder, @@ -964,7 +981,9 @@ impl<Message> Binding<Message> { keyboard::Key::Named(key::Named::Backspace) => { Some(Self::Backspace) } - keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete), + keyboard::Key::Named(key::Named::Delete) if text.is_none() => { + Some(Self::Delete) + } keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), keyboard::Key::Character("c") if modifiers.command() => { Some(Self::Copy) @@ -1045,6 +1064,7 @@ impl<Message> Update<Message> { let click = mouse::Click::new( cursor_position, + mouse::Button::Left, state.last_click, ); diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 173de136..d5ede524 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -19,7 +19,7 @@ use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::paragraph; +use crate::core::text::paragraph::{self, Paragraph as _}; use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; @@ -74,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>, @@ -103,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, @@ -127,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 } @@ -142,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( @@ -152,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 @@ -193,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 @@ -240,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); @@ -264,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); @@ -384,11 +426,11 @@ where 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(( @@ -457,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); }, @@ -469,13 +523,9 @@ where } renderer.fill_paragraph( - if text.is_empty() { - state.placeholder.raw() - } else { - state.value.raw() - }, + 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 { @@ -512,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; } } @@ -578,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(|| { @@ -600,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 => { @@ -677,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() @@ -706,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(); @@ -733,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) { @@ -757,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 => { @@ -800,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) = @@ -828,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() { @@ -852,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() { @@ -1070,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 } @@ -1382,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(), }); } @@ -1486,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/toggler.rs b/widget/src/toggler.rs index 821e2526..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(), + 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() } @@ -305,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, } @@ -394,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. @@ -454,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 { @@ -474,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 f21b996c..03ec374c 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -5,7 +5,7 @@ pub use crate::slider::{ default, Catalog, Handle, HandleShape, Status, Style, StyleFn, }; -use crate::core::border::{self, Border}; +use crate::core::border::Border; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -413,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( @@ -427,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( @@ -448,7 +448,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } |