diff options
48 files changed, 1096 insertions, 466 deletions
@@ -27,7 +27,9 @@ wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enable the `tiny-skia` software renderer backend tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `Image` widget -image = ["iced_widget/image", "dep:image"] +image = ["image-without-codecs", "image/default"] +# Enables the `Image` widget, without any built-in codecs of the `image` crate +image-without-codecs = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget svg = ["iced_widget/svg"] # Enables the `Canvas` widget @@ -147,7 +149,7 @@ glam = "0.25" glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" } guillotiere = "0.6" half = "2.2" -image = "0.24" +image = { version = "0.24", default-features = false } kamadak-exif = "0.5" kurbo = "0.10" log = "0.4" diff --git a/core/src/padding.rs b/core/src/padding.rs index fdaa0236..e26cdd9b 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -32,7 +32,7 @@ use crate::{Pixels, Size}; /// let widget = Widget::new().padding(20); // 20px on all sides /// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right /// ``` -#[derive(Debug, Copy, Clone, Default)] +#[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct Padding { /// Top padding pub top: f32, diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e3a07280..bbcdd8ff 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -161,6 +161,7 @@ impl text::Editor for () { _new_font: Self::Font, _new_size: Pixels, _new_line_height: text::LineHeight, + _new_wrapping: text::Wrapping, _new_highlighter: &mut impl text::Highlighter, ) { } diff --git a/core/src/text.rs b/core/src/text.rs index 436fee9a..d7b7fee4 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -41,6 +41,9 @@ pub struct Text<Content = String, Font = crate::Font> { /// The [`Shaping`] strategy of the [`Text`]. pub shaping: Shaping, + + /// The [`Wrapping`] strategy of the [`Text`]. + pub wrapping: Wrapping, } /// The shaping strategy of some text. @@ -67,6 +70,22 @@ pub enum Shaping { Advanced, } +/// The wrapping strategy of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Wrapping { + /// No wrapping. + None, + /// Wraps at the word level. + /// + /// This is the default. + #[default] + Word, + /// Wraps at the glyph level. + Glyph, + /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself. + WordOrGlyph, +} + /// The height of a line of text in a paragraph. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LineHeight { @@ -252,7 +271,7 @@ pub struct Span<'a, Link = (), Font = crate::Font> { } /// A text highlight. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Highlight { /// The [`Background`] of the highlight. pub background: Background, diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 135707d1..cd30db3a 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,6 +1,6 @@ //! Edit text. use crate::text::highlighter::{self, Highlighter}; -use crate::text::LineHeight; +use crate::text::{LineHeight, Wrapping}; use crate::{Pixels, Point, Rectangle, Size}; use std::sync::Arc; @@ -50,6 +50,7 @@ pub trait Editor: Sized + Default { new_font: Self::Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ); diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 04a97f35..924276c3 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -95,6 +95,7 @@ impl<P: Paragraph> Plain<P> { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, }) { Difference::None => {} Difference::Bounds => { diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 741e1a5f..4ee4b4a7 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -295,7 +295,7 @@ where /// Chains the output of an [`Operation`] with the provided function to /// build a new [`Operation`]. -pub fn chain<A, B, O>( +pub fn then<A, B, O>( operation: impl Operation<A> + 'static, f: fn(A) -> O, ) -> impl Operation<B> @@ -361,7 +361,7 @@ where Outcome::Chain(Box::new((self.next)(value))) } Outcome::Chain(operation) => { - Outcome::Chain(Box::new(chain(operation, self.next))) + Outcome::Chain(Box::new(then(operation, self.next))) } } } diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 0a6f2e96..867c682e 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -94,7 +94,10 @@ pub fn count() -> impl Operation<Count> { /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the previous focusable widget. /// - if not found, focuses the last focusable widget. -pub fn focus_previous() -> impl Operation { +pub fn focus_previous<T>() -> impl Operation<T> +where + T: Send + 'static, +{ struct FocusPrevious { count: Count, current: usize, @@ -128,13 +131,16 @@ pub fn focus_previous() -> impl Operation { } } - operation::chain(count(), |count| FocusPrevious { count, current: 0 }) + operation::then(count(), |count| FocusPrevious { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the next focusable widget. /// - if not found, focuses the first focusable widget. -pub fn focus_next() -> impl Operation { +pub fn focus_next<T>() -> impl Operation<T> +where + T: Send + 'static, +{ struct FocusNext { count: Count, current: usize, @@ -162,7 +168,7 @@ pub fn focus_next() -> impl Operation { } } - operation::chain(count(), |count| FocusNext { count, current: 0 }) + operation::then(count(), |count| FocusNext { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 5c5b78dd..d8d6e4c6 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -11,7 +11,7 @@ use crate::{ Widget, }; -pub use text::{LineHeight, Shaping}; +pub use text::{LineHeight, Shaping, Wrapping}; /// A paragraph of text. #[allow(missing_debug_implementations)] @@ -29,6 +29,7 @@ where vertical_alignment: alignment::Vertical, font: Option<Renderer::Font>, shaping: Shaping, + wrapping: Wrapping, class: Theme::Class<'a>, } @@ -48,7 +49,8 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::default(), + wrapping: Wrapping::default(), class: Theme::default(), } } @@ -115,6 +117,12 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`Text`]. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + /// Sets the style of the [`Text`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self @@ -198,6 +206,7 @@ where self.horizontal_alignment, self.vertical_alignment, self.shaping, + self.wrapping, ) } @@ -232,6 +241,7 @@ pub fn layout<Renderer>( horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, + wrapping: Wrapping, ) -> layout::Node where Renderer: text::Renderer, @@ -253,6 +263,7 @@ where horizontal_alignment, vertical_alignment, shaping, + wrapping, }); paragraph.min_bounds() diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs index 88fe2fbd..a47582a6 100644 --- a/core/src/window/settings/windows.rs +++ b/core/src/window/settings/windows.rs @@ -8,6 +8,12 @@ pub struct PlatformSpecific { /// Whether show or hide the window icon in the taskbar. pub skip_taskbar: bool, + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + pub undecorated_shadow: bool, } impl Default for PlatformSpecific { @@ -15,6 +21,7 @@ impl Default for PlatformSpecific { Self { drag_and_drop: true, skip_taskbar: false, + undecorated_shadow: false, } } } diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml deleted file mode 100644 index 83b7b8a4..00000000 --- a/examples/component/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "component" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] -edition = "2021" -publish = false - -[dependencies] -iced.workspace = true -iced.features = ["debug", "lazy"] diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs deleted file mode 100644 index a5d2e508..00000000 --- a/examples/component/src/main.rs +++ /dev/null @@ -1,149 +0,0 @@ -use iced::widget::center; -use iced::Element; - -use numeric_input::numeric_input; - -pub fn main() -> iced::Result { - iced::run("Component - Iced", Component::update, Component::view) -} - -#[derive(Default)] -struct Component { - value: Option<u32>, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - NumericInputChanged(Option<u32>), -} - -impl Component { - fn update(&mut self, message: Message) { - match message { - Message::NumericInputChanged(value) => { - self.value = value; - } - } - } - - fn view(&self) -> Element<Message> { - center(numeric_input(self.value, Message::NumericInputChanged)) - .padding(20) - .into() - } -} - -mod numeric_input { - use iced::widget::{button, component, row, text, text_input, Component}; - use iced::{Center, Element, Fill, Length, Size}; - - pub struct NumericInput<Message> { - value: Option<u32>, - on_change: Box<dyn Fn(Option<u32>) -> Message>, - } - - pub fn numeric_input<Message>( - value: Option<u32>, - on_change: impl Fn(Option<u32>) -> Message + 'static, - ) -> NumericInput<Message> { - NumericInput::new(value, on_change) - } - - #[derive(Debug, Clone)] - pub enum Event { - InputChanged(String), - IncrementPressed, - DecrementPressed, - } - - impl<Message> NumericInput<Message> { - pub fn new( - value: Option<u32>, - on_change: impl Fn(Option<u32>) -> Message + 'static, - ) -> Self { - Self { - value, - on_change: Box::new(on_change), - } - } - } - - impl<Message, Theme> Component<Message, Theme> for NumericInput<Message> - where - Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, - { - type State = (); - type Event = Event; - - fn update( - &mut self, - _state: &mut Self::State, - event: Event, - ) -> Option<Message> { - match event { - Event::IncrementPressed => Some((self.on_change)(Some( - self.value.unwrap_or_default().saturating_add(1), - ))), - Event::DecrementPressed => Some((self.on_change)(Some( - self.value.unwrap_or_default().saturating_sub(1), - ))), - Event::InputChanged(value) => { - if value.is_empty() { - Some((self.on_change)(None)) - } else { - value - .parse() - .ok() - .map(Some) - .map(self.on_change.as_ref()) - } - } - } - } - - fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> { - let button = |label, on_press| { - button(text(label).width(Fill).height(Fill).center()) - .width(40) - .height(40) - .on_press(on_press) - }; - - row![ - button("-", Event::DecrementPressed), - text_input( - "Type a number", - self.value - .as_ref() - .map(u32::to_string) - .as_deref() - .unwrap_or(""), - ) - .on_input(Event::InputChanged) - .padding(10), - button("+", Event::IncrementPressed), - ] - .align_y(Center) - .spacing(10) - .into() - } - - fn size_hint(&self) -> Size<Length> { - Size { - width: Length::Fill, - height: Length::Shrink, - } - } - } - - impl<'a, Message, Theme> From<NumericInput<Message>> - for Element<'a, Message, Theme> - where - Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, - Message: 'a, - { - fn from(numeric_input: NumericInput<Message>) -> Self { - component(numeric_input) - } - } -} diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index aa07b328..5f12aec5 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -2,7 +2,7 @@ use iced::highlighter; use iced::keyboard; use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, - text_editor, tooltip, + text_editor, toggler, tooltip, }; use iced::{Center, Element, Fill, Font, Subscription, Task, Theme}; @@ -24,6 +24,7 @@ struct Editor { file: Option<PathBuf>, content: text_editor::Content, theme: highlighter::Theme, + word_wrap: bool, is_loading: bool, is_dirty: bool, } @@ -32,6 +33,7 @@ struct Editor { enum Message { ActionPerformed(text_editor::Action), ThemeSelected(highlighter::Theme), + WordWrapToggled(bool), NewFile, OpenFile, FileOpened(Result<(PathBuf, Arc<String>), Error>), @@ -46,6 +48,7 @@ impl Editor { file: None, content: text_editor::Content::new(), theme: highlighter::Theme::SolarizedDark, + word_wrap: true, is_loading: true, is_dirty: false, }, @@ -76,6 +79,11 @@ impl Editor { Task::none() } + Message::WordWrapToggled(word_wrap) => { + self.word_wrap = word_wrap; + + Task::none() + } Message::NewFile => { if !self.is_loading { self.file = None; @@ -152,6 +160,11 @@ impl Editor { self.is_dirty.then_some(Message::SaveFile) ), horizontal_space(), + toggler( + Some("Word Wrap"), + self.word_wrap, + Message::WordWrapToggled + ), pick_list( highlighter::Theme::ALL, Some(self.theme), @@ -189,6 +202,11 @@ impl Editor { text_editor(&self.content) .height(Fill) .on_action(Message::ActionPerformed) + .wrapping(if self.word_wrap { + text::Wrapping::Word + } else { + text::Wrapping::None + }) .highlight( self.file .as_deref() diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index eb51f985..5605478f 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -29,8 +29,7 @@ impl Markdown { ( Self { content: text_editor::Content::with_text(INITIAL_CONTENT), - items: markdown::parse(INITIAL_CONTENT, theme.palette()) - .collect(), + items: markdown::parse(INITIAL_CONTENT).collect(), theme, }, widget::focus_next(), @@ -45,11 +44,8 @@ impl Markdown { self.content.perform(action); if is_edit { - self.items = markdown::parse( - &self.content.text(), - self.theme.palette(), - ) - .collect(); + self.items = + markdown::parse(&self.content.text()).collect(); } } Message::LinkClicked(link) => { @@ -67,8 +63,12 @@ impl Markdown { .font(Font::MONOSPACE) .highlight("markdown", highlighter::Theme::Base16Ocean); - let preview = markdown(&self.items, markdown::Settings::default()) - .map(Message::LinkClicked); + let preview = markdown( + &self.items, + markdown::Settings::default(), + markdown::Style::from_palette(self.theme.palette()), + ) + .map(Message::LinkClicked); row![editor, scrollable(preview).spacing(10).height(Fill)] .spacing(10) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index f1f0e8ad..067ca24d 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -201,19 +201,21 @@ where { stack![ base.into(), - mouse_area(center(opaque(content)).style(|_theme| { - container::Style { - background: Some( - Color { - a: 0.8, - ..Color::BLACK - } - .into(), - ), - ..container::Style::default() - } - })) - .on_press(on_blur) + opaque( + mouse_area(center(opaque(content)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() + } + })) + .on_press(on_blur) + ) ] .into() } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index f18fc5f3..67f4d27f 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -154,11 +154,23 @@ impl Example { .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls( - id, - total_panes, - pane.is_pinned, - is_maximized, + .controls(pane_grid::Controls::dynamic( + view_controls( + id, + total_panes, + pane.is_pinned, + is_maximized, + ), + button(text("X").size(14)) + .style(button::danger) + .padding(3) + .on_press_maybe( + if total_panes > 1 && !pane.is_pinned { + Some(Message::Close(id)) + } else { + None + }, + ), )) .padding(10) .style(if is_focused { diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 527aaa29..e19d5cf7 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -78,7 +78,7 @@ impl Styling { .on_toggle(Message::CheckboxToggled); let toggler = toggler( - String::from("Toggle me!"), + Some("Toggle me!"), self.toggler_value, Message::TogglerToggled, ) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 86845f87..a5f7b36a 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -202,7 +202,8 @@ impl Todos { .on_input(Message::InputChanged) .on_submit(Message::CreateTask) .padding(15) - .size(30); + .size(30) + .align_x(Center); let controls = view_controls(tasks, *filter); let filtered_tasks = diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index ee4754e6..0dd588fe 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -358,7 +358,7 @@ impl Tour { .push("A toggler is mostly used to enable or disable something.") .push( Container::new(toggler( - "Toggle me to continue...".to_owned(), + Some("Toggle me to continue..."), self.toggler, Message::TogglerChanged, )) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 23ec14d4..feb9932a 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -11,7 +11,7 @@ pub use cosmic_text; use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrapping}; use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; @@ -306,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap { + match wrapping { + Wrapping::None => cosmic_text::Wrap::None, + Wrapping::Word => cosmic_text::Wrap::Word, + Wrapping::Glyph => cosmic_text::Wrap::Glyph, + Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 80733bbb..1f1d0050 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -3,7 +3,7 @@ use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::LineHeight; +use crate::core::text::{LineHeight, Wrapping}; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -437,6 +437,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ) { let editor = @@ -448,13 +449,12 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); + let buffer = buffer_mut_from_editor(&mut internal.editor); + if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in buffer_mut_from_editor(&mut internal.editor) - .lines - .iter_mut() - { + for line in buffer.lines.iter_mut() { line.reset(); } @@ -465,10 +465,7 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in buffer_mut_from_editor(&mut internal.editor) - .lines - .iter_mut() - { + for line in buffer.lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); @@ -478,7 +475,7 @@ impl editor::Editor for Editor { internal.topmost_line_changed = Some(0); } - let metrics = buffer_from_editor(&internal.editor).metrics(); + let metrics = buffer.metrics(); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -486,16 +483,24 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - buffer_mut_from_editor(&mut internal.editor).set_metrics( + buffer.set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); } + let new_wrap = text::to_wrap(new_wrapping); + + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); + + buffer.set_wrap(font_system.raw(), new_wrap); + } + if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - buffer_mut_from_editor(&mut internal.editor).set_size( + buffer.set_size( font_system.raw(), Some(new_bounds.width), Some(new_bounds.height), diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index b9f9c833..07ddbb82 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,7 +1,7 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, Shaping, Span, Text}; +use crate::core::text::{Hit, Shaping, Span, Text, Wrapping}; use crate::core::{Font, Point, Rectangle, Size}; use crate::text; @@ -17,6 +17,7 @@ struct Internal { buffer: cosmic_text::Buffer, font: Font, shaping: Shaping, + wrapping: Wrapping, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, bounds: Size, @@ -94,6 +95,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, bounds: text.bounds, min_bounds, version: font_system.version(), @@ -160,6 +162,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, bounds: text.bounds, min_bounds, version: font_system.version(), @@ -192,6 +195,7 @@ impl core::text::Paragraph for Paragraph { || metrics.line_height != text.line_height.to_absolute(text.size).0 || paragraph.font != text.font || paragraph.shaping != text.shaping + || paragraph.wrapping != text.wrapping || paragraph.horizontal_alignment != text.horizontal_alignment || paragraph.vertical_alignment != text.vertical_alignment { @@ -387,6 +391,7 @@ impl Default for Internal { }), font: Font::default(), shaping: Shaping::default(), + wrapping: Wrapping::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, bounds: Size::ZERO, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index cd27cdfe..cdf3d80a 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -63,6 +63,9 @@ pub enum Action { /// Get the current logical coordinates of the window. GetPosition(Id, oneshot::Sender<Option<Point>>), + /// Get the current scale factor (DPI) of the window. + GetScaleFactor(Id, oneshot::Sender<f32>), + /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. @@ -144,6 +147,18 @@ pub enum Action { /// Screenshot the viewport of the window. Screenshot(Id, oneshot::Sender<Screenshot>), + + /// Enables mouse passthrough for the given window. + /// + /// This disables mouse events for the window and passes mouse events + /// through to whatever window is underneath. + EnableMousePassthrough(Id), + + /// Disable mouse passthrough for the given window. + /// + /// This enables mouse events for the window and stops mouse events + /// from being passed to whatever is underneath. + DisableMousePassthrough(Id), } /// Subscribes to the frames of the window of the running application. @@ -292,6 +307,13 @@ pub fn get_position(id: Id) -> Task<Option<Point>> { }) } +/// Gets the scale factor of the window with the given [`Id`]. +pub fn get_scale_factor(id: Id) -> Task<f32> { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetScaleFactor(id, channel)) + }) +} + /// Moves the window to the given logical coordinates. pub fn move_to<T>(id: Id, position: Point) -> Task<T> { task::effect(crate::Action::Window(Action::Move(id, position))) @@ -396,3 +418,19 @@ pub fn screenshot(id: Id) -> Task<Screenshot> { crate::Action::Window(Action::Screenshot(id, channel)) }) } + +/// Enables mouse passthrough for the given window. +/// +/// This disables mouse events for the window and passes mouse events +/// through to whatever window is underneath. +pub fn enable_mouse_passthrough<Message>(id: Id) -> Task<Message> { + task::effect(crate::Action::Window(Action::EnableMousePassthrough(id))) +} + +/// Disable mouse passthrough for the given window. +/// +/// This enables mouse events for the window and stops mouse events +/// from being passed to whatever is underneath. +pub fn disable_mouse_passthrough<Message>(id: Id) -> Task<Message> { + task::effect(crate::Action::Window(Action::DisableMousePassthrough(id))) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 39167514..d79f0dc8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -408,6 +408,7 @@ impl Renderer { horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: core::text::Shaping::Basic, + wrapping: core::text::Wrapping::Word, }; renderer.fill_text( 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/container.rs b/widget/src/container.rs index 54043ad0..c3a66360 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(); @@ -613,6 +612,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 +635,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..349f02a6 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,7 +767,7 @@ where /// /// [`Toggler`]: crate::Toggler pub fn toggler<'a, Message, Theme, Renderer>( - label: impl Into<Option<String>>, + label: Option<impl text::IntoFragment<'a>>, is_checked: bool, f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> 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/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/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/stack.rs b/widget/src/stack.rs index 001376ac..9ccaa274 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -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/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..d1aa4640 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, @@ -148,6 +150,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 +194,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, @@ -496,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(), ); @@ -784,6 +794,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrapping: self.wrapping, }, text_bounds.position(), style.placeholder, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 173de136..92047381 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, @@ -193,6 +195,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 +251,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 +276,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 +397,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 +470,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 +494,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 +533,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 +596,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,7 +614,18 @@ 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 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, state.last_click); @@ -677,7 +702,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 +742,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 +765,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 +793,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 +840,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 +872,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 +900,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 +1122,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 +1434,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 +1539,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..57e142e8 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -26,7 +26,7 @@ use crate::core::{ /// /// let is_toggled = true; /// -/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); +/// Toggler::new(Some("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); /// ``` #[allow(missing_debug_implementations)] pub struct Toggler< @@ -40,13 +40,14 @@ pub struct Toggler< { is_toggled: bool, on_toggle: Box<dyn Fn(bool) -> Message + 'a>, - label: Option<String>, + 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>, @@ -69,7 +70,7 @@ where /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. pub fn new<F>( - label: impl Into<Option<String>>, + label: Option<impl text::IntoFragment<'a>>, is_toggled: bool, f: F, ) -> Self @@ -79,13 +80,14 @@ where Toggler { is_toggled, on_toggle: Box::new(f), - label: label.into(), + label: label.map(text::IntoFragment::into_fragment), 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(), @@ -131,6 +133,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 +224,7 @@ where self.text_alignment, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) } else { layout::Node::new(Size::ZERO) diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 7ae646fc..d54a1fe0 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -2,7 +2,7 @@ use crate::core::clipboard::Kind; use std::sync::Arc; -use winit::window::Window; +use winit::window::{Window, WindowId}; /// A buffer for short-term storage and transfer within and between /// applications. @@ -83,6 +83,14 @@ impl Clipboard { State::Unavailable => {} } } + + /// Returns the identifier of the window used to create the [`Clipboard`], if any. + pub fn window_id(&self) -> Option<WindowId> { + match &self.state { + State::Connected { window, .. } => Some(window.id()), + State::Unavailable => None, + } + } } impl crate::core::Clipboard for Clipboard { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index e88ff84d..585e2409 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -79,6 +79,10 @@ pub fn window_attributes( attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); + + attributes = attributes.with_undecorated_shadow( + settings.platform_specific.undecorated_shadow, + ); } #[cfg(target_os = "macos")] diff --git a/winit/src/program.rs b/winit/src/program.rs index efe8a978..52d8eb5f 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -307,8 +307,6 @@ where } }; - let clipboard = Clipboard::connect(window.clone()); - let finish_boot = async move { let mut compositor = C::new(graphics_settings, window.clone()).await?; @@ -318,10 +316,7 @@ where } sender - .send(Boot { - compositor, - clipboard, - }) + .send(Boot { compositor }) .ok() .expect("Send boot event"); @@ -617,7 +612,6 @@ where struct Boot<C> { compositor: C, - clipboard: Clipboard, } #[derive(Debug)] @@ -650,7 +644,7 @@ async fn run_instance<P, C>( mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>, mut proxy: Proxy<P::Message>, mut debug: Debug, - mut boot: oneshot::Receiver<Boot<C>>, + boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, mut control_sender: mpsc::UnboundedSender<Control>, is_daemon: bool, @@ -662,10 +656,7 @@ async fn run_instance<P, C>( use winit::event; use winit::event_loop::ControlFlow; - let Boot { - mut compositor, - mut clipboard, - } = boot.try_recv().ok().flatten().expect("Receive boot"); + let Boot { mut compositor } = boot.await.expect("Receive boot"); let mut window_manager = WindowManager::new(); let mut is_window_opening = !is_daemon; @@ -676,10 +667,22 @@ async fn run_instance<P, C>( let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); + let mut clipboard = Clipboard::unconnected(); debug.startup_finished(); - while let Some(event) = event_receiver.next().await { + loop { + // Empty the queue if possible + let event = if let Ok(event) = event_receiver.try_next() { + event + } else { + event_receiver.next().await + }; + + let Some(event) = event else { + break; + }; + match event { Event::WindowCreated { id, @@ -723,6 +726,10 @@ async fn run_instance<P, C>( }), )); + if clipboard.window_id().is_none() { + clipboard = Clipboard::connect(window.raw.clone()); + } + let _ = on_open.send(id); is_window_opening = false; } @@ -968,14 +975,22 @@ async fn run_instance<P, C>( winit::event::WindowEvent::CloseRequested ) && window.exit_on_close_request { - let _ = window_manager.remove(id); - let _ = user_interfaces.remove(&id); - let _ = ui_caches.remove(&id); - - events.push(( - id, - core::Event::Window(window::Event::Closed), - )); + run_action( + Action::Window(runtime::window::Action::Close( + id, + )), + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); } else { window.state.update( &window.raw, @@ -1212,13 +1227,23 @@ fn run_action<P, C>( *is_window_opening = true; } window::Action::Close(id) => { - let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); + let _ = interfaces.remove(&id); + + if let Some(window) = window_manager.remove(id) { + if clipboard.window_id() == Some(window.raw.id()) { + *clipboard = window_manager + .first() + .map(|window| window.raw.clone()) + .map(Clipboard::connect) + .unwrap_or_else(Clipboard::unconnected); + } - events.push(( - id, - core::Event::Window(core::window::Event::Closed), - )); + events.push(( + id, + core::Event::Window(core::window::Event::Closed), + )); + } } window::Action::GetOldest(channel) => { let id = @@ -1278,7 +1303,7 @@ fn run_action<P, C>( } } window::Action::GetPosition(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { + if let Some(window) = window_manager.get(id) { let position = window .raw .inner_position() @@ -1293,6 +1318,13 @@ fn run_action<P, C>( let _ = channel.send(position); } } + window::Action::GetScaleFactor(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let scale_factor = window.raw.scale_factor(); + + let _ = channel.send(scale_factor as f32); + } + } window::Action::Move(id, position) => { if let Some(window) = window_manager.get_mut(id) { window.raw.set_outer_position( @@ -1403,6 +1435,16 @@ fn run_action<P, C>( )); } } + window::Action::EnableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(false); + } + } + window::Action::DisableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(true); + } + } }, Action::System(action) => match action { system::Action::QueryInformation(_channel) => { diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index fcbf79f6..3d22e155 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -74,12 +74,20 @@ where self.entries.is_empty() } + pub fn first(&self) -> Option<&Window<P, C>> { + self.entries.first_key_value().map(|(_id, window)| window) + } + pub fn iter_mut( &mut self, ) -> impl Iterator<Item = (Id, &mut Window<P, C>)> { self.entries.iter_mut().map(|(k, v)| (*k, v)) } + pub fn get(&self, id: Id) -> Option<&Window<P, C>> { + self.entries.get(&id) + } + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> { self.entries.get_mut(&id) } |