Merge branch 'master' into feat/multi-window-support
This commit is contained in:
commit
e09b4e24dd
331 changed files with 12085 additions and 3976 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
770
widget/src/combo_box.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
53
widget/src/keyed.rs
Normal 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
320
widget/src/keyed/column.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
//! Let your users split regions of your application and organize layout dynamically.
|
||||
//!
|
||||
//! [](https://gfycat.com/mixedflatjellyfish)
|
||||
//! 
|
||||
//!
|
||||
//! # 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.
|
||||
///
|
||||
/// [](https://gfycat.com/frailfreshairedaleterrier)
|
||||
/// 
|
||||
///
|
||||
/// 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`]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ where
|
|||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ where
|
|||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
|
|
|
|||
|
|
@ -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
220
widget/src/shader.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
25
widget/src/shader/event.rs
Normal file
25
widget/src/shader/event.rs
Normal 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),
|
||||
}
|
||||
62
widget/src/shader/program.rs
Normal file
62
widget/src/shader/program.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ where
|
|||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
|
|
|
|||
|
|
@ -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
708
widget/src/text_editor.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue