Merge branch 'master' into feat/multi-window-support

This commit is contained in:
Héctor Ramón Jiménez 2023-11-29 22:28:31 +01:00
commit e09b4e24dd
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
331 changed files with 12085 additions and 3976 deletions

View file

@ -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"
iced_renderer.workspace = true
iced_runtime.workspace = true
iced_style.workspace = true
[dependencies.iced_runtime]
version = "0.1"
path = "../runtime"
num-traits.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
[dependencies.iced_renderer]
version = "0.1"
path = "../renderer"
ouroboros.workspace = true
ouroboros.optional = true
[dependencies.iced_style]
version = "0.8"
path = "../style"
[dependencies.ouroboros]
version = "0.17"
optional = true
[dependencies.qrcode]
version = "0.12"
optional = true
default-features = false
qrcode.workspace = true
qrcode.optional = true

View file

@ -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);

View file

@ -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();

View file

@ -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.

View file

@ -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,

View file

@ -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.

View file

@ -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)

770
widget/src/combo_box.rs Normal file
View file

@ -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()
}

View file

@ -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,
})
}

View file

@ -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

View file

@ -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,
);
}
}

View file

@ -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)
},
)
);
});
});
}

53
widget/src/keyed.rs Normal file
View file

@ -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)),+])
);
}

320
widget/src/keyed/column.rs Normal file
View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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();
*self.hovered_option =
Some((cursor_position.y / option_height) as usize);
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(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,
});
);
}
}
}

View file

@ -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`]

View file

@ -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),
}

View file

@ -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,
)
};

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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

View file

@ -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());
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,
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
paragraph.update(Text {
content: &label,
..option_text
});
}
if let Some(placeholder) = placeholder {
state.placeholder.update(Text {
content: placeholder,
..option_text
});
}
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,
);
let labels_width =
state.options.iter().fold(0.0, |width, paragraph| {
f32::max(width, paragraph.min_width())
});
width.round()
};
let labels = options.iter().map(ToString::to_string);
let labels_width = labels
.map(|label| measure(&label))
.fold(100.0, |candidate, current| current.max(candidate));
let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
labels_width.max(placeholder_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,
});
);
}
}

View file

@ -95,6 +95,7 @@ where
fn layout(
&self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {

View file

@ -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(

View file

@ -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,
);
}
}

View file

@ -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)

View file

@ -72,6 +72,7 @@ where
fn layout(
&self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {

View file

@ -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,

220
widget/src/shader.rs Normal file
View file

@ -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)
}
}

View file

@ -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),
}

View file

@ -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()
}
}

View file

@ -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(

View file

@ -55,6 +55,7 @@ where
fn layout(
&self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {

View file

@ -106,6 +106,7 @@ where
fn layout(
&self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {

708
widget/src/text_editor.rs Normal file
View file

@ -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()
}
}
}

View file

@ -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 = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
let offset = ((text_value_width + 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;

View file

@ -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));
}
}
}

View file

@ -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())
}
}

View file

@ -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);
let limits = limits.width(self.width);
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()),
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 {

View file

@ -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,

View file

@ -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(