summaryrefslogtreecommitdiffstats
path: root/widget/src
diff options
context:
space:
mode:
Diffstat (limited to 'widget/src')
-rw-r--r--widget/src/button.rs34
-rw-r--r--widget/src/canvas.rs21
-rw-r--r--widget/src/canvas/event.rs2
-rw-r--r--widget/src/checkbox.rs15
-rw-r--r--widget/src/column.rs79
-rw-r--r--widget/src/combo_box.rs29
-rw-r--r--widget/src/container.rs85
-rw-r--r--widget/src/helpers.rs51
-rw-r--r--widget/src/image.rs42
-rw-r--r--widget/src/image/viewer.rs25
-rw-r--r--widget/src/keyed/column.rs60
-rw-r--r--widget/src/lazy.rs16
-rw-r--r--widget/src/lazy/component.rs14
-rw-r--r--widget/src/lazy/helpers.rs3
-rw-r--r--widget/src/lazy/responsive.rs17
-rw-r--r--widget/src/lib.rs10
-rw-r--r--widget/src/mouse_area.rs10
-rw-r--r--widget/src/overlay/menu.rs25
-rw-r--r--widget/src/pane_grid.rs89
-rw-r--r--widget/src/pane_grid/content.rs9
-rw-r--r--widget/src/pane_grid/state.rs17
-rw-r--r--widget/src/pane_grid/title_bar.rs18
-rw-r--r--widget/src/pick_list.rs52
-rw-r--r--widget/src/progress_bar.rs23
-rw-r--r--widget/src/qr_code.rs11
-rw-r--r--widget/src/radio.rs14
-rw-r--r--widget/src/row.rs78
-rw-r--r--widget/src/rule.rs15
-rw-r--r--widget/src/scrollable.rs50
-rw-r--r--widget/src/shader.rs216
-rw-r--r--widget/src/shader/event.rs25
-rw-r--r--widget/src/shader/program.rs62
-rw-r--r--widget/src/slider.rs16
-rw-r--r--widget/src/space.rs15
-rw-r--r--widget/src/svg.rs25
-rw-r--r--widget/src/text_editor.rs736
-rw-r--r--widget/src/text_input.rs222
-rw-r--r--widget/src/toggler.rs14
-rw-r--r--widget/src/tooltip.rs27
-rw-r--r--widget/src/vertical_slider.rs16
40 files changed, 1729 insertions, 559 deletions
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 384a3156..0ebb8dcc 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -10,8 +10,8 @@ use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
- Background, Clipboard, Color, Element, Layout, Length, Padding, Point,
- Rectangle, Shell, Vector, Widget,
+ Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle,
+ Shell, Size, Vector, Widget,
};
pub use iced_style::button::{Appearance, StyleSheet};
@@ -71,11 +71,14 @@ where
{
/// Creates a new [`Button`] with the given content.
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
+ let content = content.into();
+ let size = content.as_widget().size_hint();
+
Button {
- content: content.into(),
+ content,
on_press: None,
- width: Length::Shrink,
- height: Length::Shrink,
+ width: size.width.fluid(),
+ height: size.height.fluid(),
padding: Padding::new(5.0),
style: <Renderer::Theme as StyleSheet>::Style::default(),
}
@@ -149,12 +152,11 @@ where
tree.diff_children(std::slice::from_ref(&self.content));
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -431,15 +433,7 @@ pub fn layout(
padding: Padding,
layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits.width(width).height(height);
-
- let mut content = layout_content(&limits.pad(padding));
- let padding = padding.fit(content.size(), limits.max());
- let size = limits.pad(padding).resolve(content.size()).pad(padding);
-
- content.move_to(Point::new(padding.left, padding.top));
-
- layout::Node::with_children(size, vec![content])
+ layout::padded(limits, width, height, padding, layout_content)
}
/// Returns the [`mouse::Interaction`] of a [`Button`].
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 390f4d92..4e42a671 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -14,8 +14,9 @@ use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
-use crate::core::{Clipboard, Element, Shell, Widget};
-use crate::core::{Length, Rectangle, Size, Vector};
+use crate::core::{
+ Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
+};
use crate::graphics::geometry;
use std::marker::PhantomData;
@@ -119,12 +120,11 @@ where
tree::State::new(P::State::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -133,10 +133,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs
index 1288365f..a8eb47f7 100644
--- a/widget/src/canvas/event.rs
+++ b/widget/src/canvas/event.rs
@@ -8,7 +8,7 @@ pub use crate::core::event::Status;
/// A [`Canvas`] event.
///
/// [`Canvas`]: crate::Canvas
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, PartialEq)]
pub enum Event {
/// A mouse event.
Mouse(mouse::Event),
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index d7fdf339..0353b3ad 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -174,12 +174,11 @@ where
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -266,7 +265,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let is_mouse_over = cursor.is_over(layout.bounds());
@@ -315,6 +314,7 @@ where
},
bounds.center(),
custom_style.icon_color,
+ *viewport,
);
}
}
@@ -330,6 +330,7 @@ where
crate::text::Appearance {
color: custom_style.text_color,
},
+ viewport,
);
}
}
diff --git a/widget/src/column.rs b/widget/src/column.rs
index 42e90ac1..d6eea84b 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -7,7 +7,7 @@ use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
- Shell, Widget,
+ Shell, Size, Widget,
};
/// A container that distributes its contents vertically.
@@ -22,16 +22,12 @@ pub struct Column<'a, Message, Renderer = crate::Renderer> {
children: Vec<Element<'a, Message, Renderer>>,
}
-impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Column<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
/// Creates an empty [`Column`].
pub fn new() -> Self {
- Self::with_children(Vec::new())
- }
-
- /// Creates a [`Column`] with the given elements.
- pub fn with_children(
- children: Vec<Element<'a, Message, Renderer>>,
- ) -> Self {
Column {
spacing: 0.0,
padding: Padding::ZERO,
@@ -39,10 +35,17 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
- children,
+ children: Vec::new(),
}
}
+ /// Creates a [`Column`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+ ) -> Self {
+ children.into_iter().fold(Self::new(), Self::push)
+ }
+
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
@@ -88,12 +91,26 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
mut self,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
- self.children.push(child.into());
+ let child = child.into();
+ let size = child.as_widget().size_hint();
+
+ if size.width.is_fill() {
+ self.width = Length::Fill;
+ }
+
+ if size.height.is_fill() {
+ self.height = Length::Fill;
+ }
+
+ self.children.push(child);
self
}
}
-impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
fn default() -> Self {
Self::new()
}
@@ -112,12 +129,11 @@ where
tree.diff_children(&self.children);
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -126,15 +142,14 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .max_width(self.max_width)
- .width(self.width)
- .height(self.height);
+ let limits = limits.max_width(self.max_width);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
+ self.width,
+ self.height,
self.padding,
self.spacing,
self.align_items,
@@ -224,15 +239,17 @@ where
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
- for ((child, state), layout) in self
- .children
- .iter()
- .zip(&tree.children)
- .zip(layout.children())
- {
- child
- .as_widget()
- .draw(state, renderer, theme, style, layout, cursor, viewport);
+ if let Some(viewport) = layout.bounds().intersection(viewport) {
+ for ((child, state), layout) in self
+ .children
+ .iter()
+ .zip(&tree.children)
+ .zip(layout.children())
+ {
+ child.as_widget().draw(
+ state, renderer, theme, style, layout, cursor, &viewport,
+ );
+ }
}
}
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 768c2402..73beeac3 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -1,6 +1,7 @@
//! Display a dropdown list of searchable and selectable options.
use crate::core::event::{self, Event};
use crate::core::keyboard;
+use crate::core::keyboard::key;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
@@ -8,7 +9,9 @@ use crate::core::renderer;
use crate::core::text;
use crate::core::time::Instant;
use crate::core::widget::{self, Widget};
-use crate::core::{Clipboard, Element, Length, Padding, Rectangle, Shell};
+use crate::core::{
+ Clipboard, Element, Length, Padding, Rectangle, Shell, Size,
+};
use crate::overlay::menu;
use crate::text::LineHeight;
use crate::{container, scrollable, text_input, TextInput};
@@ -297,12 +300,8 @@ where
+ scrollable::StyleSheet
+ menu::StyleSheet,
{
- fn width(&self) -> Length {
- Widget::<TextInputEvent, Renderer>::width(&self.text_input)
- }
-
- fn height(&self) -> Length {
- Widget::<TextInputEvent, Renderer>::height(&self.text_input)
+ fn size(&self) -> Size<Length> {
+ Widget::<TextInputEvent, Renderer>::size(&self.text_input)
}
fn layout(
@@ -438,14 +437,14 @@ where
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
- key_code,
+ key: keyboard::Key::Named(named_key),
modifiers,
..
}) = event
{
let shift_modifer = modifiers.shift();
- match (key_code, shift_modifer) {
- (keyboard::KeyCode::Enter, _) => {
+ match (named_key, shift_modifer) {
+ (key::Named::Enter, _) => {
if let Some(index) = &menu.hovered_option {
if let Some(option) =
state.filtered_options.options.get(*index)
@@ -457,8 +456,7 @@ where
event_status = event::Status::Captured;
}
- (keyboard::KeyCode::Up, _)
- | (keyboard::KeyCode::Tab, true) => {
+ (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
if let Some(index) = &mut menu.hovered_option {
if *index == 0 {
*index = state
@@ -494,8 +492,8 @@ where
event_status = event::Status::Captured;
}
- (keyboard::KeyCode::Down, _)
- | (keyboard::KeyCode::Tab, false)
+ (key::Named::ArrowDown, _)
+ | (key::Named::Tab, false)
if !modifiers.shift() =>
{
if let Some(index) = &mut menu.hovered_option {
@@ -622,7 +620,7 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let is_focused = {
let text_input_state = tree.children[0]
@@ -645,6 +643,7 @@ where
layout,
cursor,
selection,
+ viewport,
);
}
diff --git a/widget/src/container.rs b/widget/src/container.rs
index ee7a4965..cffb0458 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -46,17 +46,20 @@ where
where
T: Into<Element<'a, Message, Renderer>>,
{
+ let content = content.into();
+ let size = content.as_widget().size_hint();
+
Container {
id: None,
padding: Padding::ZERO,
- width: Length::Shrink,
- height: Length::Shrink,
+ width: size.width.fluid(),
+ height: size.height.fluid(),
max_width: f32::INFINITY,
max_height: f32::INFINITY,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
style: Default::default(),
- content: content.into(),
+ content,
}
}
@@ -152,12 +155,11 @@ where
self.content.as_widget().diff(tree);
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -252,21 +254,23 @@ where
) {
let style = theme.appearance(&self.style);
- draw_background(renderer, &style, layout.bounds());
-
- self.content.as_widget().draw(
- tree,
- renderer,
- theme,
- &renderer::Style {
- text_color: style
- .text_color
- .unwrap_or(renderer_style.text_color),
- },
- layout.children().next().unwrap(),
- cursor,
- viewport,
- );
+ if let Some(viewport) = layout.bounds().intersection(viewport) {
+ draw_background(renderer, &style, layout.bounds());
+
+ self.content.as_widget().draw(
+ tree,
+ renderer,
+ theme,
+ &renderer::Style {
+ text_color: style
+ .text_color
+ .unwrap_or(renderer_style.text_color),
+ },
+ layout.children().next().unwrap(),
+ cursor,
+ &viewport,
+ );
+ }
}
fn overlay<'b>(
@@ -309,25 +313,20 @@ pub fn layout(
vertical_alignment: alignment::Vertical,
layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits
- .loose()
- .max_width(max_width)
- .max_height(max_height)
- .width(width)
- .height(height);
-
- let mut content = layout_content(&limits.pad(padding).loose());
- let padding = padding.fit(content.size(), limits.max());
- let size = limits.pad(padding).resolve(content.size());
-
- content.move_to(Point::new(padding.left, padding.top));
- content.align(
- Alignment::from(horizontal_alignment),
- Alignment::from(vertical_alignment),
- size,
- );
-
- layout::Node::with_children(size.pad(padding), vec![content])
+ layout::positioned(
+ &limits.max_width(max_width).max_height(max_height),
+ width,
+ height,
+ padding,
+ |limits| layout_content(&limits.loose()),
+ |content, size| {
+ content.align(
+ Alignment::from(horizontal_alignment),
+ Alignment::from(vertical_alignment),
+ size,
+ )
+ },
+ )
}
/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`.
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 3c9c2b29..498dd76c 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -16,6 +16,7 @@ use crate::runtime::Command;
use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider};
use crate::text::{self, Text};
+use crate::text_editor::{self, TextEditor};
use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
@@ -33,7 +34,7 @@ macro_rules! column {
$crate::Column::new()
);
($($x:expr),+ $(,)?) => (
- $crate::Column::with_children(vec![$($crate::core::Element::from($x)),+])
+ $crate::Column::with_children([$($crate::core::Element::from($x)),+])
);
}
@@ -46,7 +47,7 @@ macro_rules! row {
$crate::Row::new()
);
($($x:expr),+ $(,)?) => (
- $crate::Row::with_children(vec![$($crate::core::Element::from($x)),+])
+ $crate::Row::with_children([$($crate::core::Element::from($x)),+])
);
}
@@ -64,9 +65,12 @@ where
}
/// Creates a new [`Column`] with the given children.
-pub fn column<Message, Renderer>(
- children: Vec<Element<'_, Message, Renderer>>,
-) -> Column<'_, Message, Renderer> {
+pub fn column<'a, Message, Renderer>(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+) -> Column<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+{
Column::with_children(children)
}
@@ -76,6 +80,7 @@ pub fn keyed_column<'a, Key, Message, Renderer>(
) -> keyed::Column<'a, Key, Message, Renderer>
where
Key: Copy + PartialEq,
+ Renderer: core::Renderer,
{
keyed::Column::with_children(children)
}
@@ -83,9 +88,12 @@ where
/// Creates a new [`Row`] with the given children.
///
/// [`Row`]: crate::Row
-pub fn row<Message, Renderer>(
- children: Vec<Element<'_, Message, Renderer>>,
-) -> Row<'_, Message, Renderer> {
+pub fn row<'a, Message, Renderer>(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+) -> Row<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+{
Row::with_children(children)
}
@@ -206,6 +214,20 @@ where
TextInput::new(placeholder, value)
}
+/// Creates a new [`TextEditor`].
+///
+/// [`TextEditor`]: crate::TextEditor
+pub fn text_editor<Message, Renderer>(
+ content: &text_editor::Content<Renderer>,
+) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Renderer>
+where
+ Message: Clone,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: text_editor::StyleSheet,
+{
+ TextEditor::new(content)
+}
+
/// Creates a new [`Slider`].
///
/// [`Slider`]: crate::Slider
@@ -249,7 +271,7 @@ pub fn pick_list<'a, Message, Renderer, T>(
on_selected: impl Fn(T) -> Message + 'a,
) -> PickList<'a, T, Message, Renderer>
where
- T: ToString + Eq + 'static,
+ T: ToString + PartialEq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: core::text::Renderer,
Renderer::Theme: pick_list::StyleSheet
@@ -370,6 +392,17 @@ where
crate::Canvas::new(program)
}
+/// Creates a new [`Shader`].
+///
+/// [`Shader`]: crate::Shader
+#[cfg(feature = "wgpu")]
+pub fn shader<Message, P>(program: P) -> crate::Shader<Message, P>
+where
+ P: crate::shader::Program<Message>,
+{
+ crate::Shader::new(program)
+}
+
/// Focuses the previous focusable widget.
pub fn focus_previous<Message>() -> Command<Message>
where
diff --git a/widget/src/image.rs b/widget/src/image.rs
index a0e89920..e906ac13 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -13,7 +13,7 @@ use crate::core::{
use std::hash::Hash;
-pub use image::Handle;
+pub use image::{FilterMethod, Handle};
/// Creates a new [`Viewer`] with the given image `Handle`.
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
@@ -37,6 +37,7 @@ pub struct Image<Handle> {
width: Length,
height: Length,
content_fit: ContentFit,
+ filter_method: FilterMethod,
}
impl<Handle> Image<Handle> {
@@ -47,6 +48,7 @@ impl<Handle> Image<Handle> {
width: Length::Shrink,
height: Length::Shrink,
content_fit: ContentFit::Contain,
+ filter_method: FilterMethod::default(),
}
}
@@ -65,11 +67,15 @@ impl<Handle> Image<Handle> {
/// Sets the [`ContentFit`] of the [`Image`].
///
/// Defaults to [`ContentFit::Contain`]
- pub fn content_fit(self, content_fit: ContentFit) -> Self {
- Self {
- content_fit,
- ..self
- }
+ pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
+ self.content_fit = content_fit;
+ self
+ }
+
+ /// Sets the [`FilterMethod`] of the [`Image`].
+ pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
+ self.filter_method = filter_method;
+ self
}
}
@@ -93,7 +99,7 @@ where
};
// The size to be available to the widget prior to `Shrink`ing
- let raw_size = limits.width(width).height(height).resolve(image_size);
+ let raw_size = limits.resolve(width, height, image_size);
// The uncropped size of the image when fit to the bounds above
let full_size = content_fit.fit(image_size, raw_size);
@@ -119,6 +125,7 @@ pub fn draw<Renderer, Handle>(
layout: Layout<'_>,
handle: &Handle,
content_fit: ContentFit,
+ filter_method: FilterMethod,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
@@ -141,7 +148,7 @@ pub fn draw<Renderer, Handle>(
..bounds
};
- renderer.draw(handle.clone(), drawing_bounds + offset);
+ renderer.draw(handle.clone(), filter_method, drawing_bounds + offset);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
@@ -157,12 +164,11 @@ where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -191,7 +197,13 @@ where
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- draw(renderer, layout, &self.handle, self.content_fit);
+ draw(
+ renderer,
+ layout,
+ &self.handle,
+ self.content_fit,
+ self.filter_method,
+ );
}
}
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index 44624fc8..98080577 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -22,19 +22,21 @@ pub struct Viewer<Handle> {
max_scale: f32,
scale_step: f32,
handle: Handle,
+ filter_method: image::FilterMethod,
}
impl<Handle> Viewer<Handle> {
/// Creates a new [`Viewer`] with the given [`State`].
pub fn new(handle: Handle) -> Self {
Viewer {
+ handle,
padding: 0.0,
width: Length::Shrink,
height: Length::Shrink,
min_scale: 0.25,
max_scale: 10.0,
scale_step: 0.10,
- handle,
+ filter_method: image::FilterMethod::default(),
}
}
@@ -95,12 +97,11 @@ where
tree::State::new(State::new())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -111,10 +112,11 @@ where
) -> layout::Node {
let Size { width, height } = renderer.dimensions(&self.handle);
- let mut size = limits
- .width(self.width)
- .height(self.height)
- .resolve(Size::new(width as f32, height as f32));
+ let mut size = limits.resolve(
+ self.width,
+ self.height,
+ Size::new(width as f32, height as f32),
+ );
let expansion_size = if height > width {
self.width
@@ -329,6 +331,7 @@ where
image::Renderer::draw(
renderer,
self.handle.clone(),
+ self.filter_method,
Rectangle {
x: bounds.x,
y: bounds.y,
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index 0ef82407..7f05a81e 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -8,7 +8,7 @@ use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
- Shell, Widget,
+ Shell, Size, Widget,
};
/// A container that distributes its contents vertically.
@@ -30,26 +30,10 @@ where
impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer>
where
Key: Copy + PartialEq,
+ Renderer: crate::core::Renderer,
{
/// Creates an empty [`Column`].
pub fn new() -> Self {
- Self::with_children(Vec::new())
- }
-
- /// Creates a [`Column`] with the given elements.
- pub fn with_children(
- children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>,
- ) -> Self {
- let (keys, children) = children.into_iter().fold(
- (Vec::new(), Vec::new()),
- |(mut keys, mut children), (key, child)| {
- keys.push(key);
- children.push(child);
-
- (keys, children)
- },
- );
-
Column {
spacing: 0.0,
padding: Padding::ZERO,
@@ -57,11 +41,20 @@ where
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
- keys,
- children,
+ keys: Vec::new(),
+ children: Vec::new(),
}
}
+ /// Creates a [`Column`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>,
+ ) -> Self {
+ children
+ .into_iter()
+ .fold(Self::new(), |column, (key, child)| column.push(key, child))
+ }
+
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
@@ -108,8 +101,19 @@ where
key: Key,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
+ let child = child.into();
+ let size = child.as_widget().size_hint();
+
+ if size.width.is_fill() {
+ self.width = Length::Fill;
+ }
+
+ if size.height.is_fill() {
+ self.height = Length::Fill;
+ }
+
self.keys.push(key);
- self.children.push(child.into());
+ self.children.push(child);
self
}
}
@@ -117,6 +121,7 @@ where
impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
where
Key: Copy + PartialEq,
+ Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
@@ -173,12 +178,11 @@ where
}
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -196,6 +200,8 @@ where
layout::flex::Axis::Vertical,
renderer,
&limits,
+ self.width,
+ self.height,
self.padding,
self.spacing,
self.align_items,
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index 589dd938..e9edbb4c 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -18,7 +18,7 @@ use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::Element;
use crate::core::{
- self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size,
+ self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector,
};
use crate::runtime::overlay::Nested;
@@ -142,12 +142,15 @@ where
}
}
- fn width(&self) -> Length {
- self.with_element(|element| element.as_widget().width())
+ fn size(&self) -> Size<Length> {
+ self.with_element(|element| element.as_widget().size())
}
- fn height(&self) -> Length {
- self.with_element(|element| element.as_widget().height())
+ fn size_hint(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -333,9 +336,10 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- overlay.layout(renderer, bounds, position)
+ overlay.layout(renderer, bounds, position, translation)
})
.unwrap_or_default()
}
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index d454b72b..3684e0c9 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -244,12 +244,15 @@ where
self.rebuild_element_if_necessary();
}
- fn width(&self) -> Length {
- self.with_element(|element| element.as_widget().width())
+ fn size(&self) -> Size<Length> {
+ self.with_element(|element| element.as_widget().size())
}
- fn height(&self) -> Length {
- self.with_element(|element| element.as_widget().height())
+ fn size_hint(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -577,9 +580,10 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- overlay.layout(renderer, bounds, position)
+ overlay.layout(renderer, bounds, position, translation)
})
.unwrap_or_default()
}
diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs
index 8ca9cb86..5dc60d52 100644
--- a/widget/src/lazy/helpers.rs
+++ b/widget/src/lazy/helpers.rs
@@ -6,6 +6,7 @@ use std::hash::Hash;
/// 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")]
pub fn lazy<'a, Message, Renderer, Dependency, View>(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
@@ -19,6 +20,7 @@ where
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
+#[cfg(feature = "lazy")]
pub fn component<'a, C, Message, Renderer>(
component: C,
) -> Element<'a, Message, Renderer>
@@ -37,6 +39,7 @@ where
/// The `view` closure will be provided with the current [`Size`] of
/// the [`Responsive`] widget and, therefore, can be used to build the
/// contents of the widget in a responsive way.
+#[cfg(feature = "lazy")]
pub fn responsive<'a, Message, Renderer>(
f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
) -> Responsive<'a, Message, Renderer>
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index ed471988..1df0866f 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -6,7 +6,8 @@ use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
+ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
+ Widget,
};
use crate::horizontal_space;
use crate::runtime::overlay::Nested;
@@ -134,12 +135,11 @@ where
})
}
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Fill
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fill,
+ height: Length::Fill,
+ }
}
fn layout(
@@ -367,9 +367,10 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- overlay.layout(renderer, bounds, position)
+ overlay.layout(renderer, bounds, position, translation)
})
.unwrap_or_default()
}
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 6feb948c..07378d83 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -35,6 +35,7 @@ pub mod scrollable;
pub mod slider;
pub mod space;
pub mod text;
+pub mod text_editor;
pub mod text_input;
pub mod toggler;
pub mod tooltip;
@@ -86,6 +87,8 @@ pub use space::Space;
#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
+pub use text_editor::TextEditor;
+#[doc(no_inline)]
pub use text_input::TextInput;
#[doc(no_inline)]
pub use toggler::Toggler;
@@ -94,6 +97,13 @@ pub use tooltip::Tooltip;
#[doc(no_inline)]
pub use vertical_slider::VerticalSlider;
+#[cfg(feature = "wgpu")]
+pub mod shader;
+
+#[cfg(feature = "wgpu")]
+#[doc(no_inline)]
+pub use shader::Shader;
+
#[cfg(feature = "svg")]
pub mod svg;
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 3a5b01a3..87cac3a7 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -8,7 +8,7 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{tree, Operation, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Rectangle, Shell, Widget,
+ Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
};
/// Emit messages on mouse events.
@@ -110,12 +110,8 @@ where
tree.diff_children(std::slice::from_ref(&self.content));
}
- fn width(&self) -> Length {
- self.content.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.content.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
}
fn layout(
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index b293f9fa..f83eebea 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -236,6 +236,7 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
let space_below = bounds.height - (position.y + self.target_height);
let space_above = position.y;
@@ -253,15 +254,14 @@ where
)
.width(self.width);
- let mut node = self.container.layout(self.state, renderer, &limits);
+ let node = self.container.layout(self.state, renderer, &limits);
+ let size = node.size();
node.move_to(if space_below > space_above {
position + Vector::new(0.0, self.target_height)
} else {
- position - Vector::new(0.0, node.size().height)
- });
-
- node
+ position - Vector::new(0.0, size.height)
+ })
}
fn on_event(
@@ -342,12 +342,11 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fill,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -358,7 +357,6 @@ where
) -> layout::Node {
use std::f32;
- let limits = limits.width(Length::Fill).height(Length::Shrink);
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
@@ -371,7 +369,7 @@ where
* self.options.len() as f32,
);
- limits.resolve(intrinsic)
+ limits.resolve(Length::Fill, Length::Shrink, intrinsic)
};
layout::Node::new(size)
@@ -543,6 +541,7 @@ where
} else {
appearance.text_color
},
+ *viewport,
);
}
}
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 2d25a543..cf1f0455 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -265,12 +265,11 @@ where
}
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -490,8 +489,7 @@ pub fn layout<Renderer, T>(
&layout::Limits,
) -> layout::Node,
) -> layout::Node {
- let limits = limits.width(width).height(height);
- let size = limits.resolve(Size::ZERO);
+ let size = limits.resolve(width, height, Size::ZERO);
let regions = node.pane_regions(spacing, size);
let children = contents
@@ -500,16 +498,14 @@ pub fn layout<Renderer, T>(
let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height);
- let mut node = layout_content(
+ let node = layout_content(
content,
tree,
renderer,
&layout::Limits::new(size, size),
);
- node.move_to(Point::new(region.x, region.y));
-
- Some(node)
+ Some(node.move_to(Point::new(region.x, region.y)))
})
.collect();
@@ -531,6 +527,8 @@ pub fn update<'a, Message, T: Draggable>(
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
) -> event::Status {
+ const DRAG_DEADBAND_DISTANCE: f32 = 10.0;
+
let mut event_status = event::Status::Ignored;
match event {
@@ -572,7 +570,6 @@ pub fn update<'a, Message, T: Draggable>(
shell,
contents,
on_click,
- on_drag,
);
}
}
@@ -584,7 +581,6 @@ pub fn update<'a, Message, T: Draggable>(
shell,
contents,
on_click,
- on_drag,
);
}
}
@@ -637,7 +633,49 @@ pub fn update<'a, Message, T: Draggable>(
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some((_, on_resize)) = on_resize {
+ if let Some((_, origin)) = action.clicked_pane() {
+ if let Some(on_drag) = &on_drag {
+ let bounds = layout.bounds();
+
+ if let Some(cursor_position) = cursor.position_over(bounds)
+ {
+ let mut clicked_region = contents
+ .zip(layout.children())
+ .filter(|(_, layout)| {
+ layout.bounds().contains(cursor_position)
+ });
+
+ if let Some(((pane, content), layout)) =
+ clicked_region.next()
+ {
+ if content
+ .can_be_dragged_at(layout, cursor_position)
+ {
+ let pane_position = layout.position();
+
+ let new_origin = cursor_position
+ - Vector::new(
+ pane_position.x,
+ pane_position.y,
+ );
+
+ if new_origin.distance(origin)
+ > DRAG_DEADBAND_DISTANCE
+ {
+ *action = state::Action::Dragging {
+ pane,
+ origin,
+ };
+
+ shell.publish(on_drag(DragEvent::Picked {
+ pane,
+ }));
+ }
+ }
+ }
+ }
+ }
+ } else if let Some((_, on_resize)) = on_resize {
if let Some((split, _)) = action.picked_split() {
let bounds = layout.bounds();
@@ -712,7 +750,6 @@ fn click_pane<'a, Message, T>(
shell: &mut Shell<'_, Message>,
contents: impl Iterator<Item = (Pane, T)>,
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
- on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
) where
T: Draggable,
{
@@ -720,23 +757,15 @@ fn click_pane<'a, Message, T>(
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().contains(cursor_position));
- if let Some(((pane, content), layout)) = clicked_region.next() {
+ if let Some(((pane, _), layout)) = clicked_region.next() {
if let Some(on_click) = &on_click {
shell.publish(on_click(pane));
}
- if let Some(on_drag) = &on_drag {
- if content.can_be_dragged_at(layout, cursor_position) {
- let pane_position = layout.position();
-
- let origin = cursor_position
- - Vector::new(pane_position.x, pane_position.y);
-
- *action = state::Action::Dragging { pane, origin };
-
- shell.publish(on_drag(DragEvent::Picked { pane }));
- }
- }
+ let pane_position = layout.position();
+ let origin =
+ cursor_position - Vector::new(pane_position.x, pane_position.y);
+ *action = state::Action::Clicking { pane, origin };
}
}
@@ -749,7 +778,7 @@ pub fn mouse_interaction(
spacing: f32,
resize_leeway: Option<f32>,
) -> Option<mouse::Interaction> {
- if action.picked_pane().is_some() {
+ if action.clicked_pane().is_some() || action.picked_pane().is_some() {
return Some(mouse::Interaction::Grabbing);
}
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index 826ea663..ee00f186 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -165,7 +165,7 @@ where
let title_bar_size = title_bar_layout.size();
- let mut body_layout = self.body.as_widget().layout(
+ let body_layout = self.body.as_widget().layout(
&mut tree.children[0],
renderer,
&layout::Limits::new(
@@ -177,11 +177,12 @@ where
),
);
- body_layout.move_to(Point::new(0.0, title_bar_size.height));
-
layout::Node::with_children(
max_size,
- vec![title_bar_layout, body_layout],
+ vec![
+ title_bar_layout,
+ body_layout.move_to(Point::new(0.0, title_bar_size.height)),
+ ],
)
} else {
self.body.as_widget().layout(
diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs
index 481cd770..5d1fe254 100644
--- a/widget/src/pane_grid/state.rs
+++ b/widget/src/pane_grid/state.rs
@@ -403,6 +403,15 @@ pub enum Action {
///
/// [`PaneGrid`]: super::PaneGrid
Idle,
+ /// A [`Pane`] in the [`PaneGrid`] is being clicked.
+ ///
+ /// [`PaneGrid`]: super::PaneGrid
+ Clicking {
+ /// The [`Pane`] being clicked.
+ pane: Pane,
+ /// The starting [`Point`] of the click interaction.
+ origin: Point,
+ },
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
///
/// [`PaneGrid`]: super::PaneGrid
@@ -432,6 +441,14 @@ impl Action {
}
}
+ /// Returns the current [`Pane`] that is being clicked, if any.
+ pub fn clicked_pane(&self) -> Option<(Pane, Point)> {
+ match *self {
+ Action::Clicking { pane, origin, .. } => Some((pane, origin)),
+ _ => None,
+ }
+ }
+
/// Returns the current [`Split`] that is being dragged, if any.
pub fn picked_split(&self) -> Option<(Split, Axis)> {
match *self {
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index f4dbb6b1..eb21b743 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -217,7 +217,7 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.pad(self.padding);
+ let limits = limits.shrink(self.padding);
let max_size = limits.max();
let title_layout = self.content.as_widget().layout(
@@ -228,8 +228,8 @@ where
let title_size = title_layout.size();
- let mut node = if let Some(controls) = &self.controls {
- let mut controls_layout = controls.as_widget().layout(
+ let node = if let Some(controls) = &self.controls {
+ let controls_layout = controls.as_widget().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
@@ -240,11 +240,13 @@ where
let height = title_size.height.max(controls_size.height);
- controls_layout.move_to(Point::new(space_before_controls, 0.0));
-
layout::Node::with_children(
Size::new(max_size.width, height),
- vec![title_layout, controls_layout],
+ vec![
+ title_layout,
+ controls_layout
+ .move_to(Point::new(space_before_controls, 0.0)),
+ ],
)
} else {
layout::Node::with_children(
@@ -253,9 +255,7 @@ where
)
};
- node.move_to(Point::new(self.padding.left, self.padding.top));
-
- layout::Node::with_children(node.size().pad(self.padding), vec![node])
+ layout::Node::container(node, self.padding)
}
pub(crate) fn operate(
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 27f32907..2e3aab6f 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -45,7 +45,7 @@ where
impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer>
where
- T: ToString + Eq,
+ T: ToString + PartialEq,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet
@@ -145,7 +145,7 @@ where
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
for PickList<'a, T, Message, Renderer>
where
- T: Clone + ToString + Eq + 'static,
+ T: Clone + ToString + PartialEq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
@@ -164,12 +164,11 @@ where
tree::State::new(State::<Renderer::Paragraph>::new())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -235,7 +234,7 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let font = self.font.unwrap_or_else(|| renderer.default_font());
draw(
@@ -253,6 +252,7 @@ where
&self.handle,
&self.style,
|| tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
+ viewport,
);
}
@@ -281,7 +281,7 @@ where
impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- T: Clone + ToString + Eq + 'static,
+ T: Clone + ToString + PartialEq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
@@ -392,7 +392,6 @@ where
{
use std::f32;
- let limits = limits.width(width).height(Length::Shrink).pad(padding);
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
@@ -415,23 +414,17 @@ where
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
- renderer.update_paragraph(
- paragraph,
- Text {
- content: &label,
- ..option_text
- },
- );
+ paragraph.update(Text {
+ content: &label,
+ ..option_text
+ });
}
if let Some(placeholder) = placeholder {
- renderer.update_paragraph(
- &mut state.placeholder,
- Text {
- content: placeholder,
- ..option_text
- },
- );
+ state.placeholder.update(Text {
+ content: placeholder,
+ ..option_text
+ });
}
let max_width = match width {
@@ -456,7 +449,11 @@ where
f32::from(text_line_height.to_absolute(text_size)),
);
- limits.resolve(intrinsic).pad(padding)
+ limits
+ .width(width)
+ .shrink(padding)
+ .resolve(width, Length::Shrink, intrinsic)
+ .expand(padding)
};
layout::Node::new(size)
@@ -637,6 +634,7 @@ pub fn draw<'a, T, Renderer>(
handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State<Renderer::Paragraph>,
+ viewport: &Rectangle,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -721,6 +719,7 @@ pub fn draw<'a, T, Renderer>(
bounds.center_y(),
),
style.handle_color,
+ *viewport,
);
}
@@ -749,6 +748,7 @@ pub fn draw<'a, T, Renderer>(
} else {
style.placeholder_color
},
+ *viewport,
);
}
}
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index 07de72d5..15f1277b 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -85,12 +85,11 @@ where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
+ }
}
fn layout(
@@ -99,13 +98,11 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .width(self.width)
- .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)));
-
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(
+ limits,
+ self.width,
+ self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
+ )
}
fn draw(
diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs
index 1dc4da7f..a229eb59 100644
--- a/widget/src/qr_code.rs
+++ b/widget/src/qr_code.rs
@@ -50,12 +50,11 @@ impl<'a> QRCode<'a> {
}
impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 57acc033..f91b20b1 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -201,12 +201,11 @@ where
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -291,7 +290,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let is_mouse_over = cursor.is_over(layout.bounds());
@@ -349,6 +348,7 @@ where
crate::text::Appearance {
color: custom_style.text_color,
},
+ viewport,
);
}
}
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 7ca90fbb..90fd2926 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -7,7 +7,7 @@ use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell,
- Widget,
+ Size, Widget,
};
/// A container that distributes its contents horizontally.
@@ -21,26 +21,29 @@ pub struct Row<'a, Message, Renderer = crate::Renderer> {
children: Vec<Element<'a, Message, Renderer>>,
}
-impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Row<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
/// Creates an empty [`Row`].
pub fn new() -> Self {
- Self::with_children(Vec::new())
- }
-
- /// Creates a [`Row`] with the given elements.
- pub fn with_children(
- children: Vec<Element<'a, Message, Renderer>>,
- ) -> Self {
Row {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
align_items: Alignment::Start,
- children,
+ children: Vec::new(),
}
}
+ /// Creates a [`Row`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+ ) -> Self {
+ children.into_iter().fold(Self::new(), Self::push)
+ }
+
/// Sets the horizontal spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
@@ -80,12 +83,26 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
mut self,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
- self.children.push(child.into());
+ let child = child.into();
+ let size = child.as_widget().size_hint();
+
+ if size.width.is_fill() {
+ self.width = Length::Fill;
+ }
+
+ if size.height.is_fill() {
+ self.height = Length::Fill;
+ }
+
+ self.children.push(child);
self
}
}
-impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
fn default() -> Self {
Self::new()
}
@@ -104,12 +121,11 @@ where
tree.diff_children(&self.children);
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -118,12 +134,12 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
layout::flex::resolve(
layout::flex::Axis::Horizontal,
renderer,
- &limits,
+ limits,
+ self.width,
+ self.height,
self.padding,
self.spacing,
self.align_items,
@@ -213,15 +229,17 @@ where
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
- for ((child, state), layout) in self
- .children
- .iter()
- .zip(&tree.children)
- .zip(layout.children())
- {
- child
- .as_widget()
- .draw(state, renderer, theme, style, layout, cursor, viewport);
+ if let Some(viewport) = layout.bounds().intersection(viewport) {
+ for ((child, state), layout) in self
+ .children
+ .iter()
+ .zip(&tree.children)
+ .zip(layout.children())
+ {
+ child.as_widget().draw(
+ state, renderer, theme, style, layout, cursor, &viewport,
+ );
+ }
}
}
diff --git a/widget/src/rule.rs b/widget/src/rule.rs
index b5c5fa55..cded9cb1 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -62,12 +62,11 @@ where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -76,9 +75,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
- layout::Node::new(limits.resolve(Size::ZERO))
+ layout::atomic(limits, self.width, self.height)
}
fn draw(
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 49aed2f0..70db490a 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -220,12 +220,11 @@ where
tree.diff_children(std::slice::from_ref(&self.content));
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -470,28 +469,25 @@ pub fn layout<Renderer>(
direction: &Direction,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits.width(width).height(height);
-
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, limits.min().height),
- Size::new(
- if direction.horizontal().is_some() {
- f32::INFINITY
- } else {
- limits.max().width
- },
- if direction.vertical().is_some() {
- f32::MAX
- } else {
- limits.max().height
- },
- ),
- );
-
- let content = layout_content(renderer, &child_limits);
- let size = limits.resolve(content.size());
+ layout::contained(limits, width, height, |limits| {
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, limits.min().height),
+ Size::new(
+ if direction.horizontal().is_some() {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ if direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
+ ),
+ );
- layout::Node::with_children(size, vec![content])
+ layout_content(renderer, &child_limits)
+ })
}
/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`]
diff --git a/widget/src/shader.rs b/widget/src/shader.rs
new file mode 100644
index 00000000..16b68c55
--- /dev/null
+++ b/widget/src/shader.rs
@@ -0,0 +1,216 @@
+//! A custom shader widget for wgpu applications.
+mod event;
+mod program;
+
+pub use event::Event;
+pub use program::Program;
+
+use crate::core;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::{self, Widget};
+use crate::core::window;
+use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size};
+use crate::renderer::wgpu::primitive::pipeline;
+
+use std::marker::PhantomData;
+
+pub use crate::renderer::wgpu::wgpu;
+pub use pipeline::{Primitive, Storage};
+
+/// A widget which can render custom shaders with Iced's `wgpu` backend.
+///
+/// Must be initialized with a [`Program`], which describes the internal widget state & how
+/// its [`Program::Primitive`]s are drawn.
+#[allow(missing_debug_implementations)]
+pub struct Shader<Message, P: Program<Message>> {
+ width: Length,
+ height: Length,
+ program: P,
+ _message: PhantomData<Message>,
+}
+
+impl<Message, P: Program<Message>> Shader<Message, P> {
+ /// Create a new custom [`Shader`].
+ pub fn new(program: P) -> Self {
+ Self {
+ width: Length::Fixed(100.0),
+ height: Length::Fixed(100.0),
+ program,
+ _message: PhantomData,
+ }
+ }
+
+ /// Set the `width` of the custom [`Shader`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Set the `height` of the custom [`Shader`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+}
+
+impl<P, Message, Renderer> Widget<Message, Renderer> for Shader<Message, P>
+where
+ P: Program<Message>,
+ Renderer: pipeline::Renderer,
+{
+ fn tag(&self) -> tree::Tag {
+ struct Tag<T>(T);
+ tree::Tag::of::<Tag<P::State>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(P::State::default())
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ _tree: &mut Tree,
+ _renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ layout::atomic(limits, self.width, self.height)
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: crate::core::Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ let bounds = layout.bounds();
+
+ let custom_shader_event = match event {
+ core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
+ core::Event::Keyboard(keyboard_event) => {
+ Some(Event::Keyboard(keyboard_event))
+ }
+ core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
+ core::Event::Window(_, window::Event::RedrawRequested(instant)) => {
+ Some(Event::RedrawRequested(instant))
+ }
+ _ => None,
+ };
+
+ if let Some(custom_shader_event) = custom_shader_event {
+ let state = tree.state.downcast_mut::<P::State>();
+
+ let (event_status, message) = self.program.update(
+ state,
+ custom_shader_event,
+ bounds,
+ cursor,
+ shell,
+ );
+
+ if let Some(message) = message {
+ shell.publish(message);
+ }
+
+ return event_status;
+ }
+
+ event::Status::Ignored
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ let bounds = layout.bounds();
+ let state = tree.state.downcast_ref::<P::State>();
+
+ self.program.mouse_interaction(state, bounds, cursor)
+ }
+
+ fn draw(
+ &self,
+ tree: &widget::Tree,
+ renderer: &mut Renderer,
+ _theme: &Renderer::Theme,
+ _style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+ let state = tree.state.downcast_ref::<P::State>();
+
+ renderer.draw_pipeline_primitive(
+ bounds,
+ self.program.draw(state, cursor_position, bounds),
+ );
+ }
+}
+
+impl<'a, Message, Renderer, P> From<Shader<Message, P>>
+ for Element<'a, Message, Renderer>
+where
+ Message: 'a,
+ Renderer: pipeline::Renderer,
+ P: Program<Message> + 'a,
+{
+ fn from(custom: Shader<Message, P>) -> Element<'a, Message, Renderer> {
+ Element::new(custom)
+ }
+}
+
+impl<Message, T> Program<Message> for &T
+where
+ T: Program<Message>,
+{
+ type State = T::State;
+ type Primitive = T::Primitive;
+
+ fn update(
+ &self,
+ state: &mut Self::State,
+ event: Event,
+ bounds: Rectangle,
+ cursor: mouse::Cursor,
+ shell: &mut Shell<'_, Message>,
+ ) -> (event::Status, Option<Message>) {
+ T::update(self, state, event, bounds, cursor, shell)
+ }
+
+ fn draw(
+ &self,
+ state: &Self::State,
+ cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive {
+ T::draw(self, state, cursor, bounds)
+ }
+
+ fn mouse_interaction(
+ &self,
+ state: &Self::State,
+ bounds: Rectangle,
+ cursor: mouse::Cursor,
+ ) -> mouse::Interaction {
+ T::mouse_interaction(self, state, bounds, cursor)
+ }
+}
diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs
new file mode 100644
index 00000000..005c8725
--- /dev/null
+++ b/widget/src/shader/event.rs
@@ -0,0 +1,25 @@
+//! Handle events of a custom shader widget.
+use crate::core::keyboard;
+use crate::core::mouse;
+use crate::core::time::Instant;
+use crate::core::touch;
+
+pub use crate::core::event::Status;
+
+/// A [`Shader`] event.
+///
+/// [`Shader`]: crate::Shader
+#[derive(Debug, Clone, PartialEq)]
+pub enum Event {
+ /// A mouse event.
+ Mouse(mouse::Event),
+
+ /// A touch event.
+ Touch(touch::Event),
+
+ /// A keyboard event.
+ Keyboard(keyboard::Event),
+
+ /// A window requested a redraw.
+ RedrawRequested(Instant),
+}
diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs
new file mode 100644
index 00000000..6dd50404
--- /dev/null
+++ b/widget/src/shader/program.rs
@@ -0,0 +1,62 @@
+use crate::core::event;
+use crate::core::mouse;
+use crate::core::{Rectangle, Shell};
+use crate::renderer::wgpu::primitive::pipeline;
+use crate::shader;
+
+/// The state and logic of a [`Shader`] widget.
+///
+/// A [`Program`] can mutate the internal state of a [`Shader`] widget
+/// and produce messages for an application.
+///
+/// [`Shader`]: crate::Shader
+pub trait Program<Message> {
+ /// The internal state of the [`Program`].
+ type State: Default + 'static;
+
+ /// The type of primitive this [`Program`] can draw.
+ type Primitive: pipeline::Primitive + 'static;
+
+ /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
+ /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a
+ /// redraw for the window, etc.
+ ///
+ /// By default, this method does and returns nothing.
+ ///
+ /// [`State`]: Self::State
+ fn update(
+ &self,
+ _state: &mut Self::State,
+ _event: shader::Event,
+ _bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ _shell: &mut Shell<'_, Message>,
+ ) -> (event::Status, Option<Message>) {
+ (event::Status::Ignored, None)
+ }
+
+ /// Draws the [`Primitive`].
+ ///
+ /// [`Primitive`]: Self::Primitive
+ fn draw(
+ &self,
+ state: &Self::State,
+ cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive;
+
+ /// Returns the current mouse interaction of the [`Program`].
+ ///
+ /// The interaction returned will be in effect even if the cursor position is out of
+ /// bounds of the [`Shader`]'s program.
+ ///
+ /// [`Shader`]: crate::Shader
+ fn mouse_interaction(
+ &self,
+ _state: &Self::State,
+ _bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> mouse::Interaction {
+ mouse::Interaction::default()
+ }
+}
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index ac0982c8..1bc94661 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -159,12 +159,11 @@ where
tree::State::new(State::new())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -173,10 +172,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
diff --git a/widget/src/space.rs b/widget/src/space.rs
index e5a8f169..eef990d1 100644
--- a/widget/src/space.rs
+++ b/widget/src/space.rs
@@ -45,12 +45,11 @@ impl<Message, Renderer> Widget<Message, Renderer> for Space
where
Renderer: core::Renderer,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -59,9 +58,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
- layout::Node::new(limits.resolve(Size::ZERO))
+ layout::atomic(limits, self.width, self.height)
}
fn draw(
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index 2d01d1ab..2357cf65 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -96,12 +96,11 @@ where
Renderer: svg::Renderer,
Renderer::Theme: iced_style::svg::StyleSheet,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -115,10 +114,7 @@ where
let image_size = Size::new(width as f32, height as f32);
// The size to be available to the widget prior to `Shrink`ing
- let raw_size = limits
- .width(self.width)
- .height(self.height)
- .resolve(image_size);
+ let raw_size = limits.resolve(self.width, self.height, image_size);
// The uncropped size of the image when fit to the bounds above
let full_size = self.content_fit.fit(image_size, raw_size);
@@ -145,7 +141,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor: mouse::Cursor,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let Size { width, height } = renderer.dimensions(&self.handle);
@@ -153,6 +149,7 @@ where
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
+ let is_mouse_over = cursor.is_over(bounds);
let render = |renderer: &mut Renderer| {
let offset = Vector::new(
@@ -166,7 +163,11 @@ where
..bounds
};
- let appearance = theme.appearance(&self.style);
+ let appearance = if is_mouse_over {
+ theme.hovered(&self.style)
+ } else {
+ theme.appearance(&self.style)
+ };
renderer.draw(
self.handle.clone(),
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
new file mode 100644
index 00000000..09a0cac0
--- /dev/null
+++ b/widget/src/text_editor.rs
@@ -0,0 +1,736 @@
+//! Display a multi-line text input for text editing.
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::keyboard::key;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text::editor::{Cursor, Editor as _};
+use crate::core::text::highlighter::{self, Highlighter};
+use crate::core::text::{self, LineHeight};
+use crate::core::widget::{self, Widget};
+use crate::core::{
+ Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, Size,
+ Vector,
+};
+
+use std::cell::RefCell;
+use std::fmt;
+use std::ops::DerefMut;
+use std::sync::Arc;
+
+pub use crate::style::text_editor::{Appearance, StyleSheet};
+pub use text::editor::{Action, Edit, Motion};
+
+/// A multi-line text input.
+#[allow(missing_debug_implementations)]
+pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer>
+where
+ Highlighter: text::Highlighter,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ content: &'a Content<Renderer>,
+ font: Option<Renderer::Font>,
+ text_size: Option<Pixels>,
+ line_height: LineHeight,
+ width: Length,
+ height: Length,
+ padding: Padding,
+ style: <Renderer::Theme as StyleSheet>::Style,
+ on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
+ highlighter_settings: Highlighter::Settings,
+ highlighter_format: fn(
+ &Highlighter::Highlight,
+ &Renderer::Theme,
+ ) -> highlighter::Format<Renderer::Font>,
+}
+
+impl<'a, Message, Renderer>
+ TextEditor<'a, highlighter::PlainText, Message, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Creates new [`TextEditor`] with the given [`Content`].
+ pub fn new(content: &'a Content<Renderer>) -> Self {
+ Self {
+ content,
+ font: None,
+ text_size: None,
+ line_height: LineHeight::default(),
+ width: Length::Fill,
+ height: Length::Fill,
+ padding: Padding::new(5.0),
+ style: Default::default(),
+ on_edit: None,
+ highlighter_settings: (),
+ highlighter_format: |_highlight, _theme| {
+ highlighter::Format::default()
+ },
+ }
+ }
+}
+
+impl<'a, Highlighter, Message, Renderer>
+ TextEditor<'a, Highlighter, Message, Renderer>
+where
+ Highlighter: text::Highlighter,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Sets the message that should be produced when some action is performed in
+ /// the [`TextEditor`].
+ ///
+ /// If this method is not called, the [`TextEditor`] will be disabled.
+ pub fn on_action(
+ mut self,
+ on_edit: impl Fn(Action) -> Message + 'a,
+ ) -> Self {
+ self.on_edit = Some(Box::new(on_edit));
+ self
+ }
+
+ /// Sets the [`Font`] of the [`TextEditor`].
+ ///
+ /// [`Font`]: text::Renderer::Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the [`Padding`] of the [`TextEditor`].
+ pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+ self.padding = padding.into();
+ self
+ }
+
+ /// Highlights the [`TextEditor`] with the given [`Highlighter`] and
+ /// a strategy to turn its highlights into some text format.
+ pub fn highlight<H: text::Highlighter>(
+ self,
+ settings: H::Settings,
+ to_format: fn(
+ &H::Highlight,
+ &Renderer::Theme,
+ ) -> highlighter::Format<Renderer::Font>,
+ ) -> TextEditor<'a, H, Message, Renderer> {
+ TextEditor {
+ content: self.content,
+ font: self.font,
+ text_size: self.text_size,
+ line_height: self.line_height,
+ width: self.width,
+ height: self.height,
+ padding: self.padding,
+ style: self.style,
+ on_edit: self.on_edit,
+ highlighter_settings: settings,
+ highlighter_format: to_format,
+ }
+ }
+
+ /// Sets the style of the [`TextEditor`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+/// The content of a [`TextEditor`].
+pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
+where
+ R: text::Renderer;
+
+struct Internal<R>
+where
+ R: text::Renderer,
+{
+ editor: R::Editor,
+ is_dirty: bool,
+}
+
+impl<R> Content<R>
+where
+ R: text::Renderer,
+{
+ /// Creates an empty [`Content`].
+ pub fn new() -> Self {
+ Self::with_text("")
+ }
+
+ /// Creates a [`Content`] with the given text.
+ pub fn with_text(text: &str) -> Self {
+ Self(RefCell::new(Internal {
+ editor: R::Editor::with_text(text),
+ is_dirty: true,
+ }))
+ }
+
+ /// Performs an [`Action`] on the [`Content`].
+ pub fn perform(&mut self, action: Action) {
+ let internal = self.0.get_mut();
+
+ internal.editor.perform(action);
+ internal.is_dirty = true;
+ }
+
+ /// Returns the amount of lines of the [`Content`].
+ pub fn line_count(&self) -> usize {
+ self.0.borrow().editor.line_count()
+ }
+
+ /// Returns the text of the line at the given index, if it exists.
+ pub fn line(
+ &self,
+ index: usize,
+ ) -> Option<impl std::ops::Deref<Target = str> + '_> {
+ std::cell::Ref::filter_map(self.0.borrow(), |internal| {
+ internal.editor.line(index)
+ })
+ .ok()
+ }
+
+ /// Returns an iterator of the text of the lines in the [`Content`].
+ pub fn lines(
+ &self,
+ ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> {
+ struct Lines<'a, Renderer: text::Renderer> {
+ internal: std::cell::Ref<'a, Internal<Renderer>>,
+ current: usize,
+ }
+
+ impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> {
+ type Item = std::cell::Ref<'a, str>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let line = std::cell::Ref::filter_map(
+ std::cell::Ref::clone(&self.internal),
+ |internal| internal.editor.line(self.current),
+ )
+ .ok()?;
+
+ self.current += 1;
+
+ Some(line)
+ }
+ }
+
+ Lines {
+ internal: self.0.borrow(),
+ current: 0,
+ }
+ }
+
+ /// Returns the text of the [`Content`].
+ ///
+ /// Lines are joined with `'\n'`.
+ pub fn text(&self) -> String {
+ let mut text = self.lines().enumerate().fold(
+ String::new(),
+ |mut contents, (i, line)| {
+ if i > 0 {
+ contents.push('\n');
+ }
+
+ contents.push_str(&line);
+
+ contents
+ },
+ );
+
+ if !text.ends_with('\n') {
+ text.push('\n');
+ }
+
+ text
+ }
+
+ /// Returns the selected text of the [`Content`].
+ pub fn selection(&self) -> Option<String> {
+ self.0.borrow().editor.selection()
+ }
+
+ /// Returns the current cursor position of the [`Content`].
+ pub fn cursor_position(&self) -> (usize, usize) {
+ self.0.borrow().editor.cursor_position()
+ }
+}
+
+impl<Renderer> Default for Content<Renderer>
+where
+ Renderer: text::Renderer,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<Renderer> fmt::Debug for Content<Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Editor: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let internal = self.0.borrow();
+
+ f.debug_struct("Content")
+ .field("editor", &internal.editor)
+ .field("is_dirty", &internal.is_dirty)
+ .finish()
+ }
+}
+
+struct State<Highlighter: text::Highlighter> {
+ is_focused: bool,
+ last_click: Option<mouse::Click>,
+ drag_click: Option<mouse::click::Kind>,
+ highlighter: RefCell<Highlighter>,
+ highlighter_settings: Highlighter::Settings,
+ highlighter_format_address: usize,
+}
+
+impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer>
+ for TextEditor<'a, Highlighter, Message, Renderer>
+where
+ Highlighter: text::Highlighter,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn tag(&self) -> widget::tree::Tag {
+ widget::tree::Tag::of::<State<Highlighter>>()
+ }
+
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(State {
+ is_focused: false,
+ last_click: None,
+ drag_click: None,
+ highlighter: RefCell::new(Highlighter::new(
+ &self.highlighter_settings,
+ )),
+ highlighter_settings: self.highlighter_settings.clone(),
+ highlighter_format_address: self.highlighter_format as usize,
+ })
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ tree: &mut widget::Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> iced_renderer::core::layout::Node {
+ let mut internal = self.content.0.borrow_mut();
+ let state = tree.state.downcast_mut::<State<Highlighter>>();
+
+ if state.highlighter_format_address != self.highlighter_format as usize
+ {
+ state.highlighter.borrow_mut().change_line(0);
+
+ state.highlighter_format_address = self.highlighter_format as usize;
+ }
+
+ if state.highlighter_settings != self.highlighter_settings {
+ state
+ .highlighter
+ .borrow_mut()
+ .update(&self.highlighter_settings);
+
+ state.highlighter_settings = self.highlighter_settings.clone();
+ }
+
+ 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,
+ state.highlighter.borrow_mut().deref_mut(),
+ );
+
+ layout::Node::new(limits.max())
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut widget::Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ let Some(on_edit) = self.on_edit.as_ref() else {
+ return event::Status::Ignored;
+ };
+
+ let state = tree.state.downcast_mut::<State<Highlighter>>();
+
+ let Some(update) = Update::from_event(
+ event,
+ state,
+ layout.bounds(),
+ self.padding,
+ cursor,
+ ) else {
+ return event::Status::Ignored;
+ };
+
+ match update {
+ Update::Click(click) => {
+ let action = match click.kind() {
+ mouse::click::Kind::Single => {
+ Action::Click(click.position())
+ }
+ mouse::click::Kind::Double => Action::SelectWord,
+ mouse::click::Kind::Triple => Action::SelectLine,
+ };
+
+ state.is_focused = true;
+ state.last_click = Some(click);
+ state.drag_click = Some(click.kind());
+
+ shell.publish(on_edit(action));
+ }
+ Update::Unfocus => {
+ state.is_focused = false;
+ state.drag_click = None;
+ }
+ Update::Release => {
+ state.drag_click = None;
+ }
+ Update::Action(action) => {
+ shell.publish(on_edit(action));
+ }
+ Update::Copy => {
+ if let Some(selection) = self.content.selection() {
+ clipboard.write(selection);
+ }
+ }
+ Update::Paste => {
+ if let Some(contents) = clipboard.read() {
+ shell.publish(on_edit(Action::Edit(Edit::Paste(
+ Arc::new(contents),
+ ))));
+ }
+ }
+ }
+
+ event::Status::Captured
+ }
+
+ fn draw(
+ &self,
+ tree: &widget::Tree,
+ renderer: &mut Renderer,
+ theme: &<Renderer as renderer::Renderer>::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+
+ let mut internal = self.content.0.borrow_mut();
+ let state = tree.state.downcast_ref::<State<Highlighter>>();
+
+ internal.editor.highlight(
+ self.font.unwrap_or_else(|| renderer.default_font()),
+ state.highlighter.borrow_mut().deref_mut(),
+ |highlight| (self.highlighter_format)(highlight, theme),
+ );
+
+ let is_disabled = self.on_edit.is_none();
+ let is_mouse_over = cursor.is_over(bounds);
+
+ let appearance = if is_disabled {
+ theme.disabled(&self.style)
+ } else if state.is_focused {
+ theme.focused(&self.style)
+ } else if is_mouse_over {
+ theme.hovered(&self.style)
+ } else {
+ theme.active(&self.style)
+ };
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds,
+ border_radius: appearance.border_radius,
+ border_width: appearance.border_width,
+ border_color: appearance.border_color,
+ },
+ appearance.background,
+ );
+
+ renderer.fill_editor(
+ &internal.editor,
+ bounds.position()
+ + Vector::new(self.padding.left, self.padding.top),
+ style.text_color,
+ *viewport,
+ );
+
+ let translation = Vector::new(
+ bounds.x + self.padding.left,
+ bounds.y + self.padding.top,
+ );
+
+ if state.is_focused {
+ match internal.editor.cursor() {
+ Cursor::Caret(position) => {
+ let position = position + translation;
+
+ if bounds.contains(position) {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: position.x,
+ y: position.y,
+ width: 1.0,
+ height: self
+ .line_height
+ .to_absolute(
+ self.text_size.unwrap_or_else(
+ || renderer.default_size(),
+ ),
+ )
+ .into(),
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ theme.value_color(&self.style),
+ );
+ }
+ }
+ Cursor::Selection(ranges) => {
+ for range in ranges.into_iter().filter_map(|range| {
+ bounds.intersection(&(range + translation))
+ }) {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: range,
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ theme.selection_color(&self.style),
+ );
+ }
+ }
+ }
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ _state: &widget::Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ let is_disabled = self.on_edit.is_none();
+
+ if cursor.is_over(layout.bounds()) {
+ if is_disabled {
+ mouse::Interaction::NotAllowed
+ } else {
+ mouse::Interaction::Text
+ }
+ } else {
+ mouse::Interaction::default()
+ }
+ }
+}
+
+impl<'a, Highlighter, Message, Renderer>
+ From<TextEditor<'a, Highlighter, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Highlighter: text::Highlighter,
+ Message: 'a,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(
+ text_editor: TextEditor<'a, Highlighter, Message, Renderer>,
+ ) -> Self {
+ Self::new(text_editor)
+ }
+}
+
+enum Update {
+ Click(mouse::Click),
+ Unfocus,
+ Release,
+ Action(Action),
+ Copy,
+ Paste,
+}
+
+impl Update {
+ fn from_event<H: Highlighter>(
+ event: Event,
+ state: &State<H>,
+ bounds: Rectangle,
+ padding: Padding,
+ cursor: mouse::Cursor,
+ ) -> Option<Self> {
+ let action = |action| Some(Update::Action(action));
+ let edit = |edit| action(Action::Edit(edit));
+
+ match event {
+ Event::Mouse(event) => match event {
+ mouse::Event::ButtonPressed(mouse::Button::Left) => {
+ if let Some(cursor_position) = cursor.position_in(bounds) {
+ let cursor_position = cursor_position
+ - Vector::new(padding.top, padding.left);
+
+ let click = mouse::Click::new(
+ cursor_position,
+ state.last_click,
+ );
+
+ Some(Update::Click(click))
+ } else if state.is_focused {
+ Some(Update::Unfocus)
+ } else {
+ None
+ }
+ }
+ mouse::Event::ButtonReleased(mouse::Button::Left) => {
+ Some(Update::Release)
+ }
+ mouse::Event::CursorMoved { .. } => match state.drag_click {
+ Some(mouse::click::Kind::Single) => {
+ let cursor_position = cursor.position_in(bounds)?
+ - Vector::new(padding.top, padding.left);
+
+ action(Action::Drag(cursor_position))
+ }
+ _ => None,
+ },
+ mouse::Event::WheelScrolled { delta }
+ if cursor.is_over(bounds) =>
+ {
+ action(Action::Scroll {
+ lines: match delta {
+ mouse::ScrollDelta::Lines { y, .. } => {
+ if y.abs() > 0.0 {
+ (y.signum() * -(y.abs() * 4.0).max(1.0))
+ as i32
+ } else {
+ 0
+ }
+ }
+ mouse::ScrollDelta::Pixels { y, .. } => {
+ (-y / 4.0) as i32
+ }
+ },
+ })
+ }
+ _ => None,
+ },
+ Event::Keyboard(event) => match event {
+ keyboard::Event::KeyPressed {
+ key,
+ modifiers,
+ text,
+ ..
+ } if state.is_focused => {
+ if let keyboard::Key::Named(named_key) = key.as_ref() {
+ if let Some(motion) = motion(named_key) {
+ let motion = if platform::is_jump_modifier_pressed(
+ modifiers,
+ ) {
+ motion.widen()
+ } else {
+ motion
+ };
+
+ return action(if modifiers.shift() {
+ Action::Select(motion)
+ } else {
+ Action::Move(motion)
+ });
+ }
+ }
+
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Enter) => {
+ edit(Edit::Enter)
+ }
+ keyboard::Key::Named(key::Named::Backspace) => {
+ edit(Edit::Backspace)
+ }
+ keyboard::Key::Named(key::Named::Delete) => {
+ edit(Edit::Delete)
+ }
+ keyboard::Key::Named(key::Named::Escape) => {
+ Some(Self::Unfocus)
+ }
+ keyboard::Key::Character("c")
+ if modifiers.command() =>
+ {
+ Some(Self::Copy)
+ }
+ keyboard::Key::Character("v")
+ if modifiers.command() && !modifiers.alt() =>
+ {
+ Some(Self::Paste)
+ }
+ _ => {
+ let text = text?;
+
+ edit(Edit::Insert(
+ text.chars().next().unwrap_or_default(),
+ ))
+ }
+ }
+ }
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+}
+
+fn motion(key: key::Named) -> Option<Motion> {
+ match key {
+ key::Named::ArrowLeft => Some(Motion::Left),
+ key::Named::ArrowRight => Some(Motion::Right),
+ key::Named::ArrowUp => Some(Motion::Up),
+ key::Named::ArrowDown => Some(Motion::Down),
+ key::Named::Home => Some(Motion::Home),
+ key::Named::End => Some(Motion::End),
+ key::Named::PageUp => Some(Motion::PageUp),
+ key::Named::PageDown => Some(Motion::PageDown),
+ _ => None,
+ }
+}
+
+mod platform {
+ use crate::core::keyboard;
+
+ pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
+ if cfg!(target_os = "macos") {
+ modifiers.alt()
+ } else {
+ modifiers.control()
+ }
+ }
+}
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 9e1fb796..c3dce8be 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -14,6 +14,7 @@ use editor::Editor;
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::keyboard;
+use crate::core::keyboard::key;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
@@ -238,6 +239,7 @@ where
layout: Layout<'_>,
cursor: mouse::Cursor,
value: Option<&Value>,
+ viewport: &Rectangle,
) {
draw(
renderer,
@@ -250,6 +252,7 @@ where
self.is_secure,
self.icon.as_ref(),
&self.style,
+ viewport,
);
}
}
@@ -281,12 +284,11 @@ where
}
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -362,7 +364,7 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
draw(
renderer,
@@ -375,6 +377,7 @@ where
self.is_secure,
self.icon.as_ref(),
&self.style,
+ viewport,
);
}
@@ -503,14 +506,11 @@ where
{
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = size.unwrap_or_else(|| renderer.default_size());
-
let padding = padding.fit(Size::ZERO, limits.max());
- let limits = limits
- .width(width)
- .pad(padding)
- .height(line_height.to_absolute(text_size));
+ let height = line_height.to_absolute(text_size);
- let text_bounds = limits.resolve(Size::ZERO);
+ let limits = limits.width(width).shrink(padding);
+ let text_bounds = limits.resolve(width, height, Size::ZERO);
let placeholder_text = Text {
font,
@@ -523,18 +523,15 @@ where
shaping: text::Shaping::Advanced,
};
- renderer.update_paragraph(&mut state.placeholder, placeholder_text);
+ state.placeholder.update(placeholder_text);
let secure_value = is_secure.then(|| value.secure());
let value = secure_value.as_ref().unwrap_or(value);
- renderer.update_paragraph(
- &mut state.value,
- Text {
- content: &value.to_string(),
- ..placeholder_text
- },
- );
+ state.value.update(Text {
+ content: &value.to_string(),
+ ..placeholder_text
+ });
if let Some(icon) = icon {
let icon_text = Text {
@@ -548,45 +545,45 @@ where
shaping: text::Shaping::Advanced,
};
- renderer.update_paragraph(&mut state.icon, icon_text);
+ state.icon.update(icon_text);
let icon_width = state.icon.min_width();
- let mut text_node = layout::Node::new(
- text_bounds - Size::new(icon_width + icon.spacing, 0.0),
- );
-
- let mut icon_node =
- layout::Node::new(Size::new(icon_width, text_bounds.height));
-
- match icon.side {
- Side::Left => {
- text_node.move_to(Point::new(
+ let (text_position, icon_position) = match icon.side {
+ Side::Left => (
+ Point::new(
padding.left + icon_width + icon.spacing,
padding.top,
- ));
-
- icon_node.move_to(Point::new(padding.left, padding.top));
- }
- Side::Right => {
- text_node.move_to(Point::new(padding.left, padding.top));
-
- icon_node.move_to(Point::new(
+ ),
+ Point::new(padding.left, padding.top),
+ ),
+ Side::Right => (
+ Point::new(padding.left, padding.top),
+ Point::new(
padding.left + text_bounds.width - icon_width,
padding.top,
- ));
- }
+ ),
+ ),
};
+ let text_node = layout::Node::new(
+ text_bounds - Size::new(icon_width + icon.spacing, 0.0),
+ )
+ .move_to(text_position);
+
+ let icon_node =
+ layout::Node::new(Size::new(icon_width, text_bounds.height))
+ .move_to(icon_position);
+
layout::Node::with_children(
- text_bounds.pad(padding),
+ text_bounds.expand(padding),
vec![text_node, icon_node],
)
} else {
- let mut text = layout::Node::new(text_bounds);
- text.move_to(Point::new(padding.left, padding.top));
+ let text = layout::Node::new(text_bounds)
+ .move_to(Point::new(padding.left, padding.top));
- layout::Node::with_children(text_bounds.pad(padding), vec![text])
+ layout::Node::with_children(text_bounds.expand(padding), vec![text])
}
}
@@ -752,34 +749,7 @@ where
return event::Status::Captured;
}
}
- Event::Keyboard(keyboard::Event::CharacterReceived(c)) => {
- let state = state();
-
- if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = on_input else {
- return event::Status::Ignored;
- };
-
- if state.is_pasting.is_none()
- && !state.keyboard_modifiers.command()
- && !c.is_control()
- {
- let mut editor = Editor::new(value, &mut state.cursor);
-
- editor.insert(c);
-
- let message = (on_input)(editor.contents());
- shell.publish(message);
-
- focus.updated_at = Instant::now();
-
- update_cache(state, value);
-
- return event::Status::Captured;
- }
- }
- }
- Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => {
+ Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@@ -790,14 +760,13 @@ where
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
- match key_code {
- keyboard::KeyCode::Enter
- | keyboard::KeyCode::NumpadEnter => {
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Enter) => {
if let Some(on_submit) = on_submit.clone() {
shell.publish(on_submit);
}
}
- keyboard::KeyCode::Backspace => {
+ keyboard::Key::Named(key::Named::Backspace) => {
if platform::is_jump_modifier_pressed(modifiers)
&& state.cursor.selection(value).is_none()
{
@@ -817,7 +786,7 @@ where
update_cache(state, value);
}
- keyboard::KeyCode::Delete => {
+ keyboard::Key::Named(key::Named::Delete) => {
if platform::is_jump_modifier_pressed(modifiers)
&& state.cursor.selection(value).is_none()
{
@@ -839,7 +808,7 @@ where
update_cache(state, value);
}
- keyboard::KeyCode::Left => {
+ keyboard::Key::Named(key::Named::ArrowLeft) => {
if platform::is_jump_modifier_pressed(modifiers)
&& !is_secure
{
@@ -854,7 +823,7 @@ where
state.cursor.move_left(value);
}
}
- keyboard::KeyCode::Right => {
+ keyboard::Key::Named(key::Named::ArrowRight) => {
if platform::is_jump_modifier_pressed(modifiers)
&& !is_secure
{
@@ -869,7 +838,7 @@ where
state.cursor.move_right(value);
}
}
- keyboard::KeyCode::Home => {
+ keyboard::Key::Named(key::Named::Home) => {
if modifiers.shift() {
state
.cursor
@@ -878,7 +847,7 @@ where
state.cursor.move_to(0);
}
}
- keyboard::KeyCode::End => {
+ keyboard::Key::Named(key::Named::End) => {
if modifiers.shift() {
state.cursor.select_range(
state.cursor.start(value),
@@ -888,7 +857,7 @@ where
state.cursor.move_to(value.len());
}
}
- keyboard::KeyCode::C
+ keyboard::Key::Character("c")
if state.keyboard_modifiers.command() =>
{
if let Some((start, end)) =
@@ -898,7 +867,7 @@ where
.write(value.select(start, end).to_string());
}
}
- keyboard::KeyCode::X
+ keyboard::Key::Character("x")
if state.keyboard_modifiers.command() =>
{
if let Some((start, end)) =
@@ -916,7 +885,7 @@ where
update_cache(state, value);
}
- keyboard::KeyCode::V => {
+ keyboard::Key::Character("v") => {
if state.keyboard_modifiers.command()
&& !state.keyboard_modifiers.alt()
{
@@ -953,12 +922,12 @@ where
state.is_pasting = None;
}
}
- keyboard::KeyCode::A
+ keyboard::Key::Character("a")
if state.keyboard_modifiers.command() =>
{
state.cursor.select_all(value);
}
- keyboard::KeyCode::Escape => {
+ keyboard::Key::Named(key::Named::Escape) => {
state.is_focused = None;
state.is_dragging = false;
state.is_pasting = None;
@@ -966,28 +935,55 @@ where
state.keyboard_modifiers =
keyboard::Modifiers::default();
}
- keyboard::KeyCode::Tab
- | keyboard::KeyCode::Up
- | keyboard::KeyCode::Down => {
+ keyboard::Key::Named(
+ key::Named::Tab
+ | key::Named::ArrowUp
+ | key::Named::ArrowDown,
+ ) => {
return event::Status::Ignored;
}
- _ => {}
+ _ => {
+ if let Some(text) = text {
+ let c = text.chars().next().unwrap_or_default();
+
+ if state.is_pasting.is_none()
+ && !state.keyboard_modifiers.command()
+ && !c.is_control()
+ {
+ let mut editor =
+ Editor::new(value, &mut state.cursor);
+
+ editor.insert(c);
+
+ let message = (on_input)(editor.contents());
+ shell.publish(message);
+
+ focus.updated_at = Instant::now();
+
+ update_cache(state, value);
+
+ return event::Status::Captured;
+ }
+ }
+ }
}
return event::Status::Captured;
}
}
- Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => {
+ Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
let state = state();
if state.is_focused.is_some() {
- match key_code {
- keyboard::KeyCode::V => {
+ match key.as_ref() {
+ keyboard::Key::Character("v") => {
state.is_pasting = None;
}
- keyboard::KeyCode::Tab
- | keyboard::KeyCode::Up
- | keyboard::KeyCode::Down => {
+ keyboard::Key::Named(
+ key::Named::Tab
+ | key::Named::ArrowUp
+ | key::Named::ArrowDown,
+ ) => {
return event::Status::Ignored;
}
_ => {}
@@ -1003,14 +999,14 @@ where
state.keyboard_modifiers = modifiers;
}
- Event::Window(window::Event::Unfocused) => {
+ Event::Window(_, window::Event::Unfocused) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
focus.is_window_focused = false;
}
}
- Event::Window(window::Event::Focused) => {
+ Event::Window(_, window::Event::Focused) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@@ -1020,7 +1016,7 @@ where
shell.request_redraw(window::RedrawRequest::NextFrame);
}
}
- Event::Window(window::Event::RedrawRequested(now)) => {
+ Event::Window(_, window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@@ -1058,6 +1054,7 @@ pub fn draw<Renderer>(
is_secure: bool,
icon: Option<&Icon<Renderer::Font>>,
style: &<Renderer::Theme as StyleSheet>::Style,
+ viewport: &Rectangle,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -1099,6 +1096,7 @@ pub fn draw<Renderer>(
&state.icon,
icon_layout.bounds().center(),
appearance.icon_color,
+ *viewport,
);
}
@@ -1192,11 +1190,11 @@ pub fn draw<Renderer>(
(None, 0.0)
};
- let text_width = state.value.min_width();
-
- let render = |renderer: &mut Renderer| {
+ let draw = |renderer: &mut Renderer, viewport| {
if let Some((cursor, color)) = cursor {
- renderer.fill_quad(cursor, color);
+ renderer.with_translation(Vector::new(-offset, 0.0), |renderer| {
+ renderer.fill_quad(cursor, color);
+ });
} else {
renderer.with_translation(Vector::ZERO, |_| {});
}
@@ -1207,7 +1205,8 @@ pub fn draw<Renderer>(
} else {
&state.value
},
- Point::new(text_bounds.x, text_bounds.center_y()),
+ Point::new(text_bounds.x, text_bounds.center_y())
+ - Vector::new(offset, 0.0),
if text.is_empty() {
theme.placeholder_color(style)
} else if is_disabled {
@@ -1215,15 +1214,14 @@ pub fn draw<Renderer>(
} else {
theme.value_color(style)
},
+ viewport,
);
};
- if text_width > text_bounds.width {
- renderer.with_layer(text_bounds, |renderer| {
- renderer.with_translation(Vector::new(-offset, 0.0), render);
- });
+ if cursor.is_some() {
+ renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport));
} else {
- render(renderer);
+ draw(renderer, text_bounds);
}
}
@@ -1461,7 +1459,7 @@ fn replace_paragraph<Renderer>(
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
- state.value = renderer.create_paragraph(Text {
+ state.value = Renderer::Paragraph::with_text(Text {
font,
line_height,
content: &value.to_string(),
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index 476c8330..941159ea 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -168,12 +168,11 @@ where
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -266,7 +265,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
/// Makes sure that the border radius of the toggler looks good at every size.
const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
@@ -287,6 +286,7 @@ where
label_layout,
tree.state.downcast_ref(),
crate::text::Appearance::default(),
+ viewport,
);
}
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index b041d2e9..d09a9255 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -64,6 +64,12 @@ where
self
}
+ /// Sets the [`text::Shaping`] strategy of the [`Tooltip`].
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.tooltip = self.tooltip.shaping(shaping);
+ self
+ }
+
/// Sets the font of the [`Tooltip`].
///
/// [`Font`]: Renderer::Font
@@ -125,12 +131,8 @@ where
widget::tree::Tag::of::<State>()
}
- fn width(&self) -> Length {
- self.content.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.content.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
}
fn layout(
@@ -157,11 +159,19 @@ where
) -> event::Status {
let state = tree.state.downcast_mut::<State>();
+ let was_idle = *state == State::Idle;
+
*state = cursor
.position_over(layout.bounds())
.map(|cursor_position| State::Hovered { cursor_position })
.unwrap_or_default();
+ let is_idle = *state == State::Idle;
+
+ if was_idle != is_idle {
+ shell.invalidate_layout();
+ }
+
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
@@ -289,7 +299,7 @@ pub enum Position {
Right,
}
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
enum State {
#[default]
Idle,
@@ -325,6 +335,7 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
let viewport = Rectangle::with_size(bounds);
@@ -338,7 +349,7 @@ where
.then(|| viewport.size())
.unwrap_or(Size::INFINITY),
)
- .pad(Padding::new(self.padding)),
+ .shrink(Padding::new(self.padding)),
);
let text_bounds = text_layout.bounds();
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index 01d3359c..a3029d76 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -156,12 +156,11 @@ where
tree::State::new(State::new())
}
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: self.height,
+ }
}
fn layout(
@@ -170,10 +169,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(