summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
Diffstat (limited to 'widget')
-rw-r--r--widget/Cargo.toml47
-rw-r--r--widget/src/button.rs35
-rw-r--r--widget/src/canvas.rs2
-rw-r--r--widget/src/canvas/event.rs2
-rw-r--r--widget/src/canvas/program.rs11
-rw-r--r--widget/src/checkbox.rs112
-rw-r--r--widget/src/column.rs8
-rw-r--r--widget/src/combo_box.rs770
-rw-r--r--widget/src/container.rs133
-rw-r--r--widget/src/helpers.rs107
-rw-r--r--widget/src/image.rs32
-rw-r--r--widget/src/image/viewer.rs11
-rw-r--r--widget/src/keyed.rs53
-rw-r--r--widget/src/keyed/column.rs320
-rw-r--r--widget/src/lazy.rs20
-rw-r--r--widget/src/lazy/component.rs28
-rw-r--r--widget/src/lazy/responsive.rs31
-rw-r--r--widget/src/lib.rs23
-rw-r--r--widget/src/mouse_area.rs7
-rw-r--r--widget/src/overlay/menu.rs86
-rw-r--r--widget/src/pane_grid.rs57
-rw-r--r--widget/src/pane_grid/configuration.rs4
-rw-r--r--widget/src/pane_grid/content.rs22
-rw-r--r--widget/src/pane_grid/node.rs16
-rw-r--r--widget/src/pane_grid/pane.rs2
-rw-r--r--widget/src/pane_grid/split.rs2
-rw-r--r--widget/src/pane_grid/state.rs110
-rw-r--r--widget/src/pane_grid/title_bar.rs31
-rw-r--r--widget/src/pick_list.rs196
-rw-r--r--widget/src/progress_bar.rs1
-rw-r--r--widget/src/qr_code.rs3
-rw-r--r--widget/src/radio.rs75
-rw-r--r--widget/src/row.rs10
-rw-r--r--widget/src/rule.rs1
-rw-r--r--widget/src/scrollable.rs113
-rw-r--r--widget/src/shader.rs220
-rw-r--r--widget/src/shader/event.rs25
-rw-r--r--widget/src/shader/program.rs62
-rw-r--r--widget/src/slider.rs8
-rw-r--r--widget/src/space.rs1
-rw-r--r--widget/src/svg.rs1
-rw-r--r--widget/src/text_editor.rs708
-rw-r--r--widget/src/text_input.rs432
-rw-r--r--widget/src/text_input/cursor.rs26
-rw-r--r--widget/src/text_input/value.rs13
-rw-r--r--widget/src/toggler.rs99
-rw-r--r--widget/src/tooltip.rs70
-rw-r--r--widget/src/vertical_slider.rs4
48 files changed, 3405 insertions, 745 deletions
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 14aae72e..e8e363c4 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -1,7 +1,18 @@
[package]
name = "iced_widget"
-version = "0.1.0"
-edition = "2021"
+description = "The built-in widgets for iced"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+categories.workspace = true
+keywords.workspace = true
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+all-features = true
[features]
lazy = ["ouroboros"]
@@ -9,29 +20,19 @@ image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
qr_code = ["canvas", "qrcode"]
+wgpu = ["iced_renderer/wgpu"]
[dependencies]
-unicode-segmentation = "1.6"
-num-traits = "0.2"
-thiserror = "1"
-
-[dependencies.iced_runtime]
-version = "0.1"
-path = "../runtime"
-
-[dependencies.iced_renderer]
-version = "0.1"
-path = "../renderer"
+iced_renderer.workspace = true
+iced_runtime.workspace = true
+iced_style.workspace = true
-[dependencies.iced_style]
-version = "0.8"
-path = "../style"
+num-traits.workspace = true
+thiserror.workspace = true
+unicode-segmentation.workspace = true
-[dependencies.ouroboros]
-version = "0.17"
-optional = true
+ouroboros.workspace = true
+ouroboros.optional = true
-[dependencies.qrcode]
-version = "0.12"
-optional = true
-default-features = false
+qrcode.workspace = true
+qrcode.optional = true
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 8ebc9657..384a3156 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -119,9 +119,9 @@ where
/// Sets the style variant of this [`Button`].
pub fn style(
mut self,
- style: <Renderer::Theme as StyleSheet>::Style,
+ style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
) -> Self {
- self.style = style;
+ self.style = style.into();
self
}
}
@@ -146,7 +146,7 @@ where
}
fn diff(&self, tree: &mut Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ tree.diff_children(std::slice::from_ref(&self.content));
}
fn width(&self) -> Length {
@@ -159,19 +159,17 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout(
- renderer,
- limits,
- self.width,
- self.height,
- self.padding,
- |renderer, limits| {
- self.content.as_widget().layout(renderer, limits)
- },
- )
+ layout(limits, self.width, self.height, self.padding, |limits| {
+ self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ limits,
+ )
+ })
}
fn operate(
@@ -181,7 +179,7 @@ where
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
- operation.container(None, &mut |operation| {
+ operation.container(None, layout.bounds(), &mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
@@ -200,6 +198,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
if let event::Status::Captured = self.content.as_widget_mut().on_event(
&mut tree.children[0],
@@ -209,6 +208,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
) {
return event::Status::Captured;
}
@@ -424,17 +424,16 @@ where
}
/// Computes the layout of a [`Button`].
-pub fn layout<Renderer>(
- renderer: &Renderer,
+pub fn layout(
limits: &layout::Limits,
width: Length,
height: Length,
padding: Padding,
- layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
+ layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
- let mut content = layout_content(renderer, &limits.pad(padding));
+ 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);
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 96062038..390f4d92 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -129,6 +129,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -147,6 +148,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
let bounds = layout.bounds();
diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs
index 4508c184..1288365f 100644
--- a/widget/src/canvas/event.rs
+++ b/widget/src/canvas/event.rs
@@ -7,7 +7,7 @@ pub use crate::core::event::Status;
/// A [`Canvas`] event.
///
-/// [`Canvas`]: crate::widget::Canvas
+/// [`Canvas`]: crate::Canvas
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Event {
/// A mouse event.
diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs
index b3f6175e..2ac23061 100644
--- a/widget/src/canvas/program.rs
+++ b/widget/src/canvas/program.rs
@@ -8,7 +8,7 @@ use crate::graphics::geometry;
/// A [`Program`] can mutate internal state and produce messages for an
/// application.
///
-/// [`Canvas`]: crate::widget::Canvas
+/// [`Canvas`]: crate::Canvas
pub trait Program<Message, Renderer = crate::Renderer>
where
Renderer: geometry::Renderer,
@@ -26,7 +26,7 @@ where
///
/// By default, this method does and returns nothing.
///
- /// [`Canvas`]: crate::widget::Canvas
+ /// [`Canvas`]: crate::Canvas
fn update(
&self,
_state: &mut Self::State,
@@ -42,8 +42,9 @@ where
/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
/// [`Cache`].
///
- /// [`Frame`]: crate::widget::canvas::Frame
- /// [`Cache`]: crate::widget::canvas::Cache
+ /// [`Geometry`]: crate::canvas::Geometry
+ /// [`Frame`]: crate::canvas::Frame
+ /// [`Cache`]: crate::canvas::Cache
fn draw(
&self,
state: &Self::State,
@@ -58,7 +59,7 @@ where
/// The interaction returned will be in effect even if the cursor position
/// is out of bounds of the program's [`Canvas`].
///
- /// [`Canvas`]: crate::widget::Canvas
+ /// [`Canvas`]: crate::Canvas
fn mouse_interaction(
&self,
_state: &Self::State,
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index aa0bff42..d7fdf339 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -6,12 +6,11 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
-use crate::core::widget::Tree;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell,
- Widget,
+ Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget,
};
-use crate::{Row, Text};
pub use iced_style::checkbox::{Appearance, StyleSheet};
@@ -45,7 +44,7 @@ where
width: Length,
size: f32,
spacing: f32,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -62,7 +61,7 @@ where
const DEFAULT_SIZE: f32 = 20.0;
/// The default spacing of a [`Checkbox`].
- const DEFAULT_SPACING: f32 = 15.0;
+ const DEFAULT_SPACING: f32 = 10.0;
/// Creates a new [`Checkbox`].
///
@@ -118,11 +117,11 @@ where
/// Sets the text size of the [`Checkbox`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
- /// Sets the text [`LineHeight`] of the [`Checkbox`].
+ /// Sets the text [`text::LineHeight`] of the [`Checkbox`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
@@ -137,9 +136,9 @@ where
self
}
- /// Sets the [`Font`] of the text of the [`Checkbox`].
+ /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`].
///
- /// [`Font`]: crate::text::Renderer::Font
+ /// [`Renderer::Font`]: crate::core::text::Renderer
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
@@ -167,6 +166,14 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -177,26 +184,35 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- Row::<(), Renderer>::new()
- .width(self.width)
- .spacing(self.spacing)
- .align_items(Alignment::Center)
- .push(Row::new().width(self.size).height(self.size))
- .push(
- Text::new(&self.label)
- .font(self.font.unwrap_or_else(|| renderer.default_font()))
- .width(self.width)
- .size(
- self.text_size
- .unwrap_or_else(|| renderer.default_size()),
- )
- .line_height(self.text_line_height)
- .shaping(self.text_shaping),
- )
- .layout(renderer, limits)
+ layout::next_to_each_other(
+ &limits.width(self.width),
+ self.spacing,
+ |_| layout::Node::new(Size::new(self.size, self.size)),
+ |limits| {
+ let state = tree
+ .state
+ .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
+
+ widget::text::layout(
+ state,
+ renderer,
+ limits,
+ self.width,
+ Length::Shrink,
+ &self.label,
+ self.text_line_height,
+ self.text_size,
+ self.font,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ self.text_shaping,
+ )
+ },
+ )
}
fn on_event(
@@ -208,6 +224,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
@@ -243,7 +260,7 @@ where
fn draw(
&self,
- _tree: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -282,24 +299,23 @@ where
line_height,
shaping,
} = &self.icon;
- let size = size.unwrap_or(bounds.height * 0.7);
+ let size = size.unwrap_or(Pixels(bounds.height * 0.7));
if self.is_checked {
- renderer.fill_text(text::Text {
- content: &code_point.to_string(),
- font: *font,
- size,
- line_height: *line_height,
- bounds: Rectangle {
- x: bounds.center_x(),
- y: bounds.center_y(),
- ..bounds
+ renderer.fill_text(
+ text::Text {
+ content: &code_point.to_string(),
+ font: *font,
+ size,
+ line_height: *line_height,
+ bounds: bounds.size(),
+ horizontal_alignment: alignment::Horizontal::Center,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: *shaping,
},
- color: custom_style.icon_color,
- horizontal_alignment: alignment::Horizontal::Center,
- vertical_alignment: alignment::Vertical::Center,
- shaping: *shaping,
- });
+ bounds.center(),
+ custom_style.icon_color,
+ );
}
}
@@ -310,16 +326,10 @@ where
renderer,
style,
label_layout,
- &self.label,
- self.text_size,
- self.text_line_height,
- self.font,
+ tree.state.downcast_ref(),
crate::text::Appearance {
color: custom_style.text_color,
},
- alignment::Horizontal::Left,
- alignment::Vertical::Center,
- self.text_shaping,
);
}
}
@@ -347,7 +357,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
- pub size: Option<f32>,
+ pub size: Option<Pixels>,
/// The line height of the icon.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
diff --git a/widget/src/column.rs b/widget/src/column.rs
index d92d794b..42e90ac1 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -122,6 +122,7 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -138,6 +139,7 @@ where
self.spacing,
self.align_items,
&self.children,
+ &mut tree.children,
)
}
@@ -148,7 +150,7 @@ where
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
- operation.container(None, &mut |operation| {
+ operation.container(None, layout.bounds(), &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)
@@ -157,7 +159,7 @@ where
child
.as_widget()
.operate(state, layout, renderer, operation);
- })
+ });
});
}
@@ -170,6 +172,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
self.children
.iter_mut()
@@ -184,6 +187,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
new file mode 100644
index 00000000..768c2402
--- /dev/null
+++ b/widget/src/combo_box.rs
@@ -0,0 +1,770 @@
+//! Display a dropdown list of searchable and selectable options.
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+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::overlay::menu;
+use crate::text::LineHeight;
+use crate::{container, scrollable, text_input, TextInput};
+
+use std::cell::RefCell;
+use std::fmt::Display;
+
+/// A widget for searching and selecting a single value from a list of options.
+///
+/// This widget is composed by a [`TextInput`] that can be filled with the text
+/// to search for corresponding values from the list of options that are displayed
+/// as a Menu.
+#[allow(missing_debug_implementations)]
+pub struct ComboBox<'a, T, Message, Renderer = crate::Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: text_input::StyleSheet + menu::StyleSheet,
+{
+ state: &'a State<T>,
+ text_input: TextInput<'a, TextInputEvent, Renderer>,
+ font: Option<Renderer::Font>,
+ selection: text_input::Value,
+ on_selected: Box<dyn Fn(T) -> Message>,
+ on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
+ on_close: Option<Message>,
+ on_input: Option<Box<dyn Fn(String) -> Message>>,
+ menu_style: <Renderer::Theme as menu::StyleSheet>::Style,
+ padding: Padding,
+ size: Option<f32>,
+}
+
+impl<'a, T, Message, Renderer> ComboBox<'a, T, Message, Renderer>
+where
+ T: std::fmt::Display + Clone,
+ Renderer: text::Renderer,
+ Renderer::Theme: text_input::StyleSheet + menu::StyleSheet,
+{
+ /// Creates a new [`ComboBox`] with the given list of options, a placeholder,
+ /// the current selected value, and the message to produce when an option is
+ /// selected.
+ pub fn new(
+ state: &'a State<T>,
+ placeholder: &str,
+ selection: Option<&T>,
+ on_selected: impl Fn(T) -> Message + 'static,
+ ) -> Self {
+ let text_input = TextInput::new(placeholder, &state.value())
+ .on_input(TextInputEvent::TextChanged);
+
+ let selection = selection.map(T::to_string).unwrap_or_default();
+
+ Self {
+ state,
+ text_input,
+ font: None,
+ selection: text_input::Value::new(&selection),
+ on_selected: Box::new(on_selected),
+ on_option_hovered: None,
+ on_input: None,
+ on_close: None,
+ menu_style: Default::default(),
+ padding: text_input::DEFAULT_PADDING,
+ size: None,
+ }
+ }
+
+ /// Sets the message that should be produced when some text is typed into
+ /// the [`TextInput`] of the [`ComboBox`].
+ pub fn on_input(
+ mut self,
+ on_input: impl Fn(String) -> Message + 'static,
+ ) -> Self {
+ self.on_input = Some(Box::new(on_input));
+ self
+ }
+
+ /// Sets the message that will be produced when an option of the
+ /// [`ComboBox`] is hovered using the arrow keys.
+ pub fn on_option_hovered(
+ mut self,
+ on_option_hovered: impl Fn(T) -> Message + 'static,
+ ) -> Self {
+ self.on_option_hovered = Some(Box::new(on_option_hovered));
+ self
+ }
+
+ /// Sets the message that will be produced when the outside area
+ /// of the [`ComboBox`] is pressed.
+ pub fn on_close(mut self, message: Message) -> Self {
+ self.on_close = Some(message);
+ self
+ }
+
+ /// Sets the [`Padding`] of the [`ComboBox`].
+ pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+ self.padding = padding.into();
+ self.text_input = self.text_input.padding(self.padding);
+ self
+ }
+
+ /// Sets the style of the [`ComboBox`].
+ // TODO: Define its own `StyleSheet` trait
+ pub fn style<S>(mut self, style: S) -> Self
+ where
+ S: Into<<Renderer::Theme as text_input::StyleSheet>::Style>
+ + Into<<Renderer::Theme as menu::StyleSheet>::Style>
+ + Clone,
+ {
+ self.menu_style = style.clone().into();
+ self.text_input = self.text_input.style(style);
+ self
+ }
+
+ /// Sets the style of the [`TextInput`] of the [`ComboBox`].
+ pub fn text_input_style<S>(mut self, style: S) -> Self
+ where
+ S: Into<<Renderer::Theme as text_input::StyleSheet>::Style> + Clone,
+ {
+ self.text_input = self.text_input.style(style);
+ self
+ }
+
+ /// Sets the [`Renderer::Font`] of the [`ComboBox`].
+ ///
+ /// [`Renderer::Font`]: text::Renderer
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.text_input = self.text_input.font(font);
+ self.font = Some(font);
+ self
+ }
+
+ /// Sets the [`text_input::Icon`] of the [`ComboBox`].
+ pub fn icon(mut self, icon: text_input::Icon<Renderer::Font>) -> Self {
+ self.text_input = self.text_input.icon(icon);
+ self
+ }
+
+ /// Sets the text sixe of the [`ComboBox`].
+ pub fn size(mut self, size: f32) -> Self {
+ self.text_input = self.text_input.size(size);
+ self.size = Some(size);
+ self
+ }
+
+ /// Sets the [`LineHeight`] of the [`ComboBox`].
+ pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
+ Self {
+ text_input: self.text_input.line_height(line_height),
+ ..self
+ }
+ }
+
+ /// Sets the width of the [`ComboBox`].
+ pub fn width(self, width: impl Into<Length>) -> Self {
+ Self {
+ text_input: self.text_input.width(width),
+ ..self
+ }
+ }
+}
+
+/// The local state of a [`ComboBox`].
+#[derive(Debug, Clone)]
+pub struct State<T>(RefCell<Inner<T>>);
+
+#[derive(Debug, Clone)]
+struct Inner<T> {
+ value: String,
+ options: Vec<T>,
+ option_matchers: Vec<String>,
+ filtered_options: Filtered<T>,
+}
+
+#[derive(Debug, Clone)]
+struct Filtered<T> {
+ options: Vec<T>,
+ updated: Instant,
+}
+
+impl<T> State<T>
+where
+ T: Display + Clone,
+{
+ /// Creates a new [`State`] for a [`ComboBox`] with the given list of options.
+ pub fn new(options: Vec<T>) -> Self {
+ Self::with_selection(options, None)
+ }
+
+ /// Creates a new [`State`] for a [`ComboBox`] with the given list of options
+ /// and selected value.
+ pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
+ let value = selection.map(T::to_string).unwrap_or_default();
+
+ // Pre-build "matcher" strings ahead of time so that search is fast
+ let option_matchers = build_matchers(&options);
+
+ let filtered_options = Filtered::new(
+ search(&options, &option_matchers, &value)
+ .cloned()
+ .collect(),
+ );
+
+ Self(RefCell::new(Inner {
+ value,
+ options,
+ option_matchers,
+ filtered_options,
+ }))
+ }
+
+ fn value(&self) -> String {
+ let inner = self.0.borrow();
+
+ inner.value.clone()
+ }
+
+ fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
+ let inner = self.0.borrow();
+
+ f(&inner)
+ }
+
+ fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
+ let mut inner = self.0.borrow_mut();
+
+ f(&mut inner);
+ }
+
+ fn sync_filtered_options(&self, options: &mut Filtered<T>) {
+ let inner = self.0.borrow();
+
+ inner.filtered_options.sync(options);
+ }
+}
+
+impl<T> Filtered<T>
+where
+ T: Clone,
+{
+ fn new(options: Vec<T>) -> Self {
+ Self {
+ options,
+ updated: Instant::now(),
+ }
+ }
+
+ fn empty() -> Self {
+ Self {
+ options: vec![],
+ updated: Instant::now(),
+ }
+ }
+
+ fn update(&mut self, options: Vec<T>) {
+ self.options = options;
+ self.updated = Instant::now();
+ }
+
+ fn sync(&self, other: &mut Filtered<T>) {
+ if other.updated != self.updated {
+ *other = self.clone();
+ }
+ }
+}
+
+struct Menu<T> {
+ menu: menu::State,
+ hovered_option: Option<usize>,
+ new_selection: Option<T>,
+ filtered_options: Filtered<T>,
+}
+
+#[derive(Debug, Clone)]
+enum TextInputEvent {
+ TextChanged(String),
+}
+
+impl<'a, T, Message, Renderer> Widget<Message, Renderer>
+ for ComboBox<'a, T, Message, Renderer>
+where
+ T: Display + Clone + 'static,
+ Message: Clone,
+ Renderer: text::Renderer,
+ Renderer::Theme: container::StyleSheet
+ + text_input::StyleSheet
+ + 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 layout(
+ &self,
+ tree: &mut widget::Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ self.text_input.layout(
+ &mut tree.children[0],
+ renderer,
+ limits,
+ (!is_focused).then_some(&self.selection),
+ )
+ }
+
+ fn tag(&self) -> widget::tree::Tag {
+ widget::tree::Tag::of::<Menu<T>>()
+ }
+
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(Menu::<T> {
+ menu: menu::State::new(),
+ filtered_options: Filtered::empty(),
+ hovered_option: Some(0),
+ new_selection: None,
+ })
+ }
+
+ fn children(&self) -> Vec<widget::Tree> {
+ vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _>)]
+ }
+
+ 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 menu = tree.state.downcast_mut::<Menu<T>>();
+
+ let started_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+ // This is intended to check whether or not the message buffer was empty,
+ // since `Shell` does not expose such functionality.
+ let mut published_message_to_shell = false;
+
+ // Create a new list of local messages
+ let mut local_messages = Vec::new();
+ let mut local_shell = Shell::new(&mut local_messages);
+
+ // Provide it to the widget
+ let mut event_status = self.text_input.on_event(
+ &mut tree.children[0],
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ &mut local_shell,
+ viewport,
+ );
+
+ // Then finally react to them here
+ for message in local_messages {
+ let TextInputEvent::TextChanged(new_value) = message;
+
+ if let Some(on_input) = &self.on_input {
+ shell.publish((on_input)(new_value.clone()));
+ published_message_to_shell = true;
+ }
+
+ // Couple the filtered options with the `ComboBox`
+ // value and only recompute them when the value changes,
+ // instead of doing it in every `view` call
+ self.state.with_inner_mut(|state| {
+ menu.hovered_option = Some(0);
+ state.value = new_value;
+
+ state.filtered_options.update(
+ search(
+ &state.options,
+ &state.option_matchers,
+ &state.value,
+ )
+ .cloned()
+ .collect(),
+ );
+ });
+ shell.invalidate_layout();
+ }
+
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ if is_focused {
+ self.state.with_inner(|state| {
+ if !started_focused {
+ if let Some(on_option_hovered) = &mut self.on_option_hovered
+ {
+ let hovered_option = menu.hovered_option.unwrap_or(0);
+
+ if let Some(option) =
+ state.filtered_options.options.get(hovered_option)
+ {
+ shell.publish(on_option_hovered(option.clone()));
+ published_message_to_shell = true;
+ }
+ }
+ }
+
+ if let Event::Keyboard(keyboard::Event::KeyPressed {
+ key_code,
+ modifiers,
+ ..
+ }) = event
+ {
+ let shift_modifer = modifiers.shift();
+ match (key_code, shift_modifer) {
+ (keyboard::KeyCode::Enter, _) => {
+ if let Some(index) = &menu.hovered_option {
+ if let Some(option) =
+ state.filtered_options.options.get(*index)
+ {
+ menu.new_selection = Some(option.clone());
+ }
+ }
+
+ event_status = event::Status::Captured;
+ }
+
+ (keyboard::KeyCode::Up, _)
+ | (keyboard::KeyCode::Tab, true) => {
+ if let Some(index) = &mut menu.hovered_option {
+ if *index == 0 {
+ *index = state
+ .filtered_options
+ .options
+ .len()
+ .saturating_sub(1);
+ } else {
+ *index = index.saturating_sub(1);
+ }
+ } else {
+ menu.hovered_option = Some(0);
+ }
+
+ if let Some(on_option_hovered) =
+ &mut self.on_option_hovered
+ {
+ if let Some(option) =
+ menu.hovered_option.and_then(|index| {
+ state
+ .filtered_options
+ .options
+ .get(index)
+ })
+ {
+ // Notify the selection
+ shell.publish((on_option_hovered)(
+ option.clone(),
+ ));
+ published_message_to_shell = true;
+ }
+ }
+
+ event_status = event::Status::Captured;
+ }
+ (keyboard::KeyCode::Down, _)
+ | (keyboard::KeyCode::Tab, false)
+ if !modifiers.shift() =>
+ {
+ if let Some(index) = &mut menu.hovered_option {
+ if *index
+ >= state
+ .filtered_options
+ .options
+ .len()
+ .saturating_sub(1)
+ {
+ *index = 0;
+ } else {
+ *index = index.saturating_add(1).min(
+ state
+ .filtered_options
+ .options
+ .len()
+ .saturating_sub(1),
+ );
+ }
+ } else {
+ menu.hovered_option = Some(0);
+ }
+
+ if let Some(on_option_hovered) =
+ &mut self.on_option_hovered
+ {
+ if let Some(option) =
+ menu.hovered_option.and_then(|index| {
+ state
+ .filtered_options
+ .options
+ .get(index)
+ })
+ {
+ // Notify the selection
+ shell.publish((on_option_hovered)(
+ option.clone(),
+ ));
+ published_message_to_shell = true;
+ }
+ }
+
+ event_status = event::Status::Captured;
+ }
+ _ => {}
+ }
+ }
+ });
+ }
+
+ // If the overlay menu has selected something
+ self.state.with_inner_mut(|state| {
+ if let Some(selection) = menu.new_selection.take() {
+ // Clear the value and reset the options and menu
+ state.value = String::new();
+ state.filtered_options.update(state.options.clone());
+ menu.menu = menu::State::default();
+
+ // Notify the selection
+ shell.publish((self.on_selected)(selection));
+ published_message_to_shell = true;
+
+ // Unfocus the input
+ let _ = self.text_input.on_event(
+ &mut tree.children[0],
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ )),
+ layout,
+ mouse::Cursor::Unavailable,
+ renderer,
+ clipboard,
+ &mut Shell::new(&mut vec![]),
+ viewport,
+ );
+ }
+ });
+
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ if started_focused && !is_focused && !published_message_to_shell {
+ if let Some(message) = self.on_close.take() {
+ shell.publish(message);
+ }
+ }
+
+ // Focus changed, invalidate widget tree to force a fresh `view`
+ if started_focused != is_focused {
+ shell.invalidate_widgets();
+ }
+
+ event_status
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &widget::Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.text_input.mouse_interaction(
+ &tree.children[0],
+ layout,
+ cursor,
+ viewport,
+ renderer,
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &widget::Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ _style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ let selection = if is_focused || self.selection.is_empty() {
+ None
+ } else {
+ Some(&self.selection)
+ };
+
+ self.text_input.draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ layout,
+ cursor,
+ selection,
+ );
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut widget::Tree,
+ layout: Layout<'_>,
+ _renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ let is_focused = {
+ let text_input_state = tree.children[0]
+ .state
+ .downcast_ref::<text_input::State<Renderer::Paragraph>>();
+
+ text_input_state.is_focused()
+ };
+
+ if is_focused {
+ let Menu {
+ menu,
+ filtered_options,
+ hovered_option,
+ ..
+ } = tree.state.downcast_mut::<Menu<T>>();
+
+ let bounds = layout.bounds();
+
+ self.state.sync_filtered_options(filtered_options);
+
+ let mut menu = menu::Menu::new(
+ menu,
+ &filtered_options.options,
+ hovered_option,
+ |x| {
+ tree.children[0]
+ .state
+ .downcast_mut::<text_input::State<Renderer::Paragraph>>(
+ )
+ .unfocus();
+
+ (self.on_selected)(x)
+ },
+ self.on_option_hovered.as_deref(),
+ )
+ .width(bounds.width)
+ .padding(self.padding)
+ .style(self.menu_style.clone());
+
+ if let Some(font) = self.font {
+ menu = menu.font(font);
+ }
+
+ if let Some(size) = self.size {
+ menu = menu.text_size(size);
+ }
+
+ Some(menu.overlay(layout.position(), bounds.height))
+ } else {
+ None
+ }
+ }
+}
+
+impl<'a, T, Message, Renderer> From<ComboBox<'a, T, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ T: Display + Clone + 'static,
+ Message: 'a + Clone,
+ Renderer: text::Renderer + 'a,
+ Renderer::Theme: container::StyleSheet
+ + text_input::StyleSheet
+ + scrollable::StyleSheet
+ + menu::StyleSheet,
+{
+ fn from(combo_box: ComboBox<'a, T, Message, Renderer>) -> Self {
+ Self::new(combo_box)
+ }
+}
+
+/// Search list of options for a given query.
+pub fn search<'a, T, A>(
+ options: impl IntoIterator<Item = T> + 'a,
+ option_matchers: impl IntoIterator<Item = &'a A> + 'a,
+ query: &'a str,
+) -> impl Iterator<Item = T> + 'a
+where
+ A: AsRef<str> + 'a,
+{
+ let query: Vec<String> = query
+ .to_lowercase()
+ .split(|c: char| !c.is_ascii_alphanumeric())
+ .map(String::from)
+ .collect();
+
+ options
+ .into_iter()
+ .zip(option_matchers)
+ // Make sure each part of the query is found in the option
+ .filter_map(move |(option, matcher)| {
+ if query.iter().all(|part| matcher.as_ref().contains(part)) {
+ Some(option)
+ } else {
+ None
+ }
+ })
+}
+
+/// Build matchers from given list of options.
+pub fn build_matchers<'a, T>(
+ options: impl IntoIterator<Item = T> + 'a,
+) -> Vec<String>
+where
+ T: Display + 'a,
+{
+ options
+ .into_iter()
+ .map(|opt| {
+ let mut matcher = opt.to_string();
+ matcher.retain(|c| c.is_ascii_alphanumeric());
+ matcher.to_lowercase()
+ })
+ .collect()
+}
diff --git a/widget/src/container.rs b/widget/src/container.rs
index da9a31d6..ee7a4965 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -5,11 +5,13 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
-use crate::core::widget::{self, Operation, Tree};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::{self, Operation};
use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
- Point, Rectangle, Shell, Widget,
+ Point, Rectangle, Shell, Size, Vector, Widget,
};
+use crate::runtime::Command;
pub use iced_style::container::{Appearance, StyleSheet};
@@ -134,12 +136,20 @@ where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ self.content.as_widget().tag()
+ }
+
+ fn state(&self) -> tree::State {
+ self.content.as_widget().state()
+ }
+
fn children(&self) -> Vec<Tree> {
- vec![Tree::new(&self.content)]
+ self.content.as_widget().children()
}
fn diff(&self, tree: &mut Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ self.content.as_widget().diff(tree);
}
fn width(&self) -> Length {
@@ -152,11 +162,11 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
- renderer,
limits,
self.width,
self.height,
@@ -165,9 +175,7 @@ where
self.padding,
self.horizontal_alignment,
self.vertical_alignment,
- |renderer, limits| {
- self.content.as_widget().layout(renderer, limits)
- },
+ |limits| self.content.as_widget().layout(tree, renderer, limits),
)
}
@@ -180,9 +188,10 @@ where
) {
operation.container(
self.id.as_ref().map(|id| &id.0),
+ layout.bounds(),
&mut |operation| {
self.content.as_widget().operate(
- &mut tree.children[0],
+ tree,
layout.children().next().unwrap(),
renderer,
operation,
@@ -200,15 +209,17 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
self.content.as_widget_mut().on_event(
- &mut tree.children[0],
+ tree,
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
+ viewport,
)
}
@@ -221,7 +232,7 @@ where
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
- &tree.children[0],
+ tree,
layout.children().next().unwrap(),
cursor,
viewport,
@@ -244,7 +255,7 @@ where
draw_background(renderer, &style, layout.bounds());
self.content.as_widget().draw(
- &tree.children[0],
+ tree,
renderer,
theme,
&renderer::Style {
@@ -265,7 +276,7 @@ where
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
self.content.as_widget_mut().overlay(
- &mut tree.children[0],
+ tree,
layout.children().next().unwrap(),
renderer,
)
@@ -287,8 +298,7 @@ where
}
/// Computes the layout of a [`Container`].
-pub fn layout<Renderer>(
- renderer: &Renderer,
+pub fn layout(
limits: &layout::Limits,
width: Length,
height: Length,
@@ -297,7 +307,7 @@ pub fn layout<Renderer>(
padding: Padding,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
- layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
+ layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
let limits = limits
.loose()
@@ -306,7 +316,7 @@ pub fn layout<Renderer>(
.width(width)
.height(height);
- let mut content = layout_content(renderer, &limits.pad(padding).loose());
+ 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());
@@ -366,3 +376,92 @@ impl From<Id> for widget::Id {
id.0
}
}
+
+/// Produces a [`Command`] that queries the visible screen bounds of the
+/// [`Container`] with the given [`Id`].
+pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
+ struct VisibleBounds {
+ target: widget::Id,
+ depth: usize,
+ scrollables: Vec<(Vector, Rectangle, usize)>,
+ bounds: Option<Rectangle>,
+ }
+
+ impl Operation<Option<Rectangle>> for VisibleBounds {
+ fn scrollable(
+ &mut self,
+ _state: &mut dyn widget::operation::Scrollable,
+ _id: Option<&widget::Id>,
+ bounds: Rectangle,
+ translation: Vector,
+ ) {
+ match self.scrollables.last() {
+ Some((last_translation, last_viewport, _depth)) => {
+ let viewport = last_viewport
+ .intersection(&(bounds - *last_translation))
+ .unwrap_or(Rectangle::new(Point::ORIGIN, Size::ZERO));
+
+ self.scrollables.push((
+ translation + *last_translation,
+ viewport,
+ self.depth,
+ ));
+ }
+ None => {
+ self.scrollables.push((translation, bounds, self.depth));
+ }
+ }
+ }
+
+ fn container(
+ &mut self,
+ id: Option<&widget::Id>,
+ bounds: Rectangle,
+ operate_on_children: &mut dyn FnMut(
+ &mut dyn Operation<Option<Rectangle>>,
+ ),
+ ) {
+ if self.bounds.is_some() {
+ return;
+ }
+
+ if id == Some(&self.target) {
+ match self.scrollables.last() {
+ Some((translation, viewport, _)) => {
+ self.bounds =
+ viewport.intersection(&(bounds - *translation));
+ }
+ None => {
+ self.bounds = Some(bounds);
+ }
+ }
+
+ return;
+ }
+
+ self.depth += 1;
+
+ operate_on_children(self);
+
+ self.depth -= 1;
+
+ match self.scrollables.last() {
+ Some((_, _, depth)) if self.depth == *depth => {
+ let _ = self.scrollables.pop();
+ }
+ _ => {}
+ }
+ }
+
+ fn finish(&self) -> widget::operation::Outcome<Option<Rectangle>> {
+ widget::operation::Outcome::Some(self.bounds)
+ }
+ }
+
+ Command::widget(VisibleBounds {
+ target: id.into(),
+ depth: 0,
+ scrollables: Vec::new(),
+ bounds: None,
+ })
+}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 3f5136f8..115198fb 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -1,10 +1,12 @@
//! Helper functions to create pure widgets.
use crate::button::{self, Button};
use crate::checkbox::{self, Checkbox};
+use crate::combo_box::{self, ComboBox};
use crate::container::{self, Container};
use crate::core;
use crate::core::widget::operation;
use crate::core::{Element, Length, Pixels};
+use crate::keyed;
use crate::overlay;
use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar};
@@ -14,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};
@@ -24,7 +27,7 @@ use std::ops::RangeInclusive;
/// Creates a [`Column`] with the given children.
///
-/// [`Column`]: widget::Column
+/// [`Column`]: crate::Column
#[macro_export]
macro_rules! column {
() => (
@@ -37,7 +40,7 @@ macro_rules! column {
/// Creates a [`Row`] with the given children.
///
-/// [`Row`]: widget::Row
+/// [`Row`]: crate::Row
#[macro_export]
macro_rules! row {
() => (
@@ -50,7 +53,7 @@ macro_rules! row {
/// Creates a new [`Container`] with the provided content.
///
-/// [`Container`]: widget::Container
+/// [`Container`]: crate::Container
pub fn container<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Container<'a, Message, Renderer>
@@ -62,17 +65,25 @@ where
}
/// Creates a new [`Column`] with the given children.
-///
-/// [`Column`]: widget::Column
pub fn column<Message, Renderer>(
children: Vec<Element<'_, Message, Renderer>>,
) -> Column<'_, Message, Renderer> {
Column::with_children(children)
}
+/// Creates a new [`keyed::Column`] with the given children.
+pub fn keyed_column<'a, Key, Message, Renderer>(
+ children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>,
+) -> keyed::Column<'a, Key, Message, Renderer>
+where
+ Key: Copy + PartialEq,
+{
+ keyed::Column::with_children(children)
+}
+
/// Creates a new [`Row`] with the given children.
///
-/// [`Row`]: widget::Row
+/// [`Row`]: crate::Row
pub fn row<Message, Renderer>(
children: Vec<Element<'_, Message, Renderer>>,
) -> Row<'_, Message, Renderer> {
@@ -81,7 +92,7 @@ pub fn row<Message, Renderer>(
/// Creates a new [`Scrollable`] with the provided content.
///
-/// [`Scrollable`]: widget::Scrollable
+/// [`Scrollable`]: crate::Scrollable
pub fn scrollable<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Scrollable<'a, Message, Renderer>
@@ -94,7 +105,7 @@ where
/// Creates a new [`Button`] with the provided content.
///
-/// [`Button`]: widget::Button
+/// [`Button`]: crate::Button
pub fn button<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Button<'a, Message, Renderer>
@@ -108,8 +119,8 @@ where
/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`].
///
-/// [`Tooltip`]: widget::Tooltip
-/// [`tooltip::Position`]: widget::tooltip::Position
+/// [`Tooltip`]: crate::Tooltip
+/// [`tooltip::Position`]: crate::tooltip::Position
pub fn tooltip<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
tooltip: impl ToString,
@@ -124,7 +135,7 @@ where
/// Creates a new [`Text`] widget with the provided content.
///
-/// [`Text`]: widget::Text
+/// [`Text`]: core::widget::Text
pub fn text<'a, Renderer>(text: impl ToString) -> Text<'a, Renderer>
where
Renderer: core::text::Renderer,
@@ -135,7 +146,7 @@ where
/// Creates a new [`Checkbox`].
///
-/// [`Checkbox`]: widget::Checkbox
+/// [`Checkbox`]: crate::Checkbox
pub fn checkbox<'a, Message, Renderer>(
label: impl Into<String>,
is_checked: bool,
@@ -150,7 +161,7 @@ where
/// Creates a new [`Radio`].
///
-/// [`Radio`]: widget::Radio
+/// [`Radio`]: crate::Radio
pub fn radio<Message, Renderer, V>(
label: impl Into<String>,
value: V,
@@ -168,7 +179,7 @@ where
/// Creates a new [`Toggler`].
///
-/// [`Toggler`]: widget::Toggler
+/// [`Toggler`]: crate::Toggler
pub fn toggler<'a, Message, Renderer>(
label: impl Into<Option<String>>,
is_checked: bool,
@@ -183,7 +194,7 @@ where
/// Creates a new [`TextInput`].
///
-/// [`TextInput`]: widget::TextInput
+/// [`TextInput`]: crate::TextInput
pub fn text_input<'a, Message, Renderer>(
placeholder: &str,
value: &str,
@@ -196,9 +207,23 @@ 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`]: widget::Slider
+/// [`Slider`]: crate::Slider
pub fn slider<'a, T, Message, Renderer>(
range: std::ops::RangeInclusive<T>,
value: T,
@@ -215,7 +240,7 @@ where
/// Creates a new [`VerticalSlider`].
///
-/// [`VerticalSlider`]: widget::VerticalSlider
+/// [`VerticalSlider`]: crate::VerticalSlider
pub fn vertical_slider<'a, T, Message, Renderer>(
range: std::ops::RangeInclusive<T>,
value: T,
@@ -232,7 +257,7 @@ where
/// Creates a new [`PickList`].
///
-/// [`PickList`]: widget::PickList
+/// [`PickList`]: crate::PickList
pub fn pick_list<'a, Message, Renderer, T>(
options: impl Into<Cow<'a, [T]>>,
selected: Option<T>,
@@ -252,23 +277,40 @@ where
PickList::new(options, selected, on_selected)
}
+/// Creates a new [`ComboBox`].
+///
+/// [`ComboBox`]: crate::ComboBox
+pub fn combo_box<'a, T, Message, Renderer>(
+ state: &'a combo_box::State<T>,
+ placeholder: &str,
+ selection: Option<&T>,
+ on_selected: impl Fn(T) -> Message + 'static,
+) -> ComboBox<'a, T, Message, Renderer>
+where
+ T: std::fmt::Display + Clone,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: text_input::StyleSheet + overlay::menu::StyleSheet,
+{
+ ComboBox::new(state, placeholder, selection, on_selected)
+}
+
/// Creates a new horizontal [`Space`] with the given [`Length`].
///
-/// [`Space`]: widget::Space
+/// [`Space`]: crate::Space
pub fn horizontal_space(width: impl Into<Length>) -> Space {
Space::with_width(width)
}
/// Creates a new vertical [`Space`] with the given [`Length`].
///
-/// [`Space`]: widget::Space
+/// [`Space`]: crate::Space
pub fn vertical_space(height: impl Into<Length>) -> Space {
Space::with_height(height)
}
/// Creates a horizontal [`Rule`] with the given height.
///
-/// [`Rule`]: widget::Rule
+/// [`Rule`]: crate::Rule
pub fn horizontal_rule<Renderer>(height: impl Into<Pixels>) -> Rule<Renderer>
where
Renderer: core::Renderer,
@@ -279,7 +321,7 @@ where
/// Creates a vertical [`Rule`] with the given width.
///
-/// [`Rule`]: widget::Rule
+/// [`Rule`]: crate::Rule
pub fn vertical_rule<Renderer>(width: impl Into<Pixels>) -> Rule<Renderer>
where
Renderer: core::Renderer,
@@ -294,7 +336,7 @@ where
/// * an inclusive range of possible values, and
/// * the current value of the [`ProgressBar`].
///
-/// [`ProgressBar`]: widget::ProgressBar
+/// [`ProgressBar`]: crate::ProgressBar
pub fn progress_bar<Renderer>(
range: RangeInclusive<f32>,
value: f32,
@@ -308,7 +350,7 @@ where
/// Creates a new [`Image`].
///
-/// [`Image`]: widget::Image
+/// [`Image`]: crate::Image
#[cfg(feature = "image")]
pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
crate::Image::new(handle.into())
@@ -316,8 +358,8 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
/// Creates a new [`Svg`] widget from the given [`Handle`].
///
-/// [`Svg`]: widget::Svg
-/// [`Handle`]: widget::svg::Handle
+/// [`Svg`]: crate::Svg
+/// [`Handle`]: crate::svg::Handle
#[cfg(feature = "svg")]
pub fn svg<Renderer>(
handle: impl Into<core::svg::Handle>,
@@ -330,6 +372,8 @@ where
}
/// Creates a new [`Canvas`].
+///
+/// [`Canvas`]: crate::Canvas
#[cfg(feature = "canvas")]
pub fn canvas<P, Message, Renderer>(
program: P,
@@ -341,6 +385,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 66bf2156..67699102 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
}
}
@@ -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,14 +148,14 @@ 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
{
renderer.with_layer(bounds, render);
} else {
- render(renderer)
+ render(renderer);
}
}
@@ -167,6 +174,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -190,7 +198,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 8040d6bd..68015ba8 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(),
}
}
@@ -105,6 +107,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -148,12 +151,13 @@ where
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
let bounds = layout.bounds();
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
- let Some(cursor_position) = cursor.position() else {
+ let Some(cursor_position) = cursor.position_over(bounds) else {
return event::Status::Ignored;
};
@@ -327,12 +331,13 @@ where
image::Renderer::draw(
renderer,
self.handle.clone(),
+ self.filter_method,
Rectangle {
x: bounds.x,
y: bounds.y,
..Rectangle::with_size(image_size)
},
- )
+ );
});
});
}
diff --git a/widget/src/keyed.rs b/widget/src/keyed.rs
new file mode 100644
index 00000000..ad531e66
--- /dev/null
+++ b/widget/src/keyed.rs
@@ -0,0 +1,53 @@
+//! Use widgets that can provide hints to ensure continuity.
+//!
+//! # What is continuity?
+//! Continuity is the feeling of persistence of state.
+//!
+//! In a graphical user interface, users expect widgets to have a
+//! certain degree of continuous state. For instance, a text input
+//! that is focused should stay focused even if the widget tree
+//! changes slightly.
+//!
+//! Continuity is tricky in `iced` and the Elm Architecture because
+//! the whole widget tree is rebuilt during every `view` call. This is
+//! very convenient from a developer perspective because you can build
+//! extremely dynamic interfaces without worrying about changing state.
+//!
+//! However, the tradeoff is that determining what changed becomes hard
+//! for `iced`. If you have a list of things, adding an element at the
+//! top may cause a loss of continuity on every element on the list!
+//!
+//! # How can we keep continuity?
+//! The good news is that user interfaces generally have a static widget
+//! structure. This structure can be relied on to ensure some degree of
+//! continuity. `iced` already does this.
+//!
+//! However, sometimes you have a certain part of your interface that is
+//! quite dynamic. For instance, a list of things where items may be added
+//! or removed at any place.
+//!
+//! There are different ways to mitigate this during the reconciliation
+//! stage, but they involve comparing trees at certain depths and
+//! backtracking... Quite computationally expensive.
+//!
+//! One approach that is cheaper consists in letting the user provide some hints
+//! about the identities of the different widgets so that they can be compared
+//! directly without going deeper.
+//!
+//! The widgets in this module will all ask for a "hint" of some sort. In order
+//! to help them keep continuity, you need to make sure the hint stays the same
+//! for the same items in your user interface between `view` calls.
+pub mod column;
+
+pub use column::Column;
+
+/// Creates a [`Column`] with the given children.
+#[macro_export]
+macro_rules! keyed_column {
+ () => (
+ $crate::Column::new()
+ );
+ ($($x:expr),+ $(,)?) => (
+ $crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+])
+ );
+}
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
new file mode 100644
index 00000000..0ef82407
--- /dev/null
+++ b/widget/src/keyed/column.rs
@@ -0,0 +1,320 @@
+//! Distribute content vertically.
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::Operation;
+use crate::core::{
+ Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
+ Shell, Widget,
+};
+
+/// A container that distributes its contents vertically.
+#[allow(missing_debug_implementations)]
+pub struct Column<'a, Key, Message, Renderer = crate::Renderer>
+where
+ Key: Copy + PartialEq,
+{
+ spacing: f32,
+ padding: Padding,
+ width: Length,
+ height: Length,
+ max_width: f32,
+ align_items: Alignment,
+ keys: Vec<Key>,
+ children: Vec<Element<'a, Message, Renderer>>,
+}
+
+impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer>
+where
+ Key: Copy + PartialEq,
+{
+ /// 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,
+ width: Length::Shrink,
+ height: Length::Shrink,
+ max_width: f32::INFINITY,
+ align_items: Alignment::Start,
+ keys,
+ children,
+ }
+ }
+
+ /// Sets the vertical spacing _between_ elements.
+ ///
+ /// Custom margins per element do not exist in iced. You should use this
+ /// method instead! While less flexible, it helps you keep spacing between
+ /// elements consistent.
+ pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
+ self.spacing = amount.into().0;
+ self
+ }
+
+ /// Sets the [`Padding`] of the [`Column`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
+ self
+ }
+
+ /// Sets the width of the [`Column`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Column`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Sets the maximum width of the [`Column`].
+ pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
+ self.max_width = max_width.into().0;
+ self
+ }
+
+ /// Sets the horizontal alignment of the contents of the [`Column`] .
+ pub fn align_items(mut self, align: Alignment) -> Self {
+ self.align_items = align;
+ self
+ }
+
+ /// Adds an element to the [`Column`].
+ pub fn push(
+ mut self,
+ key: Key,
+ child: impl Into<Element<'a, Message, Renderer>>,
+ ) -> Self {
+ self.keys.push(key);
+ self.children.push(child.into());
+ self
+ }
+}
+
+impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
+where
+ Key: Copy + PartialEq,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+struct State<Key>
+where
+ Key: Copy + PartialEq,
+{
+ keys: Vec<Key>,
+}
+
+impl<'a, Key, Message, Renderer> Widget<Message, Renderer>
+ for Column<'a, Key, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+ Key: Copy + PartialEq + 'static,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State<Key>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State {
+ keys: self.keys.clone(),
+ })
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ self.children.iter().map(Tree::new).collect()
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ let Tree {
+ state, children, ..
+ } = tree;
+
+ let state = state.downcast_mut::<State<Key>>();
+
+ tree::diff_children_custom_with_search(
+ children,
+ &self.children,
+ |tree, child| child.as_widget().diff(tree),
+ |index| {
+ self.keys.get(index).or_else(|| self.keys.last()).copied()
+ != Some(state.keys[index])
+ },
+ |child| Tree::new(child.as_widget()),
+ );
+
+ if state.keys != self.keys {
+ state.keys = self.keys.clone();
+ }
+ }
+
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits
+ .max_width(self.max_width)
+ .width(self.width)
+ .height(self.height);
+
+ layout::flex::resolve(
+ layout::flex::Axis::Vertical,
+ renderer,
+ &limits,
+ self.padding,
+ self.spacing,
+ self.align_items,
+ &self.children,
+ &mut tree.children,
+ )
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn Operation<Message>,
+ ) {
+ operation.container(None, layout.bounds(), &mut |operation| {
+ self.children
+ .iter()
+ .zip(&mut tree.children)
+ .zip(layout.children())
+ .for_each(|((child, state), layout)| {
+ child
+ .as_widget()
+ .operate(state, layout, renderer, operation);
+ });
+ });
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
+ ) -> event::Status {
+ self.children
+ .iter_mut()
+ .zip(&mut tree.children)
+ .zip(layout.children())
+ .map(|((child, state), layout)| {
+ child.as_widget_mut().on_event(
+ state,
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ viewport,
+ )
+ })
+ .fold(event::Status::Ignored, event::Status::merge)
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.children
+ .iter()
+ .zip(&tree.children)
+ .zip(layout.children())
+ .map(|((child, state), layout)| {
+ child.as_widget().mouse_interaction(
+ state, layout, cursor, viewport, renderer,
+ )
+ })
+ .max()
+ .unwrap_or_default()
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ 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);
+ }
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ overlay::from_children(&mut self.children, tree, layout, renderer)
+ }
+}
+
+impl<'a, Key, Message, Renderer> From<Column<'a, Key, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Key: Copy + PartialEq + 'static,
+ Message: 'a,
+ Renderer: crate::core::Renderer + 'a,
+{
+ fn from(column: Column<'a, Key, Message, Renderer>) -> Self {
+ Self::new(column)
+ }
+}
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index da287f06..167a055d 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;
@@ -135,7 +135,7 @@ where
(*self.element.borrow_mut()) = Some(current.element.clone());
self.with_element(|element| {
- tree.diff_children(std::slice::from_ref(&element.as_widget()))
+ tree.diff_children(std::slice::from_ref(&element.as_widget()));
});
} else {
(*self.element.borrow_mut()) = Some(current.element.clone());
@@ -152,11 +152,14 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.with_element(|element| {
- element.as_widget().layout(renderer, limits)
+ element
+ .as_widget()
+ .layout(&mut tree.children[0], renderer, limits)
})
}
@@ -186,6 +189,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
self.with_element_mut(|element| {
element.as_widget_mut().on_event(
@@ -196,6 +200,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
})
}
@@ -238,8 +243,8 @@ where
layout,
cursor,
viewport,
- )
- })
+ );
+ });
}
fn overlay<'b>(
@@ -324,13 +329,14 @@ where
Renderer: core::Renderer,
{
fn layout(
- &self,
+ &mut self,
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 c7814966..ad0c3823 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -7,7 +7,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::runtime::overlay::Nested;
@@ -253,11 +254,18 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
+ let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
+
self.with_element(|element| {
- element.as_widget().layout(renderer, limits)
+ element.as_widget().layout(
+ &mut t.borrow_mut().as_mut().unwrap().children[0],
+ renderer,
+ limits,
+ )
})
}
@@ -270,6 +278,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
@@ -284,6 +293,7 @@ where
renderer,
clipboard,
&mut local_shell,
+ viewport,
)
});
@@ -338,11 +348,12 @@ where
fn container(
&mut self,
id: Option<&widget::Id>,
+ bounds: Rectangle,
operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>,
),
) {
- self.operation.container(id, &mut |operation| {
+ self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation });
});
}
@@ -367,8 +378,10 @@ where
&mut self,
state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>,
+ bounds: Rectangle,
+ translation: Vector,
) {
- self.operation.scrollable(state, id);
+ self.operation.scrollable(state, id, bounds, translation);
}
fn custom(
@@ -498,7 +511,7 @@ impl<'a, 'b, Message, Renderer, Event, S> Drop
for Overlay<'a, 'b, Message, Renderer, Event, S>
{
fn drop(&mut self) {
- if let Some(heads) = self.0.take().map(|inner| inner.into_heads()) {
+ if let Some(heads) = self.0.take().map(Inner::into_heads) {
*heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree);
}
}
@@ -560,13 +573,14 @@ where
S: 'static + Default,
{
fn layout(
- &self,
+ &mut self,
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/responsive.rs b/widget/src/lazy/responsive.rs
index 07300857..86d37b6c 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;
@@ -60,13 +61,13 @@ impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
Renderer: core::Renderer,
{
- fn layout(&mut self, renderer: &Renderer) {
+ fn layout(&mut self, tree: &mut Tree, renderer: &Renderer) {
if self.layout.is_none() {
- self.layout =
- Some(self.element.as_widget().layout(
- renderer,
- &layout::Limits::new(Size::ZERO, self.size),
- ));
+ self.layout = Some(self.element.as_widget().layout(
+ tree,
+ renderer,
+ &layout::Limits::new(Size::ZERO, self.size),
+ ));
}
}
@@ -104,7 +105,7 @@ where
R: Deref<Target = Renderer>,
{
self.update(tree, layout.bounds().size(), view);
- self.layout(renderer.deref());
+ self.layout(tree, renderer.deref());
let content_layout = Layout::with_offset(
layout.position() - Point::ORIGIN,
@@ -144,6 +145,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -182,6 +184,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
@@ -203,6 +206,7 @@ where
renderer,
clipboard,
&mut local_shell,
+ viewport,
)
},
);
@@ -237,9 +241,9 @@ where
|tree, renderer, layout, element| {
element.as_widget().draw(
tree, renderer, theme, style, layout, cursor, viewport,
- )
+ );
},
- )
+ );
}
fn mouse_interaction(
@@ -283,7 +287,7 @@ where
overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>,
tree| {
content.update(tree, layout.bounds().size(), &self.view);
- content.layout(renderer);
+ content.layout(tree, renderer);
let Content {
element,
@@ -360,13 +364,14 @@ where
Renderer: core::Renderer,
{
fn layout(
- &self,
+ &mut self,
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 9da13f9b..07378d83 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -2,18 +2,13 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
+#![forbid(unsafe_code, rust_2018_idioms)]
#![deny(
missing_debug_implementations,
missing_docs,
unused_results,
- clippy::extra_unused_lifetimes,
- clippy::from_over_into,
- clippy::needless_borrow,
- clippy::new_without_default,
- clippy::useless_conversion
+ rustdoc::broken_intra_doc_links
)]
-#![forbid(unsafe_code, rust_2018_idioms)]
-#![allow(clippy::inherent_to_string, clippy::type_complexity)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use iced_renderer as renderer;
pub use iced_renderer::graphics;
@@ -27,7 +22,9 @@ mod row;
pub mod button;
pub mod checkbox;
+pub mod combo_box;
pub mod container;
+pub mod keyed;
pub mod overlay;
pub mod pane_grid;
pub mod pick_list;
@@ -38,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;
@@ -63,6 +61,8 @@ pub use checkbox::Checkbox;
#[doc(no_inline)]
pub use column::Column;
#[doc(no_inline)]
+pub use combo_box::ComboBox;
+#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
pub use mouse_area::MouseArea;
@@ -87,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;
@@ -95,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 da7dc88f..3a5b01a3 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -120,10 +120,13 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.content.as_widget().layout(renderer, limits)
+ self.content
+ .as_widget()
+ .layout(&mut tree.children[0], renderer, limits)
}
fn operate(
@@ -150,6 +153,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
if let event::Status::Captured = self.content.as_widget_mut().on_event(
&mut tree.children[0],
@@ -159,6 +163,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
) {
return event::Status::Captured;
}
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index ccf4dfb5..5098fa17 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -28,9 +28,10 @@ where
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
+ on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
width: f32,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -52,12 +53,14 @@ where
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: impl FnMut(T) -> Message + 'a,
+ on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
) -> Self {
Menu {
state,
options,
hovered_option,
on_selected: Box::new(on_selected),
+ on_option_hovered,
width: 0.0,
padding: Padding::ZERO,
text_size: None,
@@ -82,11 +85,11 @@ where
/// Sets the text size of the [`Menu`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
- /// Sets the text [`LineHeight`] of the [`Menu`].
+ /// Sets the text [`text::LineHeight`] of the [`Menu`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
@@ -187,6 +190,7 @@ where
options,
hovered_option,
on_selected,
+ on_option_hovered,
width,
padding,
font,
@@ -200,6 +204,7 @@ where
options,
hovered_option,
on_selected,
+ on_option_hovered,
font,
text_size,
text_line_height,
@@ -227,10 +232,11 @@ where
Renderer::Theme: StyleSheet + container::StyleSheet,
{
fn layout(
- &self,
+ &mut self,
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;
@@ -248,7 +254,7 @@ where
)
.width(self.width);
- let mut node = self.container.layout(renderer, &limits);
+ let mut node = self.container.layout(self.state, renderer, &limits);
node.move_to(if space_below > space_above {
position + Vector::new(0.0, self.target_height)
@@ -268,8 +274,11 @@ where
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
+ let bounds = layout.bounds();
+
self.container.on_event(
self.state, event, layout, cursor, renderer, clipboard, shell,
+ &bounds,
)
}
@@ -318,8 +327,9 @@ where
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
+ on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -343,6 +353,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -352,8 +363,7 @@ where
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
- let text_line_height =
- self.text_line_height.to_absolute(Pixels(text_size));
+ let text_line_height = self.text_line_height.to_absolute(text_size);
let size = {
let intrinsic = Size::new(
@@ -377,6 +387,7 @@ where
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
@@ -397,12 +408,25 @@ where
.text_size
.unwrap_or_else(|| renderer.default_size());
- let option_height = f32::from(
- self.text_line_height.to_absolute(Pixels(text_size)),
- ) + self.padding.vertical();
+ let option_height =
+ f32::from(self.text_line_height.to_absolute(text_size))
+ + self.padding.vertical();
+
+ let new_hovered_option =
+ (cursor_position.y / option_height) as usize;
+
+ if let Some(on_option_hovered) = self.on_option_hovered {
+ if *self.hovered_option != Some(new_hovered_option) {
+ if let Some(option) =
+ self.options.get(new_hovered_option)
+ {
+ shell
+ .publish(on_option_hovered(option.clone()));
+ }
+ }
+ }
- *self.hovered_option =
- Some((cursor_position.y / option_height) as usize);
+ *self.hovered_option = Some(new_hovered_option);
}
}
Event::Touch(touch::Event::FingerPressed { .. }) => {
@@ -413,9 +437,9 @@ where
.text_size
.unwrap_or_else(|| renderer.default_size());
- let option_height = f32::from(
- self.text_line_height.to_absolute(Pixels(text_size)),
- ) + self.padding.vertical();
+ let option_height =
+ f32::from(self.text_line_height.to_absolute(text_size))
+ + self.padding.vertical();
*self.hovered_option =
Some((cursor_position.y / option_height) as usize);
@@ -467,7 +491,7 @@ where
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
let option_height =
- f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
+ f32::from(self.text_line_height.to_absolute(text_size))
+ self.padding.vertical();
let offset = viewport.y - bounds.y;
@@ -503,26 +527,24 @@ where
);
}
- renderer.fill_text(Text {
- content: &option.to_string(),
- bounds: Rectangle {
- x: bounds.x + self.padding.left,
- y: bounds.center_y(),
- width: f32::INFINITY,
- ..bounds
+ renderer.fill_text(
+ Text {
+ content: &option.to_string(),
+ bounds: Size::new(f32::INFINITY, bounds.height),
+ size: text_size,
+ line_height: self.text_line_height,
+ font: self.font.unwrap_or_else(|| renderer.default_font()),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: self.text_shaping,
},
- size: text_size,
- line_height: self.text_line_height,
- font: self.font.unwrap_or_else(|| renderer.default_font()),
- color: if is_selected {
+ Point::new(bounds.x + self.padding.left, bounds.center_y()),
+ if is_selected {
appearance.selected_text_color
} else {
appearance.text_color
},
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: self.text_shaping,
- });
+ );
}
}
}
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 31bb0e86..2d25a543 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -1,12 +1,12 @@
//! Let your users split regions of your application and organize layout dynamically.
//!
-//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+//! ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
//!
//! # Example
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
-//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid
+//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.10/examples/pane_grid
mod axis;
mod configuration;
mod content;
@@ -49,7 +49,7 @@ use crate::core::{
/// A collection of panes distributed using either vertical or horizontal splits
/// to completely fill the space available.
///
-/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](https://gfycat.com/frailfreshairedaleterrier)
+/// ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
///
/// This distribution of space is common in tiling window managers (like
/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
@@ -275,10 +275,12 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
+ tree,
renderer,
limits,
self.contents.layout(),
@@ -286,7 +288,9 @@ where
self.height,
self.spacing,
self.contents.iter(),
- |content, renderer, limits| content.layout(renderer, limits),
+ |content, tree, renderer, limits| {
+ content.layout(tree, renderer, limits)
+ },
)
}
@@ -297,14 +301,14 @@ where
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
- operation.container(None, &mut |operation| {
+ operation.container(None, layout.bounds(), &mut |operation| {
self.contents
.iter()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|(((_pane, content), state), layout)| {
content.operate(state, layout, renderer, operation);
- })
+ });
});
}
@@ -317,6 +321,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
let action = tree.state.downcast_mut::<state::Action>();
@@ -357,6 +362,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
is_picked,
)
})
@@ -430,7 +436,7 @@ where
tree, renderer, theme, style, layout, cursor, rectangle,
);
},
- )
+ );
}
fn overlay<'b>(
@@ -469,6 +475,7 @@ where
/// Calculates the [`Layout`] of a [`PaneGrid`].
pub fn layout<Renderer, T>(
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
node: &Node,
@@ -476,19 +483,26 @@ pub fn layout<Renderer, T>(
height: Length,
spacing: f32,
contents: impl Iterator<Item = (Pane, T)>,
- layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node,
+ layout_content: impl Fn(
+ T,
+ &mut Tree,
+ &Renderer,
+ &layout::Limits,
+ ) -> layout::Node,
) -> layout::Node {
let limits = limits.width(width).height(height);
let size = limits.resolve(Size::ZERO);
let regions = node.pane_regions(spacing, size);
let children = contents
- .filter_map(|(pane, content)| {
+ .zip(tree.children.iter_mut())
+ .filter_map(|((pane, content), tree)| {
let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height);
let mut node = layout_content(
content,
+ tree,
renderer,
&layout::Limits::new(size, size),
);
@@ -592,11 +606,10 @@ pub fn update<'a, Message, T: Draggable>(
} else {
let dropped_region = contents
.zip(layout.children())
- .filter_map(|(target, layout)| {
+ .find_map(|(target, layout)| {
layout_region(layout, cursor_position)
.map(|region| (target, region))
- })
- .next();
+ });
match dropped_region {
Some(((target, _), region))
@@ -1137,21 +1150,19 @@ pub struct ResizeEvent {
* Helpers
*/
fn hovered_split<'a>(
- splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
+ mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
spacing: f32,
cursor_position: Point,
) -> Option<(Split, Axis, Rectangle)> {
- splits
- .filter_map(|(split, (axis, region, ratio))| {
- let bounds = axis.split_line_bounds(*region, *ratio, spacing);
+ splits.find_map(|(split, (axis, region, ratio))| {
+ let bounds = axis.split_line_bounds(*region, *ratio, spacing);
- if bounds.contains(cursor_position) {
- Some((*split, *axis, bounds))
- } else {
- None
- }
- })
- .next()
+ if bounds.contains(cursor_position) {
+ Some((*split, *axis, bounds))
+ } else {
+ None
+ }
+ })
}
/// The visible contents of the [`PaneGrid`]
diff --git a/widget/src/pane_grid/configuration.rs b/widget/src/pane_grid/configuration.rs
index ddbc3bc2..b8aa2c7d 100644
--- a/widget/src/pane_grid/configuration.rs
+++ b/widget/src/pane_grid/configuration.rs
@@ -2,7 +2,7 @@ use crate::pane_grid::Axis;
/// The arrangement of a [`PaneGrid`].
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone)]
pub enum Configuration<T> {
/// A split of the available space.
@@ -21,6 +21,6 @@ pub enum Configuration<T> {
},
/// A [`Pane`].
///
- /// [`Pane`]: crate::widget::pane_grid::Pane
+ /// [`Pane`]: super::Pane
Pane(T),
}
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index c28ae6e3..826ea663 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -10,7 +10,7 @@ use crate::pane_grid::{Draggable, TitleBar};
/// The content of a [`Pane`].
///
-/// [`Pane`]: crate::widget::pane_grid::Pane
+/// [`Pane`]: super::Pane
#[allow(missing_debug_implementations)]
pub struct Content<'a, Message, Renderer = crate::Renderer>
where
@@ -87,7 +87,7 @@ where
/// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
///
- /// [`Renderer`]: crate::Renderer
+ /// [`Renderer`]: crate::core::Renderer
pub fn draw(
&self,
tree: &Tree,
@@ -150,18 +150,23 @@ where
pub(crate) fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
if let Some(title_bar) = &self.title_bar {
let max_size = limits.max();
- let title_bar_layout = title_bar
- .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let title_bar_layout = title_bar.layout(
+ &mut tree.children[1],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
let title_bar_size = title_bar_layout.size();
let mut body_layout = self.body.as_widget().layout(
+ &mut tree.children[0],
renderer,
&layout::Limits::new(
Size::ZERO,
@@ -179,7 +184,11 @@ where
vec![title_bar_layout, body_layout],
)
} else {
- self.body.as_widget().layout(renderer, limits)
+ self.body.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ limits,
+ )
}
}
@@ -222,6 +231,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
is_picked: bool,
) -> event::Status {
let mut event_status = event::Status::Ignored;
@@ -237,6 +247,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
);
children.next().unwrap()
@@ -255,6 +266,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
};
diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs
index 6de5920f..1f568f95 100644
--- a/widget/src/pane_grid/node.rs
+++ b/widget/src/pane_grid/node.rs
@@ -5,7 +5,7 @@ use std::collections::BTreeMap;
/// A layout node of a [`PaneGrid`].
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone)]
pub enum Node {
/// The region of this [`Node`] is split into two.
@@ -95,13 +95,13 @@ impl Node {
splits
}
- pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> {
+ pub(crate) fn find(&mut self, pane: Pane) -> Option<&mut Node> {
match self {
Node::Split { a, b, .. } => {
a.find(pane).or_else(move || b.find(pane))
}
Node::Pane(p) => {
- if p == pane {
+ if *p == pane {
Some(self)
} else {
None
@@ -139,12 +139,12 @@ impl Node {
f(self);
}
- pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool {
+ pub(crate) fn resize(&mut self, split: Split, percentage: f32) -> bool {
match self {
Node::Split {
id, ratio, a, b, ..
} => {
- if id == split {
+ if *id == split {
*ratio = percentage;
true
@@ -158,13 +158,13 @@ impl Node {
}
}
- pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> {
+ pub(crate) fn remove(&mut self, pane: Pane) -> Option<Pane> {
match self {
Node::Split { a, b, .. } => {
- if a.pane() == Some(*pane) {
+ if a.pane() == Some(pane) {
*self = *b.clone();
Some(self.first_pane())
- } else if b.pane() == Some(*pane) {
+ } else if b.pane() == Some(pane) {
*self = *a.clone();
Some(self.first_pane())
} else {
diff --git a/widget/src/pane_grid/pane.rs b/widget/src/pane_grid/pane.rs
index d6fbab83..cabf55c1 100644
--- a/widget/src/pane_grid/pane.rs
+++ b/widget/src/pane_grid/pane.rs
@@ -1,5 +1,5 @@
/// A rectangular region in a [`PaneGrid`] used to display widgets.
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Pane(pub(super) usize);
diff --git a/widget/src/pane_grid/split.rs b/widget/src/pane_grid/split.rs
index 8132272a..ce021978 100644
--- a/widget/src/pane_grid/split.rs
+++ b/widget/src/pane_grid/split.rs
@@ -1,5 +1,5 @@
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Split(pub(super) usize);
diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs
index 6fd15890..481cd770 100644
--- a/widget/src/pane_grid/state.rs
+++ b/widget/src/pane_grid/state.rs
@@ -1,6 +1,6 @@
//! The state of a [`PaneGrid`].
//!
-//! [`PaneGrid`]: crate::widget::PaneGrid
+//! [`PaneGrid`]: super::PaneGrid
use crate::core::{Point, Size};
use crate::pane_grid::{
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
@@ -18,23 +18,23 @@ use std::collections::HashMap;
/// provided to the view function of [`PaneGrid::new`] for displaying each
/// [`Pane`].
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
-/// [`PaneGrid::new`]: crate::widget::PaneGrid::new
+/// [`PaneGrid`]: super::PaneGrid
+/// [`PaneGrid::new`]: super::PaneGrid::new
#[derive(Debug, Clone)]
pub struct State<T> {
/// The panes of the [`PaneGrid`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
pub panes: HashMap<Pane, T>,
/// The internal state of the [`PaneGrid`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
pub internal: Internal,
/// The maximized [`Pane`] of the [`PaneGrid`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
pub(super) maximized: Option<Pane>,
}
@@ -75,14 +75,14 @@ impl<T> State<T> {
}
/// Returns the internal state of the given [`Pane`], if it exists.
- pub fn get(&self, pane: &Pane) -> Option<&T> {
- self.panes.get(pane)
+ pub fn get(&self, pane: Pane) -> Option<&T> {
+ self.panes.get(&pane)
}
/// Returns the internal state of the given [`Pane`] with mutability, if it
/// exists.
- pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> {
- self.panes.get_mut(pane)
+ pub fn get_mut(&mut self, pane: Pane) -> Option<&mut T> {
+ self.panes.get_mut(&pane)
}
/// Returns an iterator over all the panes of the [`State`], alongside its
@@ -104,13 +104,13 @@ impl<T> State<T> {
/// Returns the adjacent [`Pane`] of another [`Pane`] in the given
/// direction, if there is one.
- pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
+ pub fn adjacent(&self, pane: Pane, direction: Direction) -> Option<Pane> {
let regions = self
.internal
.layout
.pane_regions(0.0, Size::new(4096.0, 4096.0));
- let current_region = regions.get(pane)?;
+ let current_region = regions.get(&pane)?;
let target = match direction {
Direction::Left => {
@@ -142,7 +142,7 @@ impl<T> State<T> {
pub fn split(
&mut self,
axis: Axis,
- pane: &Pane,
+ pane: Pane,
state: T,
) -> Option<(Pane, Split)> {
self.split_node(axis, Some(pane), state, false)
@@ -151,32 +151,32 @@ impl<T> State<T> {
/// Split a target [`Pane`] with a given [`Pane`] on a given [`Region`].
///
/// Panes will be swapped by default for [`Region::Center`].
- pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) {
+ pub fn split_with(&mut self, target: Pane, pane: Pane, region: Region) {
match region {
Region::Center => self.swap(pane, target),
Region::Edge(edge) => match edge {
Edge::Top => {
- self.split_and_swap(Axis::Horizontal, target, pane, true)
+ self.split_and_swap(Axis::Horizontal, target, pane, true);
}
Edge::Bottom => {
- self.split_and_swap(Axis::Horizontal, target, pane, false)
+ self.split_and_swap(Axis::Horizontal, target, pane, false);
}
Edge::Left => {
- self.split_and_swap(Axis::Vertical, target, pane, true)
+ self.split_and_swap(Axis::Vertical, target, pane, true);
}
Edge::Right => {
- self.split_and_swap(Axis::Vertical, target, pane, false)
+ self.split_and_swap(Axis::Vertical, target, pane, false);
}
},
}
}
/// Drops the given [`Pane`] into the provided [`Target`].
- pub fn drop(&mut self, pane: &Pane, target: Target) {
+ pub fn drop(&mut self, pane: Pane, target: Target) {
match target {
Target::Edge(edge) => self.move_to_edge(pane, edge),
Target::Pane(target, region) => {
- self.split_with(&target, pane, region)
+ self.split_with(target, pane, region);
}
}
}
@@ -184,7 +184,7 @@ impl<T> State<T> {
fn split_node(
&mut self,
axis: Axis,
- pane: Option<&Pane>,
+ pane: Option<Pane>,
state: T,
inverse: bool,
) -> Option<(Pane, Split)> {
@@ -222,33 +222,35 @@ impl<T> State<T> {
fn split_and_swap(
&mut self,
axis: Axis,
- target: &Pane,
- pane: &Pane,
+ target: Pane,
+ pane: Pane,
swap: bool,
) {
if let Some((state, _)) = self.close(pane) {
if let Some((new_pane, _)) = self.split(axis, target, state) {
if swap {
- self.swap(target, &new_pane);
+ self.swap(target, new_pane);
}
}
}
}
/// Move [`Pane`] to an [`Edge`] of the [`PaneGrid`].
- pub fn move_to_edge(&mut self, pane: &Pane, edge: Edge) {
+ ///
+ /// [`PaneGrid`]: super::PaneGrid
+ pub fn move_to_edge(&mut self, pane: Pane, edge: Edge) {
match edge {
Edge::Top => {
- self.split_major_node_and_swap(Axis::Horizontal, pane, true)
+ self.split_major_node_and_swap(Axis::Horizontal, pane, true);
}
Edge::Bottom => {
- self.split_major_node_and_swap(Axis::Horizontal, pane, false)
+ self.split_major_node_and_swap(Axis::Horizontal, pane, false);
}
Edge::Left => {
- self.split_major_node_and_swap(Axis::Vertical, pane, true)
+ self.split_major_node_and_swap(Axis::Vertical, pane, true);
}
Edge::Right => {
- self.split_major_node_and_swap(Axis::Vertical, pane, false)
+ self.split_major_node_and_swap(Axis::Vertical, pane, false);
}
}
}
@@ -256,7 +258,7 @@ impl<T> State<T> {
fn split_major_node_and_swap(
&mut self,
axis: Axis,
- pane: &Pane,
+ pane: Pane,
swap: bool,
) {
if let Some((state, _)) = self.close(pane) {
@@ -269,16 +271,16 @@ impl<T> State<T> {
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
/// will need to call this method when handling a [`DragEvent`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
- /// [`DragEvent`]: crate::widget::pane_grid::DragEvent
- pub fn swap(&mut self, a: &Pane, b: &Pane) {
+ /// [`PaneGrid`]: super::PaneGrid
+ /// [`DragEvent`]: super::DragEvent
+ pub fn swap(&mut self, a: Pane, b: Pane) {
self.internal.layout.update(&|node| match node {
Node::Split { .. } => {}
Node::Pane(pane) => {
- if pane == a {
- *node = Node::Pane(*b);
- } else if pane == b {
- *node = Node::Pane(*a);
+ if *pane == a {
+ *node = Node::Pane(b);
+ } else if *pane == b {
+ *node = Node::Pane(a);
}
}
});
@@ -292,21 +294,21 @@ impl<T> State<T> {
/// If you want to enable resize interactions in your [`PaneGrid`], you will
/// need to call this method when handling a [`ResizeEvent`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
- /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent
- pub fn resize(&mut self, split: &Split, ratio: f32) {
+ /// [`PaneGrid`]: super::PaneGrid
+ /// [`ResizeEvent`]: super::ResizeEvent
+ pub fn resize(&mut self, split: Split, ratio: f32) {
let _ = self.internal.layout.resize(split, ratio);
}
/// Closes the given [`Pane`] and returns its internal state and its closest
/// sibling, if it exists.
- pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> {
- if self.maximized == Some(*pane) {
+ pub fn close(&mut self, pane: Pane) -> Option<(T, Pane)> {
+ if self.maximized == Some(pane) {
let _ = self.maximized.take();
}
if let Some(sibling) = self.internal.layout.remove(pane) {
- self.panes.remove(pane).map(|state| (state, sibling))
+ self.panes.remove(&pane).map(|state| (state, sibling))
} else {
None
}
@@ -315,22 +317,22 @@ impl<T> State<T> {
/// Maximize the given [`Pane`]. Only this pane will be rendered by the
/// [`PaneGrid`] until [`Self::restore()`] is called.
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
- pub fn maximize(&mut self, pane: &Pane) {
- self.maximized = Some(*pane);
+ /// [`PaneGrid`]: super::PaneGrid
+ pub fn maximize(&mut self, pane: Pane) {
+ self.maximized = Some(pane);
}
/// Restore the currently maximized [`Pane`] to it's normal size. All panes
/// will be rendered by the [`PaneGrid`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
pub fn restore(&mut self) {
let _ = self.maximized.take();
}
/// Returns the maximized [`Pane`] of the [`PaneGrid`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
pub fn maximized(&self) -> Option<Pane> {
self.maximized
}
@@ -338,7 +340,7 @@ impl<T> State<T> {
/// The internal state of a [`PaneGrid`].
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone)]
pub struct Internal {
layout: Node,
@@ -349,7 +351,7 @@ impl Internal {
/// Initializes the [`Internal`] state of a [`PaneGrid`] from a
/// [`Configuration`].
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
pub fn from_configuration<T>(
panes: &mut HashMap<Pane, T>,
content: Configuration<T>,
@@ -394,16 +396,16 @@ impl Internal {
/// The current action of a [`PaneGrid`].
///
-/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid`]: super::PaneGrid
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
/// The [`PaneGrid`] is idle.
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
Idle,
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
Dragging {
/// The [`Pane`] being dragged.
pane: Pane,
@@ -412,7 +414,7 @@ pub enum Action {
},
/// A [`Split`] in the [`PaneGrid`] is being dragged.
///
- /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`PaneGrid`]: super::PaneGrid
Resizing {
/// The [`Split`] being dragged.
split: Split,
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index 2fe79f80..f4dbb6b1 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -11,7 +11,7 @@ use crate::core::{
/// The title bar of a [`Pane`].
///
-/// [`Pane`]: crate::widget::pane_grid::Pane
+/// [`Pane`]: super::Pane
#[allow(missing_debug_implementations)]
pub struct TitleBar<'a, Message, Renderer = crate::Renderer>
where
@@ -75,7 +75,7 @@ where
/// [`TitleBar`] is hovered.
///
/// [`controls`]: Self::controls
- /// [`Pane`]: crate::widget::pane_grid::Pane
+ /// [`Pane`]: super::Pane
pub fn always_show_controls(mut self) -> Self {
self.always_show_controls = true;
self
@@ -114,7 +114,7 @@ where
/// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
///
- /// [`Renderer`]: crate::Renderer
+ /// [`Renderer`]: crate::core::Renderer
pub fn draw(
&self,
tree: &Tree,
@@ -213,23 +213,27 @@ where
pub(crate) fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.pad(self.padding);
let max_size = limits.max();
- let title_layout = self
- .content
- .as_widget()
- .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let title_layout = self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
let title_size = title_layout.size();
let mut node = if let Some(controls) = &self.controls {
- let mut controls_layout = controls
- .as_widget()
- .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let mut controls_layout = controls.as_widget().layout(
+ &mut tree.children[1],
+ renderer,
+ &layout::Limits::new(Size::ZERO, max_size),
+ );
let controls_size = controls_layout.size();
let space_before_controls = max_size.width - controls_size.width;
@@ -282,7 +286,7 @@ where
controls_layout,
renderer,
operation,
- )
+ );
};
if show_title {
@@ -291,7 +295,7 @@ where
title_layout,
renderer,
operation,
- )
+ );
}
}
@@ -304,6 +308,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
let mut children = layout.children();
let padded = children.next().unwrap();
@@ -328,6 +333,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
} else {
event::Status::Ignored
@@ -342,6 +348,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
} else {
event::Status::Ignored
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 832aae6b..00c1a7ff 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -7,12 +7,12 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
-use crate::core::text::{self, Text};
+use crate::core::text::{self, Paragraph as _, Text};
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell,
- Size, Widget,
+ Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
+ Shell, Size, Widget,
};
use crate::overlay::menu::{self, Menu};
use crate::scrollable;
@@ -35,7 +35,7 @@ where
selected: Option<T>,
width: Length,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -76,7 +76,7 @@ where
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic,
font: None,
- handle: Default::default(),
+ handle: Handle::default(),
style: Default::default(),
}
}
@@ -101,11 +101,11 @@ where
/// Sets the text size of the [`PickList`].
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
- self.text_size = Some(size.into().0);
+ self.text_size = Some(size.into());
self
}
- /// Sets the text [`LineHeight`] of the [`PickList`].
+ /// Sets the text [`text::LineHeight`] of the [`PickList`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
@@ -157,11 +157,11 @@ where
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
- tree::Tag::of::<State>()
+ tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
- tree::State::new(State::new())
+ tree::State::new(State::<Renderer::Paragraph>::new())
}
fn width(&self) -> Length {
@@ -174,10 +174,12 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
+ tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
renderer,
limits,
self.width,
@@ -200,6 +202,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
update(
event,
@@ -209,7 +212,7 @@ where
self.on_selected.as_ref(),
self.selected.as_ref(),
&self.options,
- || tree.state.downcast_mut::<State>(),
+ || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
)
}
@@ -249,8 +252,8 @@ where
self.selected.as_ref(),
&self.handle,
&self.style,
- || tree.state.downcast_ref::<State>(),
- )
+ || tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
+ );
}
fn overlay<'b>(
@@ -259,7 +262,7 @@ where
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
- let state = tree.state.downcast_mut::<State>();
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
overlay(
layout,
@@ -294,28 +297,32 @@ where
}
}
-/// The local state of a [`PickList`].
+/// The state of a [`PickList`].
#[derive(Debug)]
-pub struct State {
+pub struct State<P: text::Paragraph> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
+ options: Vec<P>,
+ placeholder: P,
}
-impl State {
+impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`] for a [`PickList`].
- pub fn new() -> Self {
+ fn new() -> Self {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
+ options: Vec::new(),
+ placeholder: P::default(),
}
}
}
-impl Default for State {
+impl<P: text::Paragraph> Default for State<P> {
fn default() -> Self {
Self::new()
}
@@ -329,7 +336,7 @@ pub enum Handle<Font> {
/// This is the default.
Arrow {
/// Font size of the content.
- size: Option<f32>,
+ size: Option<Pixels>,
},
/// A custom static handle.
Static(Icon<Font>),
@@ -358,7 +365,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// Font size of the content.
- pub size: Option<f32>,
+ pub size: Option<Pixels>,
/// Line height of the content.
pub line_height: text::LineHeight,
/// The shaping strategy of the icon.
@@ -367,11 +374,12 @@ pub struct Icon<Font> {
/// Computes the layout of a [`PickList`].
pub fn layout<Renderer, T>(
+ state: &mut State<Renderer::Paragraph>,
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -385,38 +393,61 @@ 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());
- let max_width = match width {
- Length::Shrink => {
- let measure = |label: &str| -> f32 {
- let width = renderer.measure_width(
- label,
- text_size,
- font.unwrap_or_else(|| renderer.default_font()),
- text_shaping,
- );
-
- width.round()
- };
+ state.options.resize_with(options.len(), Default::default);
+
+ let option_text = Text {
+ content: "",
+ bounds: Size::new(
+ f32::INFINITY,
+ text_line_height.to_absolute(text_size).into(),
+ ),
+ size: text_size,
+ line_height: text_line_height,
+ font,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text_shaping,
+ };
- let labels = options.iter().map(ToString::to_string);
+ for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
+ let label = option.to_string();
- let labels_width = labels
- .map(|label| measure(&label))
- .fold(100.0, |candidate, current| current.max(candidate));
+ paragraph.update(Text {
+ content: &label,
+ ..option_text
+ });
+ }
- let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
+ if let Some(placeholder) = placeholder {
+ state.placeholder.update(Text {
+ content: placeholder,
+ ..option_text
+ });
+ }
- labels_width.max(placeholder_width)
+ let max_width = match width {
+ Length::Shrink => {
+ let labels_width =
+ state.options.iter().fold(0.0, |width, paragraph| {
+ f32::max(width, paragraph.min_width())
+ });
+
+ labels_width.max(
+ placeholder
+ .map(|_| state.placeholder.min_width())
+ .unwrap_or(0.0),
+ )
}
_ => 0.0,
};
let size = {
let intrinsic = Size::new(
- max_width + text_size + padding.left,
- f32::from(text_line_height.to_absolute(Pixels(text_size))),
+ max_width + text_size.0 + padding.left,
+ f32::from(text_line_height.to_absolute(text_size)),
);
limits.resolve(intrinsic).pad(padding)
@@ -427,7 +458,7 @@ where
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
/// accordingly.
-pub fn update<'a, T, Message>(
+pub fn update<'a, T, P, Message>(
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
@@ -435,10 +466,11 @@ pub fn update<'a, T, Message>(
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
- state: impl FnOnce() -> &'a mut State,
+ state: impl FnOnce() -> &'a mut State<P>,
) -> event::Status
where
T: PartialEq + Clone + 'a,
+ P: text::Paragraph + 'a,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
@@ -533,9 +565,9 @@ pub fn mouse_interaction(
/// Returns the current overlay of a [`PickList`].
pub fn overlay<'a, T, Message, Renderer>(
layout: Layout<'_>,
- state: &'a mut State,
+ state: &'a mut State<Renderer::Paragraph>,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_shaping: text::Shaping,
font: Renderer::Font,
options: &'a [T],
@@ -565,6 +597,7 @@ where
(on_selected)(option)
},
+ None,
)
.width(bounds.width)
.padding(padding)
@@ -589,7 +622,7 @@ pub fn draw<'a, T, Renderer>(
layout: Layout<'_>,
cursor: mouse::Cursor,
padding: Padding,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Renderer::Font,
@@ -597,7 +630,7 @@ pub fn draw<'a, T, Renderer>(
selected: Option<&T>,
handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style,
- state: impl FnOnce() -> &'a State,
+ state: impl FnOnce() -> &'a State<Renderer::Paragraph>,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -663,22 +696,26 @@ pub fn draw<'a, T, Renderer>(
if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
- renderer.fill_text(Text {
- content: &code_point.to_string(),
- size,
- line_height,
- font,
- color: style.handle_color,
- bounds: Rectangle {
- x: bounds.x + bounds.width - padding.horizontal(),
- y: bounds.center_y(),
- height: f32::from(line_height.to_absolute(Pixels(size))),
- ..bounds
+ renderer.fill_text(
+ Text {
+ content: &code_point.to_string(),
+ size,
+ line_height,
+ font,
+ bounds: Size::new(
+ bounds.width,
+ f32::from(line_height.to_absolute(size)),
+ ),
+ horizontal_alignment: alignment::Horizontal::Right,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping,
},
- horizontal_alignment: alignment::Horizontal::Right,
- vertical_alignment: alignment::Vertical::Center,
- shaping,
- });
+ Point::new(
+ bounds.x + bounds.width - padding.horizontal(),
+ bounds.center_y(),
+ ),
+ style.handle_color,
+ );
}
let label = selected.map(ToString::to_string);
@@ -686,27 +723,26 @@ pub fn draw<'a, T, Renderer>(
if let Some(label) = label.as_deref().or(placeholder) {
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
- renderer.fill_text(Text {
- content: label,
- size: text_size,
- line_height: text_line_height,
- font,
- color: if is_selected {
+ renderer.fill_text(
+ Text {
+ content: label,
+ size: text_size,
+ line_height: text_line_height,
+ font,
+ bounds: Size::new(
+ bounds.width - padding.horizontal(),
+ f32::from(text_line_height.to_absolute(text_size)),
+ ),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text_shaping,
+ },
+ Point::new(bounds.x + padding.left, bounds.center_y()),
+ if is_selected {
style.text_color
} else {
style.placeholder_color
},
- bounds: Rectangle {
- x: bounds.x + padding.left,
- y: bounds.center_y(),
- width: bounds.width - padding.horizontal(),
- height: f32::from(
- text_line_height.to_absolute(Pixels(text_size)),
- ),
- },
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text_shaping,
- });
+ );
}
}
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index 37c6bc72..07de72d5 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -95,6 +95,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs
index 51a541fd..1dc4da7f 100644
--- a/widget/src/qr_code.rs
+++ b/widget/src/qr_code.rs
@@ -60,6 +60,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer<Theme>,
_limits: &layout::Limits,
) -> layout::Node {
@@ -86,7 +87,7 @@ impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
let geometry =
self.state.cache.draw(renderer, bounds.size(), |frame| {
// Scale units to cell size
- frame.scale(f32::from(self.cell_size));
+ frame.scale(self.cell_size);
// Draw background
frame.fill_rectangle(
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 5b883147..57acc033 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -6,12 +6,12 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
-use crate::core::widget::Tree;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle,
- Shell, Widget,
+ Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size,
+ Widget,
};
-use crate::{Row, Text};
pub use iced_style::radio::{Appearance, StyleSheet};
@@ -80,7 +80,7 @@ where
width: Length,
size: f32,
spacing: f32,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
@@ -152,11 +152,11 @@ where
/// Sets the text size of the [`Radio`] button.
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
- /// Sets the text [`LineHeight`] of the [`Radio`] button.
+ /// Sets the text [`text::LineHeight`] of the [`Radio`] button.
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
@@ -193,6 +193,14 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -203,25 +211,35 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- Row::<(), Renderer>::new()
- .width(self.width)
- .spacing(self.spacing)
- .align_items(Alignment::Center)
- .push(Row::new().width(self.size).height(self.size))
- .push(
- Text::new(&self.label)
- .width(self.width)
- .size(
- self.text_size
- .unwrap_or_else(|| renderer.default_size()),
- )
- .line_height(self.text_line_height)
- .shaping(self.text_shaping),
- )
- .layout(renderer, limits)
+ layout::next_to_each_other(
+ &limits.width(self.width),
+ self.spacing,
+ |_| layout::Node::new(Size::new(self.size, self.size)),
+ |limits| {
+ let state = tree
+ .state
+ .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
+
+ widget::text::layout(
+ state,
+ renderer,
+ limits,
+ self.width,
+ Length::Shrink,
+ &self.label,
+ self.text_line_height,
+ self.text_size,
+ self.font,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ self.text_shaping,
+ )
+ },
+ )
}
fn on_event(
@@ -233,6 +251,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
@@ -266,7 +285,7 @@ where
fn draw(
&self,
- _state: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -326,16 +345,10 @@ where
renderer,
style,
label_layout,
- &self.label,
- self.text_size,
- self.text_line_height,
- self.font,
+ tree.state.downcast_ref(),
crate::text::Appearance {
color: custom_style.text_color,
},
- alignment::Horizontal::Left,
- alignment::Vertical::Center,
- self.text_shaping,
);
}
}
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 1db22416..7ca90fbb 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -101,7 +101,7 @@ where
}
fn diff(&self, tree: &mut Tree) {
- tree.diff_children(&self.children)
+ tree.diff_children(&self.children);
}
fn width(&self) -> Length {
@@ -114,6 +114,7 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -127,6 +128,7 @@ where
self.spacing,
self.align_items,
&self.children,
+ &mut tree.children,
)
}
@@ -137,7 +139,7 @@ where
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
- operation.container(None, &mut |operation| {
+ operation.container(None, layout.bounds(), &mut |operation| {
self.children
.iter()
.zip(&mut tree.children)
@@ -146,7 +148,7 @@ where
child
.as_widget()
.operate(state, layout, renderer, operation);
- })
+ });
});
}
@@ -159,6 +161,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> event::Status {
self.children
.iter_mut()
@@ -173,6 +176,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
})
.fold(event::Status::Ignored, event::Status::merge)
diff --git a/widget/src/rule.rs b/widget/src/rule.rs
index d703e6ae..b5c5fa55 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -72,6 +72,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 88746ac4..49aed2f0 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -46,7 +46,7 @@ where
id: None,
width: Length::Shrink,
height: Length::Shrink,
- direction: Default::default(),
+ direction: Direction::default(),
content: content.into(),
on_scroll: None,
style: Default::default(),
@@ -117,7 +117,7 @@ impl Direction {
match self {
Self::Horizontal(properties) => Some(properties),
Self::Both { horizontal, .. } => Some(horizontal),
- _ => None,
+ Self::Vertical(_) => None,
}
}
@@ -126,7 +126,7 @@ impl Direction {
match self {
Self::Vertical(properties) => Some(properties),
Self::Both { vertical, .. } => Some(vertical),
- _ => None,
+ Self::Horizontal(_) => None,
}
}
}
@@ -217,7 +217,7 @@ where
}
fn diff(&self, tree: &mut Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ tree.diff_children(std::slice::from_ref(&self.content));
}
fn width(&self) -> Length {
@@ -230,6 +230,7 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -240,7 +241,11 @@ where
self.height,
&self.direction,
|renderer, limits| {
- self.content.as_widget().layout(renderer, limits)
+ self.content.as_widget().layout(
+ &mut tree.children[0],
+ renderer,
+ limits,
+ )
},
)
}
@@ -254,10 +259,22 @@ where
) {
let state = tree.state.downcast_mut::<State>();
- operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+ let translation =
+ state.translation(self.direction, bounds, content_bounds);
+
+ operation.scrollable(
+ state,
+ self.id.as_ref().map(|id| &id.0),
+ bounds,
+ translation,
+ );
operation.container(
self.id.as_ref().map(|id| &id.0),
+ bounds,
&mut |operation| {
self.content.as_widget().operate(
&mut tree.children[0],
@@ -278,6 +295,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
update(
tree.state.downcast_mut::<State>(),
@@ -288,7 +306,7 @@ where
shell,
self.direction,
&self.on_scroll,
- |event, layout, cursor, clipboard, shell| {
+ |event, layout, cursor, clipboard, shell, viewport| {
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
@@ -297,6 +315,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
},
)
@@ -329,9 +348,9 @@ where
layout,
cursor,
viewport,
- )
+ );
},
- )
+ );
}
fn mouse_interaction(
@@ -492,6 +511,7 @@ pub fn update<Message>(
mouse::Cursor,
&mut dyn Clipboard,
&mut Shell<'_, Message>,
+ &Rectangle,
) -> event::Status,
) -> event::Status {
let bounds = layout.bounds();
@@ -518,7 +538,20 @@ pub fn update<Message>(
_ => mouse::Cursor::Unavailable,
};
- update_content(event.clone(), content, cursor, clipboard, shell)
+ let translation = state.translation(direction, bounds, content_bounds);
+
+ update_content(
+ event.clone(),
+ content,
+ cursor,
+ clipboard,
+ shell,
+ &Rectangle {
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
+ ..bounds
+ },
+ )
};
if let event::Status::Captured = event_status {
@@ -565,7 +598,7 @@ pub fn update<Message>(
match event {
touch::Event::FingerPressed { .. } => {
let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored
+ return event::Status::Ignored;
};
state.scroll_area_touched_at = Some(cursor_position);
@@ -575,7 +608,7 @@ pub fn update<Message>(
state.scroll_area_touched_at
{
let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored
+ return event::Status::Ignored;
};
let delta = Vector::new(
@@ -620,7 +653,7 @@ pub fn update<Message>(
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if let Some(scrollbar) = scrollbars.y {
let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored
+ return event::Status::Ignored;
};
state.scroll_y_to(
@@ -650,7 +683,7 @@ pub fn update<Message>(
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored
+ return event::Status::Ignored;
};
if let (Some(scroller_grabbed_at), Some(scrollbar)) =
@@ -694,7 +727,7 @@ pub fn update<Message>(
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored
+ return event::Status::Ignored;
};
if let Some(scrollbar) = scrollbars.x {
@@ -725,7 +758,7 @@ pub fn update<Message>(
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored
+ return event::Status::Ignored;
};
if let (Some(scroller_grabbed_at), Some(scrollbar)) =
@@ -1036,7 +1069,7 @@ impl operation::Scrollable for State {
}
fn scroll_to(&mut self, offset: AbsoluteOffset) {
- State::scroll_to(self, offset)
+ State::scroll_to(self, offset);
}
}
@@ -1095,6 +1128,20 @@ impl Viewport {
AbsoluteOffset { x, y }
}
+ /// Returns the [`AbsoluteOffset`] of the current [`Viewport`], but with its
+ /// alignment reversed.
+ ///
+ /// This method can be useful to switch the alignment of a [`Scrollable`]
+ /// while maintaining its scrolling position.
+ pub fn absolute_offset_reversed(&self) -> AbsoluteOffset {
+ let AbsoluteOffset { x, y } = self.absolute_offset();
+
+ AbsoluteOffset {
+ x: (self.content_bounds.width - self.bounds.width).max(0.0) - x,
+ y: (self.content_bounds.height - self.bounds.height).max(0.0) - y,
+ }
+ }
+
/// Returns the [`RelativeOffset`] of the current [`Viewport`].
pub fn relative_offset(&self) -> RelativeOffset {
let AbsoluteOffset { x, y } = self.absolute_offset();
@@ -1104,6 +1151,16 @@ impl Viewport {
RelativeOffset { x, y }
}
+
+ /// Returns the bounds of the current [`Viewport`].
+ pub fn bounds(&self) -> Rectangle {
+ self.bounds
+ }
+
+ /// Returns the content bounds of the current [`Viewport`].
+ pub fn content_bounds(&self) -> Rectangle {
+ self.content_bounds
+ }
}
impl State {
@@ -1146,7 +1203,7 @@ impl State {
(self.offset_y.absolute(bounds.height, content_bounds.height)
- delta.y)
.clamp(0.0, content_bounds.height - bounds.height),
- )
+ );
}
if bounds.width < content_bounds.width {
@@ -1307,15 +1364,15 @@ impl Scrollbars {
let ratio = bounds.height / content_bounds.height;
// min height for easier grabbing with super tall content
- let scroller_height = (bounds.height * ratio).max(2.0);
- let scroller_offset = translation.y * ratio;
+ let scroller_height = (scrollbar_bounds.height * ratio).max(2.0);
+ let scroller_offset =
+ translation.y * ratio * scrollbar_bounds.height / bounds.height;
let scroller_bounds = Rectangle {
x: bounds.x + bounds.width
- total_scrollbar_width / 2.0
- scroller_width / 2.0,
- y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height)
- .max(0.0),
+ y: (scrollbar_bounds.y + scroller_offset).max(0.0),
width: scroller_width,
height: scroller_height,
};
@@ -1342,8 +1399,8 @@ impl Scrollbars {
// Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
// is present
- let scrollbar_y_width = show_scrollbar_y
- .map_or(0.0, |v| v.width.max(v.scroller_width) + v.margin);
+ let scrollbar_y_width = y_scrollbar
+ .map_or(0.0, |scrollbar| scrollbar.total_bounds.width);
let total_scrollbar_height =
width.max(scroller_width) + 2.0 * margin;
@@ -1368,12 +1425,12 @@ impl Scrollbars {
let ratio = bounds.width / content_bounds.width;
// min width for easier grabbing with extra wide content
- let scroller_length = (bounds.width * ratio).max(2.0);
- let scroller_offset = translation.x * ratio;
+ let scroller_length = (scrollbar_bounds.width * ratio).max(2.0);
+ let scroller_offset =
+ translation.x * ratio * scrollbar_bounds.width / bounds.width;
let scroller_bounds = Rectangle {
- x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width)
- .max(0.0),
+ x: (scrollbar_bounds.x + scroller_offset).max(0.0),
y: bounds.y + bounds.height
- total_scrollbar_height / 2.0
- scroller_width / 2.0,
diff --git a/widget/src/shader.rs b/widget/src/shader.rs
new file mode 100644
index 00000000..8e334693
--- /dev/null
+++ b/widget/src/shader.rs
@@ -0,0 +1,220 @@
+//! 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 width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ _tree: &mut Tree,
+ _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)
+ }
+
+ 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..1cc484fb
--- /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, Copy, 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 3ea4391b..ac0982c8 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -137,8 +137,8 @@ where
}
/// Sets the step size of the [`Slider`].
- pub fn step(mut self, step: T) -> Self {
- self.step = step;
+ pub fn step(mut self, step: impl Into<T>) -> Self {
+ self.step = step.into();
self
}
}
@@ -169,6 +169,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -187,6 +188,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
update(
event,
@@ -221,7 +223,7 @@ where
&self.range,
theme,
&self.style,
- )
+ );
}
fn mouse_interaction(
diff --git a/widget/src/space.rs b/widget/src/space.rs
index 9a5385e8..e5a8f169 100644
--- a/widget/src/space.rs
+++ b/widget/src/space.rs
@@ -55,6 +55,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index 1ccc5d62..2d01d1ab 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -106,6 +106,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
new file mode 100644
index 00000000..1708a2e5
--- /dev/null
+++ b/widget/src/text_editor.rs
@@ -0,0 +1,708 @@
+//! Display a multi-line text input for text editing.
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+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,
+ 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,
+ }
+ }
+}
+
+/// 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 width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ 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.pad(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,
+ );
+
+ 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_code,
+ modifiers,
+ } if state.is_focused => {
+ if let Some(motion) = motion(key_code) {
+ 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_code {
+ keyboard::KeyCode::Enter => edit(Edit::Enter),
+ keyboard::KeyCode::Backspace => edit(Edit::Backspace),
+ keyboard::KeyCode::Delete => edit(Edit::Delete),
+ keyboard::KeyCode::Escape => Some(Self::Unfocus),
+ keyboard::KeyCode::C if modifiers.command() => {
+ Some(Self::Copy)
+ }
+ keyboard::KeyCode::V
+ if modifiers.command() && !modifiers.alt() =>
+ {
+ Some(Self::Paste)
+ }
+ _ => None,
+ }
+ }
+ keyboard::Event::CharacterReceived(c) if state.is_focused => {
+ edit(Edit::Insert(c))
+ }
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+}
+
+fn motion(key_code: keyboard::KeyCode) -> Option<Motion> {
+ match key_code {
+ keyboard::KeyCode::Left => Some(Motion::Left),
+ keyboard::KeyCode::Right => Some(Motion::Right),
+ keyboard::KeyCode::Up => Some(Motion::Up),
+ keyboard::KeyCode::Down => Some(Motion::Down),
+ keyboard::KeyCode::Home => Some(Motion::Home),
+ keyboard::KeyCode::End => Some(Motion::End),
+ keyboard::KeyCode::PageUp => Some(Motion::PageUp),
+ keyboard::KeyCode::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 03bcb86a..f1688746 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -17,7 +17,7 @@ use crate::core::keyboard;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
-use crate::core::text::{self, Text};
+use crate::core::text::{self, Paragraph as _, Text};
use crate::core::time::{Duration, Instant};
use crate::core::touch;
use crate::core::widget;
@@ -67,7 +67,7 @@ where
font: Option<Renderer::Font>,
width: Length,
padding: Padding,
- size: Option<f32>,
+ size: Option<Pixels>,
line_height: text::LineHeight,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
@@ -76,6 +76,9 @@ where
style: <Renderer::Theme as StyleSheet>::Style,
}
+/// The default [`Padding`] of a [`TextInput`].
+pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
+
impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
where
Message: Clone,
@@ -95,7 +98,7 @@ where
is_secure: false,
font: None,
width: Length::Fill,
- padding: Padding::new(5.0),
+ padding: DEFAULT_PADDING,
size: None,
line_height: text::LineHeight::default(),
on_input: None,
@@ -175,11 +178,11 @@ where
/// Sets the text size of the [`TextInput`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
- self.size = Some(size.into().0);
+ self.size = Some(size.into());
self
}
- /// Sets the [`LineHeight`] of the [`TextInput`].
+ /// Sets the [`text::LineHeight`] of the [`TextInput`].
pub fn line_height(
mut self,
line_height: impl Into<text::LineHeight>,
@@ -197,6 +200,32 @@ where
self
}
+ /// Lays out the [`TextInput`], overriding its [`Value`] if provided.
+ ///
+ /// [`Renderer`]: text::Renderer
+ pub fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ value: Option<&Value>,
+ ) -> layout::Node {
+ layout(
+ renderer,
+ limits,
+ self.width,
+ self.padding,
+ self.size,
+ self.font,
+ self.line_height,
+ self.icon.as_ref(),
+ tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
+ value.unwrap_or(&self.value),
+ &self.placeholder,
+ self.is_secure,
+ )
+ }
+
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided.
///
@@ -215,17 +244,13 @@ where
theme,
layout,
cursor,
- tree.state.downcast_ref::<State>(),
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
value.unwrap_or(&self.value),
- &self.placeholder,
- self.size,
- self.line_height,
- self.font,
self.on_input.is_none(),
self.is_secure,
self.icon.as_ref(),
&self.style,
- )
+ );
}
}
@@ -237,15 +262,15 @@ where
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
- tree::Tag::of::<State>()
+ tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
- tree::State::new(State::new())
+ tree::State::new(State::<Renderer::Paragraph>::new())
}
fn diff(&self, tree: &mut Tree) {
- let state = tree.state.downcast_mut::<State>();
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
// Unfocus text input if it becomes disabled
if self.on_input.is_none() {
@@ -266,6 +291,7 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -275,8 +301,13 @@ where
self.width,
self.padding,
self.size,
+ self.font,
self.line_height,
self.icon.as_ref(),
+ tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
+ &self.value,
+ &self.placeholder,
+ self.is_secure,
)
}
@@ -287,7 +318,7 @@ where
_renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
- let state = tree.state.downcast_mut::<State>();
+ let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
operation.text_input(state, self.id.as_ref().map(|id| &id.0));
@@ -302,6 +333,7 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
update(
event,
@@ -318,7 +350,7 @@ where
self.on_input.as_deref(),
self.on_paste.as_deref(),
&self.on_submit,
- || tree.state.downcast_mut::<State>(),
+ || tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
)
}
@@ -337,17 +369,13 @@ where
theme,
layout,
cursor,
- tree.state.downcast_ref::<State>(),
+ tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
&self.value,
- &self.placeholder,
- self.size,
- self.line_height,
- self.font,
self.on_input.is_none(),
self.is_secure,
self.icon.as_ref(),
&self.style,
- )
+ );
}
fn mouse_interaction(
@@ -384,7 +412,7 @@ pub struct Icon<Font> {
/// The unicode code point that will be used as the icon.
pub code_point: char,
/// The font size of the content.
- pub size: Option<f32>,
+ pub size: Option<Pixels>,
/// The spacing between the [`Icon`] and the text in a [`TextInput`].
pub spacing: f32,
/// The side of a [`TextInput`] where to display the [`Icon`].
@@ -461,29 +489,65 @@ pub fn layout<Renderer>(
limits: &layout::Limits,
width: Length,
padding: Padding,
- size: Option<f32>,
+ size: Option<Pixels>,
+ font: Option<Renderer::Font>,
line_height: text::LineHeight,
icon: Option<&Icon<Renderer::Font>>,
+ state: &mut State<Renderer::Paragraph>,
+ value: &Value,
+ placeholder: &str,
+ is_secure: bool,
) -> layout::Node
where
Renderer: text::Renderer,
{
+ 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(Pixels(text_size)));
+ .height(line_height.to_absolute(text_size));
let text_bounds = limits.resolve(Size::ZERO);
+ let placeholder_text = Text {
+ font,
+ line_height,
+ content: placeholder,
+ bounds: Size::new(f32::INFINITY, text_bounds.height),
+ size: text_size,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
+ };
+
+ state.placeholder.update(placeholder_text);
+
+ let secure_value = is_secure.then(|| value.secure());
+ let value = secure_value.as_ref().unwrap_or(value);
+
+ state.value.update(Text {
+ content: &value.to_string(),
+ ..placeholder_text
+ });
+
if let Some(icon) = icon {
- let icon_width = renderer.measure_width(
- &icon.code_point.to_string(),
- icon.size.unwrap_or_else(|| renderer.default_size()),
- icon.font,
- text::Shaping::Advanced,
- );
+ let icon_text = Text {
+ line_height,
+ content: &icon.code_point.to_string(),
+ font: icon.font,
+ size: icon.size.unwrap_or_else(|| renderer.default_size()),
+ bounds: Size::new(f32::INFINITY, text_bounds.height),
+ horizontal_alignment: alignment::Horizontal::Center,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
+ };
+
+ 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),
@@ -533,19 +597,31 @@ pub fn update<'a, Message, Renderer>(
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
value: &mut Value,
- size: Option<f32>,
+ size: Option<Pixels>,
line_height: text::LineHeight,
font: Option<Renderer::Font>,
is_secure: bool,
on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
- state: impl FnOnce() -> &'a mut State,
+ state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>,
) -> event::Status
where
Message: Clone,
Renderer: text::Renderer,
{
+ let update_cache = |state, value| {
+ replace_paragraph(
+ renderer,
+ state,
+ layout,
+ value,
+ font,
+ size,
+ line_height,
+ );
+ };
+
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
@@ -564,6 +640,7 @@ where
Some(Focus {
updated_at: now,
now,
+ is_window_focused: true,
})
})
} else {
@@ -587,11 +664,7 @@ where
};
find_cursor_position(
- renderer,
text_layout.bounds(),
- font,
- size,
- line_height,
&value,
state,
target,
@@ -616,11 +689,7 @@ where
state.cursor.select_all(value);
} else {
let position = find_cursor_position(
- renderer,
text_layout.bounds(),
- font,
- size,
- line_height,
value,
state,
target,
@@ -666,11 +735,7 @@ where
};
let position = find_cursor_position(
- renderer,
text_layout.bounds(),
- font,
- size,
- line_height,
&value,
state,
target,
@@ -688,7 +753,9 @@ where
let state = state();
if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = on_input else { return event::Status::Ignored };
+ let Some(on_input) = on_input else {
+ return event::Status::Ignored;
+ };
if state.is_pasting.is_none()
&& !state.keyboard_modifiers.command()
@@ -703,6 +770,8 @@ where
focus.updated_at = Instant::now();
+ update_cache(state, value);
+
return event::Status::Captured;
}
}
@@ -711,7 +780,9 @@ where
let state = state();
if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = on_input else { return event::Status::Ignored };
+ let Some(on_input) = on_input else {
+ return event::Status::Ignored;
+ };
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
@@ -740,6 +811,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
+
+ update_cache(state, value);
}
keyboard::KeyCode::Delete => {
if platform::is_jump_modifier_pressed(modifiers)
@@ -760,6 +833,8 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
+
+ update_cache(state, value);
}
keyboard::KeyCode::Left => {
if platform::is_jump_modifier_pressed(modifiers)
@@ -771,7 +846,7 @@ where
state.cursor.move_left_by_words(value);
}
} else if modifiers.shift() {
- state.cursor.select_left(value)
+ state.cursor.select_left(value);
} else {
state.cursor.move_left(value);
}
@@ -786,7 +861,7 @@ where
state.cursor.move_right_by_words(value);
}
} else if modifiers.shift() {
- state.cursor.select_right(value)
+ state.cursor.select_right(value);
} else {
state.cursor.move_right(value);
}
@@ -835,9 +910,13 @@ where
let message = (on_input)(editor.contents());
shell.publish(message);
+
+ update_cache(state, value);
}
keyboard::KeyCode::V => {
- if state.keyboard_modifiers.command() {
+ if state.keyboard_modifiers.command()
+ && !state.keyboard_modifiers.alt()
+ {
let content = match state.is_pasting.take() {
Some(content) => content,
None => {
@@ -865,6 +944,8 @@ where
shell.publish(message);
state.is_pasting = Some(content);
+
+ update_cache(state, value);
} else {
state.is_pasting = None;
}
@@ -919,19 +1000,38 @@ where
state.keyboard_modifiers = modifiers;
}
+ 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) => {
+ let state = state();
+
+ if let Some(focus) = &mut state.is_focused {
+ focus.is_window_focused = true;
+ focus.updated_at = Instant::now();
+
+ shell.request_redraw(window::RedrawRequest::NextFrame);
+ }
+ }
Event::Window(_, window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
- focus.now = now;
+ if focus.is_window_focused {
+ focus.now = now;
- let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
- - (now - focus.updated_at).as_millis()
- % CURSOR_BLINK_INTERVAL_MILLIS;
+ let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
+ - (now - focus.updated_at).as_millis()
+ % CURSOR_BLINK_INTERVAL_MILLIS;
- shell.request_redraw(window::RedrawRequest::At(
- now + Duration::from_millis(millis_until_redraw as u64),
- ));
+ shell.request_redraw(window::RedrawRequest::At(
+ now + Duration::from_millis(millis_until_redraw as u64),
+ ));
+ }
}
}
_ => {}
@@ -949,12 +1049,8 @@ pub fn draw<Renderer>(
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor: mouse::Cursor,
- state: &State,
+ state: &State<Renderer::Paragraph>,
value: &Value,
- placeholder: &str,
- size: Option<f32>,
- line_height: text::LineHeight,
- font: Option<Renderer::Font>,
is_disabled: bool,
is_secure: bool,
icon: Option<&Icon<Renderer::Font>>,
@@ -993,40 +1089,30 @@ pub fn draw<Renderer>(
appearance.background,
);
- if let Some(icon) = icon {
+ if icon.is_some() {
let icon_layout = children_layout.next().unwrap();
- renderer.fill_text(Text {
- content: &icon.code_point.to_string(),
- size: icon.size.unwrap_or_else(|| renderer.default_size()),
- line_height: text::LineHeight::default(),
- font: icon.font,
- color: appearance.icon_color,
- bounds: Rectangle {
- y: text_bounds.center_y(),
- ..icon_layout.bounds()
- },
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text::Shaping::Advanced,
- });
+ renderer.fill_paragraph(
+ &state.icon,
+ icon_layout.bounds().center(),
+ appearance.icon_color,
+ );
}
let text = value.to_string();
- let font = font.unwrap_or_else(|| renderer.default_font());
- let size = size.unwrap_or_else(|| renderer.default_size());
- let (cursor, offset) = if let Some(focus) = &state.is_focused {
+ let (cursor, offset) = if let Some(focus) = state
+ .is_focused
+ .as_ref()
+ .filter(|focus| focus.is_window_focused)
+ {
match state.cursor.state(value) {
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(
- renderer,
+ &state.value,
text_bounds,
- value,
- size,
position,
- font,
);
let is_cursor_visible = ((focus.now - focus.updated_at)
@@ -1062,22 +1148,16 @@ pub fn draw<Renderer>(
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(
- renderer,
+ &state.value,
text_bounds,
- value,
- size,
left,
- font,
);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(
- renderer,
+ &state.value,
text_bounds,
- value,
- size,
right,
- font,
);
let width = right_position - left_position;
@@ -1109,12 +1189,7 @@ pub fn draw<Renderer>(
(None, 0.0)
};
- let text_width = renderer.measure_width(
- if text.is_empty() { placeholder } else { &text },
- size,
- font,
- text::Shaping::Advanced,
- );
+ let text_width = state.value.min_width();
let render = |renderer: &mut Renderer| {
if let Some((cursor, color)) = cursor {
@@ -1123,32 +1198,26 @@ pub fn draw<Renderer>(
renderer.with_translation(Vector::ZERO, |_| {});
}
- renderer.fill_text(Text {
- content: if text.is_empty() { placeholder } else { &text },
- color: if text.is_empty() {
+ renderer.fill_paragraph(
+ if text.is_empty() {
+ &state.placeholder
+ } else {
+ &state.value
+ },
+ Point::new(text_bounds.x, text_bounds.center_y()),
+ if text.is_empty() {
theme.placeholder_color(style)
} else if is_disabled {
theme.disabled_color(style)
} else {
theme.value_color(style)
},
- font,
- bounds: Rectangle {
- y: text_bounds.center_y(),
- width: f32::INFINITY,
- ..text_bounds
- },
- size,
- line_height,
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Center,
- shaping: text::Shaping::Advanced,
- });
+ );
};
if text_width > text_bounds.width {
renderer.with_layer(text_bounds, |renderer| {
- renderer.with_translation(Vector::new(-offset, 0.0), render)
+ renderer.with_translation(Vector::new(-offset, 0.0), render);
});
} else {
render(renderer);
@@ -1174,7 +1243,10 @@ pub fn mouse_interaction(
/// The state of a [`TextInput`].
#[derive(Debug, Default, Clone)]
-pub struct State {
+pub struct State<P: text::Paragraph> {
+ value: P,
+ placeholder: P,
+ icon: P,
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
@@ -1188,9 +1260,10 @@ pub struct State {
struct Focus {
updated_at: Instant,
now: Instant,
+ is_window_focused: bool,
}
-impl State {
+impl<P: text::Paragraph> State<P> {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self {
Self::default()
@@ -1199,6 +1272,9 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self {
Self {
+ value: P::default(),
+ placeholder: P::default(),
+ icon: P::default(),
is_focused: None,
is_dragging: false,
is_pasting: None,
@@ -1225,6 +1301,7 @@ impl State {
self.is_focused = Some(Focus {
updated_at: now,
now,
+ is_window_focused: true,
});
self.move_cursor_to_end();
@@ -1256,35 +1333,35 @@ impl State {
}
}
-impl operation::Focusable for State {
+impl<P: text::Paragraph> operation::Focusable for State<P> {
fn is_focused(&self) -> bool {
State::is_focused(self)
}
fn focus(&mut self) {
- State::focus(self)
+ State::focus(self);
}
fn unfocus(&mut self) {
- State::unfocus(self)
+ State::unfocus(self);
}
}
-impl operation::TextInput for State {
+impl<P: text::Paragraph> operation::TextInput for State<P> {
fn move_cursor_to_front(&mut self) {
- State::move_cursor_to_front(self)
+ State::move_cursor_to_front(self);
}
fn move_cursor_to_end(&mut self) {
- State::move_cursor_to_end(self)
+ State::move_cursor_to_end(self);
}
fn move_cursor_to(&mut self, position: usize) {
- State::move_cursor_to(self, position)
+ State::move_cursor_to(self, position);
}
fn select_all(&mut self) {
- State::select_all(self)
+ State::select_all(self);
}
}
@@ -1300,17 +1377,11 @@ mod platform {
}
}
-fn offset<Renderer>(
- renderer: &Renderer,
+fn offset<P: text::Paragraph>(
text_bounds: Rectangle,
- font: Renderer::Font,
- size: f32,
value: &Value,
- state: &State,
-) -> f32
-where
- Renderer: text::Renderer,
-{
+ state: &State<P>,
+) -> f32 {
if state.is_focused() {
let cursor = state.cursor();
@@ -1320,12 +1391,9 @@ where
};
let (_, offset) = measure_cursor_and_scroll_offset(
- renderer,
+ &state.value,
text_bounds,
- value,
- size,
focus_position,
- font,
);
offset
@@ -1334,72 +1402,72 @@ where
}
}
-fn measure_cursor_and_scroll_offset<Renderer>(
- renderer: &Renderer,
+fn measure_cursor_and_scroll_offset(
+ paragraph: &impl text::Paragraph,
text_bounds: Rectangle,
- value: &Value,
- size: f32,
cursor_index: usize,
- font: Renderer::Font,
-) -> (f32, f32)
-where
- Renderer: text::Renderer,
-{
- let text_before_cursor = value.until(cursor_index).to_string();
+) -> (f32, f32) {
+ let grapheme_position = paragraph
+ .grapheme_position(0, cursor_index)
+ .unwrap_or(Point::ORIGIN);
- let text_value_width = renderer.measure_width(
- &text_before_cursor,
- size,
- font,
- text::Shaping::Advanced,
- );
-
- let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0);
+ let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
- (text_value_width, offset)
+ (grapheme_position.x, offset)
}
/// Computes the position of the text cursor at the given X coordinate of
/// a [`TextInput`].
-fn find_cursor_position<Renderer>(
- renderer: &Renderer,
+fn find_cursor_position<P: text::Paragraph>(
text_bounds: Rectangle,
- font: Option<Renderer::Font>,
- size: Option<f32>,
- line_height: text::LineHeight,
value: &Value,
- state: &State,
+ state: &State<P>,
x: f32,
-) -> Option<usize>
-where
- Renderer: text::Renderer,
-{
- let font = font.unwrap_or_else(|| renderer.default_font());
- let size = size.unwrap_or_else(|| renderer.default_size());
-
- let offset = offset(renderer, text_bounds, font, size, value, state);
+) -> Option<usize> {
+ let offset = offset(text_bounds, value, state);
let value = value.to_string();
- let char_offset = renderer
- .hit_test(
- &value,
- size,
- line_height,
- font,
- Size::INFINITY,
- text::Shaping::Advanced,
- Point::new(x + offset, text_bounds.height / 2.0),
- true,
- )
+ let char_offset = state
+ .value
+ .hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
Some(
unicode_segmentation::UnicodeSegmentation::graphemes(
- &value[..char_offset],
+ &value[..char_offset.min(value.len())],
true,
)
.count(),
)
}
+fn replace_paragraph<Renderer>(
+ renderer: &Renderer,
+ state: &mut State<Renderer::Paragraph>,
+ layout: Layout<'_>,
+ value: &Value,
+ font: Option<Renderer::Font>,
+ text_size: Option<Pixels>,
+ line_height: text::LineHeight,
+) where
+ Renderer: text::Renderer,
+{
+ let font = font.unwrap_or_else(|| renderer.default_font());
+ let text_size = text_size.unwrap_or_else(|| renderer.default_size());
+
+ let mut children_layout = layout.children();
+ let text_bounds = children_layout.next().unwrap().bounds();
+
+ state.value = Renderer::Paragraph::with_text(Text {
+ font,
+ line_height,
+ content: &value.to_string(),
+ bounds: Size::new(f32::INFINITY, text_bounds.height),
+ size: text_size,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: text::Shaping::Advanced,
+ });
+}
+
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
diff --git a/widget/src/text_input/cursor.rs b/widget/src/text_input/cursor.rs
index 9680dfd7..f682b17d 100644
--- a/widget/src/text_input/cursor.rs
+++ b/widget/src/text_input/cursor.rs
@@ -56,7 +56,7 @@ impl Cursor {
State::Selection { start, end } => {
Some((start.min(end), start.max(end)))
}
- _ => None,
+ State::Index(_) => None,
}
}
@@ -65,11 +65,11 @@ impl Cursor {
}
pub(crate) fn move_right(&mut self, value: &Value) {
- self.move_right_by_amount(value, 1)
+ self.move_right_by_amount(value, 1);
}
pub(crate) fn move_right_by_words(&mut self, value: &Value) {
- self.move_to(value.next_end_of_word(self.right(value)))
+ self.move_to(value.next_end_of_word(self.right(value)));
}
pub(crate) fn move_right_by_amount(
@@ -79,7 +79,7 @@ impl Cursor {
) {
match self.state(value) {
State::Index(index) => {
- self.move_to(index.saturating_add(amount).min(value.len()))
+ self.move_to(index.saturating_add(amount).min(value.len()));
}
State::Selection { start, end } => self.move_to(end.max(start)),
}
@@ -89,7 +89,7 @@ impl Cursor {
match self.state(value) {
State::Index(index) if index > 0 => self.move_to(index - 1),
State::Selection { start, end } => self.move_to(start.min(end)),
- _ => self.move_to(0),
+ State::Index(_) => self.move_to(0),
}
}
@@ -108,10 +108,10 @@ impl Cursor {
pub(crate) fn select_left(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) if index > 0 => {
- self.select_range(index, index - 1)
+ self.select_range(index, index - 1);
}
State::Selection { start, end } if end > 0 => {
- self.select_range(start, end - 1)
+ self.select_range(start, end - 1);
}
_ => {}
}
@@ -120,10 +120,10 @@ impl Cursor {
pub(crate) fn select_right(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) if index < value.len() => {
- self.select_range(index, index + 1)
+ self.select_range(index, index + 1);
}
State::Selection { start, end } if end < value.len() => {
- self.select_range(start, end + 1)
+ self.select_range(start, end + 1);
}
_ => {}
}
@@ -132,10 +132,10 @@ impl Cursor {
pub(crate) fn select_left_by_words(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) => {
- self.select_range(index, value.previous_start_of_word(index))
+ self.select_range(index, value.previous_start_of_word(index));
}
State::Selection { start, end } => {
- self.select_range(start, value.previous_start_of_word(end))
+ self.select_range(start, value.previous_start_of_word(end));
}
}
}
@@ -143,10 +143,10 @@ impl Cursor {
pub(crate) fn select_right_by_words(&mut self, value: &Value) {
match self.state(value) {
State::Index(index) => {
- self.select_range(index, value.next_end_of_word(index))
+ self.select_range(index, value.next_end_of_word(index));
}
State::Selection { start, end } => {
- self.select_range(start, value.next_end_of_word(end))
+ self.select_range(start, value.next_end_of_word(end));
}
}
}
diff --git a/widget/src/text_input/value.rs b/widget/src/text_input/value.rs
index cf4da562..46a1f754 100644
--- a/widget/src/text_input/value.rs
+++ b/widget/src/text_input/value.rs
@@ -2,7 +2,7 @@ use unicode_segmentation::UnicodeSegmentation;
/// The value of a [`TextInput`].
///
-/// [`TextInput`]: crate::widget::TextInput
+/// [`TextInput`]: super::TextInput
// TODO: Reduce allocations, cache results (?)
#[derive(Debug, Clone)]
pub struct Value {
@@ -89,11 +89,6 @@ impl Value {
Self { graphemes }
}
- /// Converts the [`Value`] into a `String`.
- pub fn to_string(&self) -> String {
- self.graphemes.concat()
- }
-
/// Inserts a new `char` at the given grapheme `index`.
pub fn insert(&mut self, index: usize, c: char) {
self.graphemes.insert(index, c.to_string());
@@ -131,3 +126,9 @@ impl Value {
}
}
}
+
+impl std::fmt::Display for Value {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&self.graphemes.concat())
+ }
+}
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index 1b31765f..476c8330 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -6,12 +6,12 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::text;
use crate::core::touch;
-use crate::core::widget::Tree;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle,
- Shell, Widget,
+ Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size,
+ Widget,
};
-use crate::{Row, Text};
pub use crate::style::toggler::{Appearance, StyleSheet};
@@ -42,7 +42,7 @@ where
label: Option<String>,
width: Length,
size: f32,
- text_size: Option<f32>,
+ text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal,
text_shaping: text::Shaping,
@@ -85,7 +85,7 @@ where
text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left,
text_shaping: text::Shaping::Basic,
- spacing: 0.0,
+ spacing: Self::DEFAULT_SIZE / 2.0,
font: None,
style: Default::default(),
}
@@ -105,11 +105,11 @@ where
/// Sets the text size o the [`Toggler`].
pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
- self.text_size = Some(text_size.into().0);
+ self.text_size = Some(text_size.into());
self
}
- /// Sets the text [`LineHeight`] of the [`Toggler`].
+ /// Sets the text [`text::LineHeight`] of the [`Toggler`].
pub fn text_line_height(
mut self,
line_height: impl Into<text::LineHeight>,
@@ -136,9 +136,9 @@ where
self
}
- /// Sets the [`Font`] of the text of the [`Toggler`]
+ /// Sets the [`Renderer::Font`] of the text of the [`Toggler`]
///
- /// [`Font`]: crate::text::Renderer::Font
+ /// [`Renderer::Font`]: crate::core::text::Renderer
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
@@ -160,6 +160,14 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -170,32 +178,41 @@ where
fn layout(
&self,
+ tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let mut row = Row::<(), Renderer>::new()
- .width(self.width)
- .spacing(self.spacing)
- .align_items(Alignment::Center);
-
- if let Some(label) = &self.label {
- row = row.push(
- Text::new(label)
- .horizontal_alignment(self.text_alignment)
- .font(self.font.unwrap_or_else(|| renderer.default_font()))
- .width(self.width)
- .size(
- self.text_size
- .unwrap_or_else(|| renderer.default_size()),
+ let limits = limits.width(self.width);
+
+ layout::next_to_each_other(
+ &limits,
+ self.spacing,
+ |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
+ |limits| {
+ if let Some(label) = self.label.as_deref() {
+ let state = tree
+ .state
+ .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
+
+ widget::text::layout(
+ state,
+ renderer,
+ limits,
+ self.width,
+ Length::Shrink,
+ label,
+ self.text_line_height,
+ self.text_size,
+ self.font,
+ self.text_alignment,
+ alignment::Vertical::Top,
+ self.text_shaping,
)
- .line_height(self.text_line_height)
- .shaping(self.text_shaping),
- );
- }
-
- row = row.push(Row::new().width(2.0 * self.size).height(self.size));
-
- row.layout(renderer, limits)
+ } else {
+ layout::Node::new(Size::ZERO)
+ }
+ },
+ )
}
fn on_event(
@@ -207,6 +224,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
@@ -242,7 +260,7 @@ where
fn draw(
&self,
- _state: &Tree,
+ tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
@@ -258,28 +276,21 @@ where
const SPACE_RATIO: f32 = 0.05;
let mut children = layout.children();
+ let toggler_layout = children.next().unwrap();
- if let Some(label) = &self.label {
+ if self.label.is_some() {
let label_layout = children.next().unwrap();
crate::text::draw(
renderer,
style,
label_layout,
- label,
- self.text_size,
- self.text_line_height,
- self.font,
- Default::default(),
- self.text_alignment,
- alignment::Vertical::Center,
- self.text_shaping,
+ tree.state.downcast_ref(),
+ crate::text::Appearance::default(),
);
}
- let toggler_layout = children.next().unwrap();
let bounds = toggler_layout.bounds();
-
let is_mouse_over = cursor.is_over(layout.bounds());
let style = if is_mouse_over {
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index 2dc3da01..9e102c56 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -107,11 +107,14 @@ where
Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
fn children(&self) -> Vec<widget::Tree> {
- vec![widget::Tree::new(&self.content)]
+ vec![
+ widget::Tree::new(&self.content),
+ widget::Tree::new(&self.tooltip as &dyn Widget<Message, _>),
+ ]
}
fn diff(&self, tree: &mut widget::Tree) {
- tree.diff_children(std::slice::from_ref(&self.content))
+ tree.diff_children(&[self.content.as_widget(), &self.tooltip]);
}
fn state(&self) -> widget::tree::State {
@@ -132,10 +135,13 @@ where
fn layout(
&self,
+ tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- self.content.as_widget().layout(renderer, limits)
+ self.content
+ .as_widget()
+ .layout(&mut tree.children[0], renderer, limits)
}
fn on_event(
@@ -147,14 +153,23 @@ where
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
) -> 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,
@@ -163,6 +178,7 @@ where
renderer,
clipboard,
shell,
+ viewport,
)
}
@@ -212,8 +228,10 @@ where
) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_ref::<State>();
+ let mut children = tree.children.iter_mut();
+
let content = self.content.as_widget_mut().overlay(
- &mut tree.children[0],
+ children.next().unwrap(),
layout,
renderer,
);
@@ -223,6 +241,7 @@ where
layout.position(),
Box::new(Overlay {
tooltip: &self.tooltip,
+ state: children.next().unwrap(),
cursor_position,
content_bounds: layout.bounds(),
snap_within_viewport: self.snap_within_viewport,
@@ -278,7 +297,7 @@ pub enum Position {
Right,
}
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
enum State {
#[default]
Idle,
@@ -293,6 +312,7 @@ where
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
{
tooltip: &'b Text<'a, Renderer>,
+ state: &'b mut widget::Tree,
cursor_position: Point,
content_bounds: Rectangle,
snap_within_viewport: bool,
@@ -309,15 +329,17 @@ where
Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
{
fn layout(
- &self,
+ &mut self,
renderer: &Renderer,
bounds: Size,
- _position: Point,
+ position: Point,
+ _translation: Vector,
) -> layout::Node {
let viewport = Rectangle::with_size(bounds);
let text_layout = Widget::<(), Renderer>::layout(
self.tooltip,
+ self.state,
renderer,
&layout::Limits::new(
Size::ZERO,
@@ -329,45 +351,43 @@ where
);
let text_bounds = text_layout.bounds();
- let x_center = self.content_bounds.x
- + (self.content_bounds.width - text_bounds.width) / 2.0;
- let y_center = self.content_bounds.y
+ let x_center =
+ position.x + (self.content_bounds.width - text_bounds.width) / 2.0;
+ let y_center = position.y
+ (self.content_bounds.height - text_bounds.height) / 2.0;
let mut tooltip_bounds = {
let offset = match self.position {
Position::Top => Vector::new(
x_center,
- self.content_bounds.y
- - text_bounds.height
- - self.gap
- - self.padding,
+ position.y - text_bounds.height - self.gap - self.padding,
),
Position::Bottom => Vector::new(
x_center,
- self.content_bounds.y
+ position.y
+ self.content_bounds.height
+ self.gap
+ self.padding,
),
Position::Left => Vector::new(
- self.content_bounds.x
- - text_bounds.width
- - self.gap
- - self.padding,
+ position.x - text_bounds.width - self.gap - self.padding,
y_center,
),
Position::Right => Vector::new(
- self.content_bounds.x
+ position.x
+ self.content_bounds.width
+ self.gap
+ self.padding,
y_center,
),
- Position::FollowCursor => Vector::new(
- self.cursor_position.x,
- self.cursor_position.y - text_bounds.height,
- ),
+ Position::FollowCursor => {
+ let translation = position - self.content_bounds.position();
+
+ Vector::new(
+ self.cursor_position.x,
+ self.cursor_position.y - text_bounds.height,
+ ) + translation
+ }
};
Rectangle {
@@ -425,7 +445,7 @@ where
Widget::<(), Renderer>::draw(
self.tooltip,
- &widget::Tree::empty(),
+ self.state,
renderer,
theme,
&defaults,
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index 91f2b466..01d3359c 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -166,6 +166,7 @@ where
fn layout(
&self,
+ _tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
@@ -184,6 +185,7 @@ where
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
) -> event::Status {
update(
event,
@@ -218,7 +220,7 @@ where
&self.range,
theme,
&self.style,
- )
+ );
}
fn mouse_interaction(