summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
Diffstat (limited to 'widget')
-rw-r--r--widget/src/action.rs14
-rw-r--r--widget/src/button.rs4
-rw-r--r--widget/src/canvas.rs17
-rw-r--r--widget/src/canvas/program.rs4
-rw-r--r--widget/src/checkbox.rs2
-rw-r--r--widget/src/column.rs10
-rw-r--r--widget/src/combo_box.rs24
-rw-r--r--widget/src/container.rs42
-rw-r--r--widget/src/helpers.rs48
-rw-r--r--widget/src/image/viewer.rs11
-rw-r--r--widget/src/keyed/column.rs10
-rw-r--r--widget/src/lazy.rs4
-rw-r--r--widget/src/lazy/component.rs34
-rw-r--r--widget/src/lazy/responsive.rs4
-rw-r--r--widget/src/lib.rs2
-rw-r--r--widget/src/markdown.rs617
-rw-r--r--widget/src/mouse_area.rs8
-rw-r--r--widget/src/overlay/menu.rs4
-rw-r--r--widget/src/pane_grid.rs15
-rw-r--r--widget/src/pane_grid/content.rs4
-rw-r--r--widget/src/pane_grid/title_bar.rs8
-rw-r--r--widget/src/pick_list.rs8
-rw-r--r--widget/src/pin.rs2
-rw-r--r--widget/src/pop.rs26
-rw-r--r--widget/src/radio.rs2
-rw-r--r--widget/src/row.rs12
-rw-r--r--widget/src/scrollable.rs29
-rw-r--r--widget/src/shader.rs18
-rw-r--r--widget/src/shader/program.rs2
-rw-r--r--widget/src/slider.rs2
-rw-r--r--widget/src/stack.rs31
-rw-r--r--widget/src/text/rich.rs59
-rw-r--r--widget/src/text_editor.rs132
-rw-r--r--widget/src/text_input.rs151
-rw-r--r--widget/src/themer.rs4
-rw-r--r--widget/src/toggler.rs2
-rw-r--r--widget/src/tooltip.rs2
-rw-r--r--widget/src/vertical_slider.rs6
38 files changed, 970 insertions, 404 deletions
diff --git a/widget/src/action.rs b/widget/src/action.rs
index 1dd3a787..cc31e76a 100644
--- a/widget/src/action.rs
+++ b/widget/src/action.rs
@@ -6,7 +6,7 @@ use crate::core::window;
#[derive(Debug, Clone)]
pub struct Action<Message> {
message_to_publish: Option<Message>,
- redraw_request: Option<window::RedrawRequest>,
+ redraw_request: window::RedrawRequest,
event_status: event::Status,
}
@@ -14,7 +14,7 @@ impl<Message> Action<Message> {
fn new() -> Self {
Self {
message_to_publish: None,
- redraw_request: None,
+ redraw_request: window::RedrawRequest::Wait,
event_status: event::Status::Ignored,
}
}
@@ -46,7 +46,7 @@ impl<Message> Action<Message> {
/// soon as possible; without publishing any `Message`.
pub fn request_redraw() -> Self {
Self {
- redraw_request: Some(window::RedrawRequest::NextFrame),
+ redraw_request: window::RedrawRequest::NextFrame,
..Self::new()
}
}
@@ -58,7 +58,7 @@ impl<Message> Action<Message> {
/// blinking caret on a text input.
pub fn request_redraw_at(at: Instant) -> Self {
Self {
- redraw_request: Some(window::RedrawRequest::At(at)),
+ redraw_request: window::RedrawRequest::At(at),
..Self::new()
}
}
@@ -75,11 +75,7 @@ impl<Message> Action<Message> {
/// widget implementations.
pub fn into_inner(
self,
- ) -> (
- Option<Message>,
- Option<window::RedrawRequest>,
- event::Status,
- ) {
+ ) -> (Option<Message>, window::RedrawRequest, event::Status) {
(
self.message_to_publish,
self.redraw_request,
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 11839d5e..0e24328f 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -275,7 +275,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -285,7 +285,7 @@ where
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
- event.clone(),
+ event,
layout.children().next().unwrap(),
cursor,
renderer,
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 23cc3f2b..046abddf 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -218,7 +218,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -238,27 +238,18 @@ where
{
let (message, redraw_request, event_status) = action.into_inner();
+ shell.request_redraw_at(redraw_request);
+
if let Some(message) = message {
shell.publish(message);
}
- if let Some(redraw_request) = redraw_request {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
-
if event_status == event::Status::Captured {
shell.capture_event();
}
}
- if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) {
+ if shell.redraw_request() != window::RedrawRequest::NextFrame {
let mouse_interaction = self
.mouse_interaction(tree, layout, cursor, viewport, renderer);
diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs
index c68b2830..43446b64 100644
--- a/widget/src/canvas/program.rs
+++ b/widget/src/canvas/program.rs
@@ -32,7 +32,7 @@ where
fn update(
&self,
_state: &mut Self::State,
- _event: Event,
+ _event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<Action<Message>> {
@@ -82,7 +82,7 @@ where
fn update(
&self,
state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index 663bfad1..6ed3e080 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -305,7 +305,7 @@ where
fn update(
&mut self,
_tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/column.rs b/widget/src/column.rs
index c729cbdb..7200690b 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -260,7 +260,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -275,13 +275,7 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 500d2bec..f71e4a6e 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -63,7 +63,6 @@ use crate::core::renderer;
use crate::core::text;
use crate::core::time::Instant;
use crate::core::widget::{self, Widget};
-use crate::core::window;
use crate::core::{
Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme,
Vector,
@@ -513,7 +512,7 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -541,7 +540,7 @@ where
// Provide it to the widget
self.text_input.update(
&mut tree.children[0],
- event.clone(),
+ event,
layout,
cursor,
renderer,
@@ -554,16 +553,8 @@ where
shell.capture_event();
}
- if let Some(redraw_request) = local_shell.redraw_request() {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
+ shell.request_redraw_at(local_shell.redraw_request());
+ shell.request_input_method(local_shell.input_method());
// Then finally react to them here
for message in local_messages {
@@ -742,18 +733,21 @@ where
published_message_to_shell = true;
// Unfocus the input
+ let mut local_messages = Vec::new();
+ let mut local_shell = Shell::new(&mut local_messages);
self.text_input.update(
&mut tree.children[0],
- Event::Mouse(mouse::Event::ButtonPressed(
+ &Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)),
layout,
mouse::Cursor::Unavailable,
renderer,
clipboard,
- &mut Shell::new(&mut vec![]),
+ &mut local_shell,
viewport,
);
+ shell.request_input_method(local_shell.input_method());
}
});
diff --git a/widget/src/container.rs b/widget/src/container.rs
index 852481f1..86c1c7a8 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -26,6 +26,7 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::theme;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{
@@ -300,7 +301,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -714,9 +715,44 @@ pub fn bordered_box(theme: &Theme) -> Style {
/// A [`Container`] with a dark background and white text.
pub fn dark(_theme: &Theme) -> Style {
+ style(theme::palette::Pair {
+ color: color!(0x111111),
+ text: Color::WHITE,
+ })
+}
+
+/// A [`Container`] with a primary background color.
+pub fn primary(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.primary.base)
+}
+
+/// A [`Container`] with a secondary background color.
+pub fn secondary(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.secondary.base)
+}
+
+/// A [`Container`] with a success background color.
+pub fn success(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.success.base)
+}
+
+/// A [`Container`] with a danger background color.
+pub fn danger(theme: &Theme) -> Style {
+ let palette = theme.extended_palette();
+
+ style(palette.danger.base)
+}
+
+fn style(pair: theme::palette::Pair) -> Style {
Style {
- background: Some(color!(0x111111).into()),
- text_color: Some(Color::WHITE),
+ background: Some(pair.color.into()),
+ text_color: Some(pair.text),
border: border::rounded(2),
..Style::default()
}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 17cf94cc..42d0f499 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -167,7 +167,7 @@ macro_rules! text {
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::font;
/// use iced::widget::{rich_text, span};
-/// use iced::{color, Font};
+/// use iced::{color, never, Font};
///
/// #[derive(Debug, Clone)]
/// enum Message {
@@ -177,9 +177,10 @@ macro_rules! text {
/// fn view(state: &State) -> Element<'_, Message> {
/// rich_text![
/// span("I am red!").color(color!(0xff0000)),
-/// " ",
+/// span(" "),
/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }),
/// ]
+/// .on_link_click(never)
/// .size(20)
/// .into()
/// }
@@ -187,7 +188,7 @@ macro_rules! text {
#[macro_export]
macro_rules! rich_text {
() => (
- $crate::Column::new()
+ $crate::text::Rich::new()
);
($($x:expr),+ $(,)?) => (
$crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+])
@@ -633,7 +634,7 @@ where
fn update(
&mut self,
state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -836,7 +837,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -871,26 +872,28 @@ where
shell.request_redraw();
}
+ let is_visible =
+ is_hovered || self.is_top_focused || self.is_top_overlay_active;
+
if matches!(
event,
Event::Mouse(
mouse::Event::CursorMoved { .. }
| mouse::Event::ButtonReleased(_)
)
- ) || is_hovered
- || self.is_top_focused
- || self.is_top_overlay_active
+ ) || is_visible
{
+ let redraw_request = shell.redraw_request();
+
self.top.as_widget_mut().update(
- top_tree,
- event.clone(),
- top_layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
+ top_tree, event, top_layout, cursor, renderer, clipboard,
+ shell, viewport,
);
+
+ // Ignore redraw requests of invisible content
+ if !is_visible {
+ Shell::replace_redraw_request(shell, redraw_request);
+ }
};
if shell.is_event_captured() {
@@ -899,7 +902,7 @@ where
self.base.as_widget_mut().update(
base_tree,
- event.clone(),
+ event,
base_layout,
cursor,
renderer,
@@ -1136,10 +1139,11 @@ where
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::font;
/// use iced::widget::{rich_text, span};
-/// use iced::{color, Font};
+/// use iced::{color, never, Font};
///
/// #[derive(Debug, Clone)]
/// enum Message {
+/// LinkClicked(&'static str),
/// // ...
/// }
///
@@ -1149,13 +1153,14 @@ where
/// span(" "),
/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }),
/// ])
+/// .on_link_click(never)
/// .size(20)
/// .into()
/// }
/// ```
-pub fn rich_text<'a, Link, Theme, Renderer>(
+pub fn rich_text<'a, Link, Message, Theme, Renderer>(
spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a,
-) -> text::Rich<'a, Link, Theme, Renderer>
+) -> text::Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: text::Catalog + 'a,
@@ -1179,7 +1184,7 @@ where
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::font;
/// use iced::widget::{rich_text, span};
-/// use iced::{color, Font};
+/// use iced::{color, never, Font};
///
/// #[derive(Debug, Clone)]
/// enum Message {
@@ -1192,6 +1197,7 @@ where
/// " ",
/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }),
/// ]
+/// .on_link_click(never)
/// .size(20)
/// .into()
/// }
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index 20a7955f..811241a9 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -151,7 +151,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -167,7 +167,7 @@ where
return;
};
- match delta {
+ match *delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
let state = tree.state.downcast_mut::<State>();
@@ -215,6 +215,7 @@ where
}
}
+ shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
@@ -226,6 +227,8 @@ where
state.cursor_grabbed_at = Some(cursor_position);
state.starting_offset = state.current_offset;
+
+ shell.request_redraw();
shell.capture_event();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
@@ -233,6 +236,7 @@ where
if state.cursor_grabbed_at.is_some() {
state.cursor_grabbed_at = None;
+ shell.request_redraw();
shell.capture_event();
}
}
@@ -256,7 +260,7 @@ where
.max(0.0)
.round();
- let delta = position - origin;
+ let delta = *position - origin;
let x = if bounds.width < scaled_size.width {
(state.starting_offset.x - delta.x)
@@ -273,6 +277,7 @@ where
};
state.current_offset = Vector::new(x, y);
+ shell.request_redraw();
shell.capture_event();
}
}
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index ab0b0bde..313b728a 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -300,7 +300,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -315,13 +315,7 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index c6710e30..6df026de 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -198,7 +198,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -386,7 +386,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index 15b8b62e..c215de7a 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -6,7 +6,6 @@ use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
-use crate::core::window;
use crate::core::{
self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
Widget,
@@ -267,7 +266,10 @@ where
state: tree::State::new(S::default()),
children: vec![Tree::empty()],
})));
+
*self.tree.borrow_mut() = state.clone();
+ self.diff_self();
+
tree::State::new(state)
}
@@ -314,7 +316,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: core::Event,
+ event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -344,17 +346,8 @@ where
}
local_shell.revalidate_layout(|| shell.invalidate_layout());
-
- if let Some(redraw_request) = local_shell.redraw_request() {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
+ shell.request_redraw_at(local_shell.redraw_request());
+ shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut heads = self.state.take().unwrap().into_heads();
@@ -603,7 +596,7 @@ where
fn update(
&mut self,
- event: core::Event,
+ event: &core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -629,17 +622,8 @@ where
}
local_shell.revalidate_layout(|| shell.invalidate_layout());
-
- if let Some(redraw_request) = local_shell.redraw_request() {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
+ shell.request_redraw_at(local_shell.redraw_request());
+ shell.request_input_method(local_shell.input_method());
if !local_messages.is_empty() {
let mut inner =
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index 8129336e..e7c937af 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -188,7 +188,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -416,7 +416,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index b8cfa98f..31dcc205 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -12,7 +12,6 @@ mod action;
mod column;
mod mouse_area;
mod pin;
-mod row;
mod space;
mod stack;
mod themer;
@@ -28,6 +27,7 @@ pub mod pick_list;
pub mod pop;
pub mod progress_bar;
pub mod radio;
+pub mod row;
pub mod rule;
pub mod scrollable;
pub mod slider;
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs
index 858ee281..b69c663e 100644
--- a/widget/src/markdown.rs
+++ b/widget/src/markdown.rs
@@ -29,13 +29,9 @@
//! }
//!
//! fn view(&self) -> Element<'_, Message> {
-//! markdown::view(
-//! &self.markdown,
-//! markdown::Settings::default(),
-//! markdown::Style::from_palette(Theme::TokyoNightStorm.palette()),
-//! )
-//! .map(Message::LinkClicked)
-//! .into()
+//! markdown::view(&self.markdown, Theme::TokyoNight)
+//! .map(Message::LinkClicked)
+//! .into()
//! }
//!
//! fn update(state: &mut State, message: Message) {
@@ -59,6 +55,7 @@ use crate::{column, container, rich_text, row, scrollable, span, text};
use std::borrow::BorrowMut;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
+use std::mem;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
@@ -144,6 +141,7 @@ impl Content {
let mut state = State {
leftover: String::new(),
references: self.state.references.clone(),
+ images: HashSet::new(),
highlighter: None,
};
@@ -153,6 +151,7 @@ impl Content {
self.items[*index] = item;
}
+ self.state.images.extend(state.images.drain());
drop(state);
}
@@ -167,6 +166,11 @@ impl Content {
pub fn items(&self) -> &[Item] {
&self.items
}
+
+ /// Returns the URLs of the Markdown images present in the [`Content`].
+ pub fn images(&self) -> &HashSet<Url> {
+ &self.state.images
+ }
}
/// A Markdown item.
@@ -179,7 +183,14 @@ pub enum Item {
/// A code block.
///
/// You can enable the `highlighter` feature for syntax highlighting.
- CodeBlock(Vec<Text>),
+ CodeBlock {
+ /// The language of the code block, if any.
+ language: Option<String>,
+ /// The raw code of the code block.
+ code: String,
+ /// The styled lines of text in the code block.
+ lines: Vec<Text>,
+ },
/// A list.
List {
/// The first number of the list, if it is ordered.
@@ -187,6 +198,15 @@ pub enum Item {
/// The items of the list.
items: Vec<Vec<Item>>,
},
+ /// An image.
+ Image {
+ /// The destination URL of the image.
+ url: Url,
+ /// The title of the image.
+ title: String,
+ /// The alternative text of the image.
+ alt: Text,
+ },
}
/// A bunch of parsed Markdown text.
@@ -319,13 +339,9 @@ impl Span {
/// }
///
/// fn view(&self) -> Element<'_, Message> {
-/// markdown::view(
-/// &self.markdown,
-/// markdown::Settings::default(),
-/// markdown::Style::from_palette(Theme::TokyoNightStorm.palette()),
-/// )
-/// .map(Message::LinkClicked)
-/// .into()
+/// markdown::view(&self.markdown, Theme::TokyoNight)
+/// .map(Message::LinkClicked)
+/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
@@ -346,6 +362,7 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
struct State {
leftover: String,
references: HashMap<String, String>,
+ images: HashSet<Url>,
#[cfg(feature = "highlighter")]
highlighter: Option<Highlighter>,
}
@@ -367,7 +384,7 @@ impl Highlighter {
parser: iced_highlighter::Stream::new(
&iced_highlighter::Settings {
theme: iced_highlighter::Theme::Base16Ocean,
- token: language.to_string(),
+ token: language.to_owned(),
},
),
language: language.to_owned(),
@@ -436,6 +453,10 @@ fn parse_with<'a>(
mut state: impl BorrowMut<State> + 'a,
markdown: &'a str,
) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
+ enum Scope {
+ List(List),
+ }
+
struct List {
start: Option<u64>,
items: Vec<Vec<Item>>,
@@ -444,14 +465,17 @@ fn parse_with<'a>(
let broken_links = Rc::new(RefCell::new(HashSet::new()));
let mut spans = Vec::new();
- let mut code = Vec::new();
+ let mut code = String::new();
+ let mut code_language = None;
+ let mut code_lines = Vec::new();
let mut strong = false;
let mut emphasis = false;
let mut strikethrough = false;
let mut metadata = false;
let mut table = false;
let mut link = None;
- let mut lists = Vec::new();
+ let mut image = None;
+ let mut stack = Vec::new();
#[cfg(feature = "highlighter")]
let mut highlighter = None;
@@ -476,7 +500,7 @@ fn parse_with<'a>(
))
} else {
let _ = RefCell::borrow_mut(&broken_links)
- .insert(broken_link.reference.to_string());
+ .insert(broken_link.reference.into_string());
None
}
@@ -492,10 +516,18 @@ fn parse_with<'a>(
}
let produce = move |state: &mut State,
- lists: &mut Vec<List>,
+ stack: &mut Vec<Scope>,
item,
source: Range<usize>| {
- if lists.is_empty() {
+ if let Some(scope) = stack.last_mut() {
+ match scope {
+ Scope::List(list) => {
+ list.items.last_mut().expect("item context").push(item);
+ }
+ }
+
+ None
+ } else {
state.leftover = markdown[source.start..].to_owned();
Some((
@@ -503,16 +535,6 @@ fn parse_with<'a>(
&markdown[source.start..source.end],
broken_links.take(),
))
- } else {
- lists
- .last_mut()
- .expect("list context")
- .items
- .last_mut()
- .expect("item context")
- .push(item);
-
- None
}
};
@@ -549,35 +571,42 @@ fn parse_with<'a>(
None
}
+ pulldown_cmark::Tag::Image {
+ dest_url, title, ..
+ } if !metadata && !table => {
+ image = Url::parse(&dest_url)
+ .ok()
+ .map(|url| (url, title.into_string()));
+ None
+ }
pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
let prev = if spans.is_empty() {
None
} else {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Paragraph(Text::new(spans.drain(..).collect())),
source,
)
};
- lists.push(List {
+ stack.push(Scope::List(List {
start: first_item,
items: Vec::new(),
- });
+ }));
prev
}
pulldown_cmark::Tag::Item => {
- lists
- .last_mut()
- .expect("list context")
- .items
- .push(Vec::new());
+ if let Some(Scope::List(list)) = stack.last_mut() {
+ list.items.push(Vec::new());
+ }
+
None
}
pulldown_cmark::Tag::CodeBlock(
- pulldown_cmark::CodeBlockKind::Fenced(_language),
+ pulldown_cmark::CodeBlockKind::Fenced(language),
) if !metadata && !table => {
#[cfg(feature = "highlighter")]
{
@@ -587,9 +616,9 @@ fn parse_with<'a>(
.highlighter
.take()
.filter(|highlighter| {
- highlighter.language == _language.as_ref()
+ highlighter.language == language.as_ref()
})
- .unwrap_or_else(|| Highlighter::new(&_language));
+ .unwrap_or_else(|| Highlighter::new(&language));
highlighter.prepare();
@@ -597,12 +626,15 @@ fn parse_with<'a>(
});
}
+ code_language =
+ (!language.is_empty()).then(|| language.into_string());
+
let prev = if spans.is_empty() {
None
} else {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Paragraph(Text::new(spans.drain(..).collect())),
source,
)
@@ -624,7 +656,7 @@ fn parse_with<'a>(
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Heading(level, Text::new(spans.drain(..).collect())),
source,
)
@@ -646,12 +678,16 @@ fn parse_with<'a>(
None
}
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
- produce(
- state.borrow_mut(),
- &mut lists,
- Item::Paragraph(Text::new(spans.drain(..).collect())),
- source,
- )
+ if spans.is_empty() {
+ None
+ } else {
+ produce(
+ state.borrow_mut(),
+ &mut stack,
+ Item::Paragraph(Text::new(spans.drain(..).collect())),
+ source,
+ )
+ }
}
pulldown_cmark::TagEnd::Item if !metadata && !table => {
if spans.is_empty() {
@@ -659,18 +695,20 @@ fn parse_with<'a>(
} else {
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::Paragraph(Text::new(spans.drain(..).collect())),
source,
)
}
}
pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
- let list = lists.pop().expect("list context");
+ let scope = stack.pop()?;
+
+ let Scope::List(list) = scope;
produce(
state.borrow_mut(),
- &mut lists,
+ &mut stack,
Item::List {
start: list.start,
items: list.items,
@@ -678,6 +716,20 @@ fn parse_with<'a>(
source,
)
}
+ pulldown_cmark::TagEnd::Image if !metadata && !table => {
+ let (url, title) = image.take()?;
+ let alt = Text::new(spans.drain(..).collect());
+
+ let state = state.borrow_mut();
+ let _ = state.images.insert(url.clone());
+
+ produce(
+ state,
+ &mut stack,
+ Item::Image { url, title, alt },
+ source,
+ )
+ }
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
#[cfg(feature = "highlighter")]
{
@@ -686,8 +738,12 @@ fn parse_with<'a>(
produce(
state.borrow_mut(),
- &mut lists,
- Item::CodeBlock(code.drain(..).collect()),
+ &mut stack,
+ Item::CodeBlock {
+ language: code_language.take(),
+ code: mem::take(&mut code),
+ lines: code_lines.drain(..).collect(),
+ },
source,
)
}
@@ -704,8 +760,10 @@ fn parse_with<'a>(
pulldown_cmark::Event::Text(text) if !metadata && !table => {
#[cfg(feature = "highlighter")]
if let Some(highlighter) = &mut highlighter {
+ code.push_str(&text);
+
for line in text.lines() {
- code.push(Text::new(
+ code_lines.push(Text::new(
highlighter.highlight_line(line).to_vec(),
));
}
@@ -786,15 +844,25 @@ pub struct Settings {
pub code_size: Pixels,
/// The spacing to be used between elements.
pub spacing: Pixels,
+ /// The styling of the Markdown.
+ pub style: Style,
}
impl Settings {
+ /// Creates new [`Settings`] with default text size and the given [`Style`].
+ pub fn with_style(style: impl Into<Style>) -> Self {
+ Self::with_text_size(16, style)
+ }
+
/// Creates new [`Settings`] with the given base text size in [`Pixels`].
///
/// Heading levels will be adjusted automatically. Specifically,
/// the first level will be twice the base size, and then every level
/// after that will be 25% smaller.
- pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
+ pub fn with_text_size(
+ text_size: impl Into<Pixels>,
+ style: impl Into<Style>,
+ ) -> Self {
let text_size = text_size.into();
Self {
@@ -807,13 +875,20 @@ impl Settings {
h6_size: text_size,
code_size: text_size * 0.75,
spacing: text_size * 0.875,
+ style: style.into(),
}
}
}
-impl Default for Settings {
- fn default() -> Self {
- Self::with_text_size(16)
+impl From<&Theme> for Settings {
+ fn from(theme: &Theme) -> Self {
+ Self::with_style(Style::from(theme))
+ }
+}
+
+impl From<Theme> for Settings {
+ fn from(theme: Theme) -> Self {
+ Self::with_style(Style::from(theme))
}
}
@@ -845,6 +920,24 @@ impl Style {
}
}
+impl From<theme::Palette> for Style {
+ fn from(palette: theme::Palette) -> Self {
+ Self::from_palette(palette)
+ }
+}
+
+impl From<&Theme> for Style {
+ fn from(theme: &Theme) -> Self {
+ Self::from_palette(theme.palette())
+ }
+}
+
+impl From<Theme> for Style {
+ fn from(theme: Theme) -> Self {
+ Self::from_palette(theme.palette())
+ }
+}
+
/// Display a bunch of Markdown items.
///
/// You can obtain the items with [`parse`].
@@ -873,13 +966,9 @@ impl Style {
/// }
///
/// fn view(&self) -> Element<'_, Message> {
-/// markdown::view(
-/// &self.markdown,
-/// markdown::Settings::default(),
-/// markdown::Style::from_palette(Theme::TokyoNightStorm.palette()),
-/// )
-/// .map(Message::LinkClicked)
-/// .into()
+/// markdown::view(&self.markdown, Theme::TokyoNight)
+/// .map(Message::LinkClicked)
+/// .into()
/// }
///
/// fn update(state: &mut State, message: Message) {
@@ -891,109 +980,345 @@ impl Style {
/// }
/// }
/// ```
-pub fn view<'a, 'b, Theme, Renderer>(
- items: impl IntoIterator<Item = &'b Item>,
- settings: Settings,
- style: Style,
+pub fn view<'a, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: impl Into<Settings>,
) -> Element<'a, Url, Theme, Renderer>
where
Theme: Catalog + 'a,
Renderer: core::text::Renderer<Font = Font> + 'a,
{
+ view_with(items, settings, &DefaultViewer)
+}
+
+/// Runs [`view`] but with a custom [`Viewer`] to turn an [`Item`] into
+/// an [`Element`].
+///
+/// This is useful if you want to customize the look of certain Markdown
+/// elements.
+pub fn view_with<'a, Message, Theme, Renderer>(
+ items: impl IntoIterator<Item = &'a Item>,
+ settings: impl Into<Settings>,
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ let settings = settings.into();
+
+ let blocks = items
+ .into_iter()
+ .enumerate()
+ .map(|(i, item_)| item(viewer, settings, item_, i));
+
+ Element::new(column(blocks).spacing(settings.spacing))
+}
+
+/// Displays an [`Item`] using the given [`Viewer`].
+pub fn item<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ item: &'a Item,
+ index: usize,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ match item {
+ Item::Image { url, title, alt } => {
+ viewer.image(settings, url, title, alt)
+ }
+ Item::Heading(level, text) => {
+ viewer.heading(settings, level, text, index)
+ }
+ Item::Paragraph(text) => viewer.paragraph(settings, text),
+ Item::CodeBlock {
+ language,
+ code,
+ lines,
+ } => viewer.code_block(settings, language.as_deref(), code, lines),
+ Item::List { start: None, items } => {
+ viewer.unordered_list(settings, items)
+ }
+ Item::List {
+ start: Some(start),
+ items,
+ } => viewer.ordered_list(settings, *start, items),
+ }
+}
+
+/// Displays a heading using the default look.
+pub fn heading<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ level: &'a HeadingLevel,
+ text: &'a Text,
+ index: usize,
+ on_link_click: impl Fn(Url) -> Message + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
let Settings {
- text_size,
h1_size,
h2_size,
h3_size,
h4_size,
h5_size,
h6_size,
- code_size,
- spacing,
+ text_size,
+ ..
} = settings;
- let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
- Item::Heading(level, heading) => {
- container(rich_text(heading.spans(style)).size(match level {
+ container(
+ rich_text(text.spans(settings.style))
+ .on_link_click(on_link_click)
+ .size(match level {
pulldown_cmark::HeadingLevel::H1 => h1_size,
pulldown_cmark::HeadingLevel::H2 => h2_size,
pulldown_cmark::HeadingLevel::H3 => h3_size,
pulldown_cmark::HeadingLevel::H4 => h4_size,
pulldown_cmark::HeadingLevel::H5 => h5_size,
pulldown_cmark::HeadingLevel::H6 => h6_size,
- }))
- .padding(padding::top(if i > 0 {
- text_size / 2.0
- } else {
- Pixels::ZERO
- }))
- .into()
- }
- Item::Paragraph(paragraph) => {
- rich_text(paragraph.spans(style)).size(text_size).into()
- }
- Item::List { start: None, items } => {
- column(items.iter().map(|items| {
- row![
- text("•").size(text_size),
- view(
- items,
- Settings {
- spacing: settings.spacing * 0.6,
- ..settings
- },
- style
- )
- ]
- .spacing(spacing)
- .into()
- }))
- .spacing(spacing * 0.75)
- .into()
- }
- Item::List {
- start: Some(start),
- items,
- } => column(items.iter().enumerate().map(|(i, items)| {
- row![
- text!("{}.", i as u64 + *start).size(text_size),
- view(
- items,
- Settings {
- spacing: settings.spacing * 0.6,
- ..settings
- },
- style
- )
- ]
- .spacing(spacing)
- .into()
- }))
- .spacing(spacing * 0.75)
- .into(),
- Item::CodeBlock(lines) => container(
- scrollable(
- container(column(lines.iter().map(|line| {
- rich_text(line.spans(style))
- .font(Font::MONOSPACE)
- .size(code_size)
- .into()
- })))
- .padding(spacing.0 / 2.0),
+ }),
+ )
+ .padding(padding::top(if index > 0 {
+ text_size / 2.0
+ } else {
+ Pixels::ZERO
+ }))
+ .into()
+}
+
+/// Displays a paragraph using the default look.
+pub fn paragraph<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ text: &'a Text,
+ on_link_click: impl Fn(Url) -> Message + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ rich_text(text.spans(settings.style))
+ .size(settings.text_size)
+ .on_link_click(on_link_click)
+ .into()
+}
+
+/// Displays an unordered list using the default look and
+/// calling the [`Viewer`] for each bullet point item.
+pub fn unordered_list<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ items: &'a [Vec<Item>],
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ column(items.iter().map(|items| {
+ row![
+ text("•").size(settings.text_size),
+ view_with(
+ items,
+ Settings {
+ spacing: settings.spacing * 0.6,
+ ..settings
+ },
+ viewer,
+ )
+ ]
+ .spacing(settings.spacing)
+ .into()
+ }))
+ .spacing(settings.spacing * 0.75)
+ .padding([0.0, settings.spacing.0])
+ .into()
+}
+
+/// Displays an ordered list using the default look and
+/// calling the [`Viewer`] for each numbered item.
+pub fn ordered_list<'a, Message, Theme, Renderer>(
+ viewer: &impl Viewer<'a, Message, Theme, Renderer>,
+ settings: Settings,
+ start: u64,
+ items: &'a [Vec<Item>],
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ column(items.iter().enumerate().map(|(i, items)| {
+ row![
+ text!("{}.", i as u64 + start).size(settings.text_size),
+ view_with(
+ items,
+ Settings {
+ spacing: settings.spacing * 0.6,
+ ..settings
+ },
+ viewer,
)
- .direction(scrollable::Direction::Horizontal(
- scrollable::Scrollbar::default()
- .width(spacing.0 / 2.0)
- .scroller_width(spacing.0 / 2.0),
- )),
+ ]
+ .spacing(settings.spacing)
+ .into()
+ }))
+ .spacing(settings.spacing * 0.75)
+ .padding([0.0, settings.spacing.0])
+ .into()
+}
+
+/// Displays a code block using the default look.
+pub fn code_block<'a, Message, Theme, Renderer>(
+ settings: Settings,
+ lines: &'a [Text],
+ on_link_click: impl Fn(Url) -> Message + Clone + 'a,
+) -> Element<'a, Message, Theme, Renderer>
+where
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ container(
+ scrollable(
+ container(column(lines.iter().map(|line| {
+ rich_text(line.spans(settings.style))
+ .on_link_click(on_link_click.clone())
+ .font(Font::MONOSPACE)
+ .size(settings.code_size)
+ .into()
+ })))
+ .padding(settings.code_size),
)
- .width(Length::Fill)
- .padding(spacing.0 / 2.0)
+ .direction(scrollable::Direction::Horizontal(
+ scrollable::Scrollbar::default()
+ .width(settings.code_size / 2)
+ .scroller_width(settings.code_size / 2),
+ )),
+ )
+ .width(Length::Fill)
+ .padding(settings.code_size / 4)
+ .class(Theme::code_block())
+ .into()
+}
+
+/// A view strategy to display a Markdown [`Item`].j
+pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
+where
+ Self: Sized + 'a,
+ Message: 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ /// Produces a message when a link is clicked with the given [`Url`].
+ fn on_link_click(url: Url) -> Message;
+
+ /// Displays an image.
+ ///
+ /// By default, it will show a container with the image title.
+ fn image(
+ &self,
+ settings: Settings,
+ url: &'a Url,
+ title: &'a str,
+ alt: &Text,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ let _url = url;
+ let _title = title;
+
+ container(
+ rich_text(alt.spans(settings.style))
+ .on_link_click(Self::on_link_click),
+ )
+ .padding(settings.spacing.0)
.class(Theme::code_block())
- .into(),
- });
+ .into()
+ }
+
+ /// Displays a heading.
+ ///
+ /// By default, it calls [`heading`].
+ fn heading(
+ &self,
+ settings: Settings,
+ level: &'a HeadingLevel,
+ text: &'a Text,
+ index: usize,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ heading(settings, level, text, index, Self::on_link_click)
+ }
- Element::new(column(blocks).spacing(spacing))
+ /// Displays a paragraph.
+ ///
+ /// By default, it calls [`paragraph`].
+ fn paragraph(
+ &self,
+ settings: Settings,
+ text: &'a Text,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ paragraph(settings, text, Self::on_link_click)
+ }
+
+ /// Displays a code block.
+ ///
+ /// By default, it calls [`code_block`].
+ fn code_block(
+ &self,
+ settings: Settings,
+ language: Option<&'a str>,
+ code: &'a str,
+ lines: &'a [Text],
+ ) -> Element<'a, Message, Theme, Renderer> {
+ let _language = language;
+ let _code = code;
+
+ code_block(settings, lines, Self::on_link_click)
+ }
+
+ /// Displays an unordered list.
+ ///
+ /// By default, it calls [`unordered_list`].
+ fn unordered_list(
+ &self,
+ settings: Settings,
+ items: &'a [Vec<Item>],
+ ) -> Element<'a, Message, Theme, Renderer> {
+ unordered_list(self, settings, items)
+ }
+
+ /// Displays an ordered list.
+ ///
+ /// By default, it calls [`ordered_list`].
+ fn ordered_list(
+ &self,
+ settings: Settings,
+ start: u64,
+ items: &'a [Vec<Item>],
+ ) -> Element<'a, Message, Theme, Renderer> {
+ ordered_list(self, settings, start, items)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct DefaultViewer;
+
+impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
+where
+ Theme: Catalog + 'a,
+ Renderer: core::text::Renderer<Font = Font> + 'a,
+{
+ fn on_link_click(url: Url) -> Url {
+ url
+ }
}
/// The theme catalog of Markdown items.
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 9ba3cff5..c1c3ba0f 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -218,7 +218,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -228,7 +228,7 @@ where
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
- event.clone(),
+ event,
layout,
cursor,
renderer,
@@ -326,7 +326,7 @@ where
fn update<Message: Clone, Theme, Renderer>(
widget: &mut MouseArea<'_, Message, Theme, Renderer>,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
@@ -425,7 +425,7 @@ fn update<Message: Clone, Theme, Renderer>(
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if let Some(on_scroll) = widget.on_scroll.as_ref() {
- shell.publish(on_scroll(delta));
+ shell.publish(on_scroll(*delta));
shell.capture_event();
}
}
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index 611476ce..9d0539ff 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -263,7 +263,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -388,7 +388,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 5c3b343c..3ae1dfc7 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -474,7 +474,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -509,15 +509,8 @@ where
let is_picked = picked_pane == Some(pane);
content.update(
- tree,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
- viewport,
- is_picked,
+ tree, event, layout, cursor, renderer, clipboard, shell,
+ viewport, is_picked,
);
}
@@ -687,7 +680,7 @@ where
_ => {}
}
- if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) {
+ if shell.redraw_request() != window::RedrawRequest::NextFrame {
let interaction = self
.grid_interaction(action, layout, cursor)
.or_else(|| {
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index be5e5066..4d63dd18 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -242,7 +242,7 @@ where
pub(crate) fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -256,7 +256,7 @@ where
title_bar.update(
&mut tree.children[1],
- event.clone(),
+ event,
children.next().unwrap(),
cursor,
renderer,
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index 4bd2c2f6..611c3d67 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -430,7 +430,7 @@ where
pub(crate) fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -456,7 +456,7 @@ where
compact.as_widget_mut().update(
&mut tree.children[2],
- event.clone(),
+ event,
compact_layout,
cursor,
renderer,
@@ -469,7 +469,7 @@ where
controls.full.as_widget_mut().update(
&mut tree.children[1],
- event.clone(),
+ event,
controls_layout,
cursor,
renderer,
@@ -481,7 +481,7 @@ where
} else {
controls.full.as_widget_mut().update(
&mut tree.children[1],
- event.clone(),
+ event,
controls_layout,
cursor,
renderer,
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 6708e7cd..b751fcc3 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -430,7 +430,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -489,13 +489,13 @@ where
let options = self.options.borrow();
let selected = self.selected.as_ref().map(Borrow::borrow);
- let next_option = if y < 0.0 {
+ let next_option = if *y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
- } else if y > 0.0 {
+ } else if *y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
@@ -513,7 +513,7 @@ where
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
- state.keyboard_modifiers = modifiers;
+ state.keyboard_modifiers = *modifiers;
}
_ => {}
};
diff --git a/widget/src/pin.rs b/widget/src/pin.rs
index 7c1aca61..afa29398 100644
--- a/widget/src/pin.rs
+++ b/widget/src/pin.rs
@@ -177,7 +177,7 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/pop.rs b/widget/src/pop.rs
index 146cfb2b..950371ea 100644
--- a/widget/src/pop.rs
+++ b/widget/src/pop.rs
@@ -3,6 +3,7 @@ use crate::core::layout;
use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
+use crate::core::text;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
@@ -17,6 +18,7 @@ use crate::core::{
#[allow(missing_debug_implementations)]
pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
content: Element<'a, Message, Theme, Renderer>,
+ key: Option<text::Fragment<'a>>,
on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
on_hide: Option<Message>,
@@ -34,6 +36,7 @@ where
) -> Self {
Self {
content: content.into(),
+ key: None,
on_show: None,
on_resize: None,
on_hide: None,
@@ -66,6 +69,14 @@ where
self
}
+ /// Sets the key of the [`Pop`] widget, for continuity.
+ ///
+ /// If the key changes, the [`Pop`] widget will trigger again.
+ pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self {
+ self.key = Some(key.into_fragment());
+ self
+ }
+
/// Sets the distance in [`Pixels`] to use in anticipation of the
/// content popping into view.
///
@@ -77,10 +88,11 @@ where
}
}
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Default)]
struct State {
has_popped_in: bool,
last_size: Option<Size>,
+ last_key: Option<String>,
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
@@ -108,7 +120,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -118,8 +130,16 @@ where
) {
if let Event::Window(window::Event::RedrawRequested(_)) = &event {
let state = tree.state.downcast_mut::<State>();
- let bounds = layout.bounds();
+ if state.has_popped_in
+ && state.last_key.as_deref() != self.key.as_deref()
+ {
+ state.has_popped_in = false;
+ state.last_key =
+ self.key.as_ref().cloned().map(text::Fragment::into_owned);
+ }
+
+ let bounds = layout.bounds();
let top_left_distance = viewport.distance(bounds.position());
let bottom_right_distance = viewport
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 15c983df..0df4d715 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -326,7 +326,7 @@ where
fn update(
&mut self,
_state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 3b605f07..5ffeab49 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -256,7 +256,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -271,13 +271,7 @@ where
.zip(layout.children())
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
}
@@ -495,7 +489,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 312aee29..0cf75c04 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -33,8 +33,9 @@ use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
- self, Background, Clipboard, Color, Element, Event, Layout, Length,
- Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
+ self, Background, Clipboard, Color, Element, Event, InputMethod, Layout,
+ Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector,
+ Widget,
};
use crate::runtime::task::{self, Task};
use crate::runtime::Action;
@@ -516,7 +517,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -563,7 +564,8 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if let Some(scrollbar) = scrollbars.y {
- let Some(cursor_position) = cursor.position()
+ let Some(cursor_position) =
+ cursor.land().position()
else {
return;
};
@@ -635,7 +637,8 @@ where
match event {
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- let Some(cursor_position) = cursor.position() else {
+ let Some(cursor_position) = cursor.land().position()
+ else {
return;
};
@@ -726,12 +729,14 @@ where
_ => mouse::Cursor::Unavailable,
};
+ let had_input_method = shell.input_method().is_enabled();
+
let translation =
state.translation(self.direction, bounds, content_bounds);
self.content.as_widget_mut().update(
&mut tree.children[0],
- event.clone(),
+ event,
content,
cursor,
renderer,
@@ -743,6 +748,14 @@ where
..bounds
},
);
+
+ if !had_input_method {
+ if let InputMethod::Enabled { position, .. } =
+ shell.input_method_mut()
+ {
+ *position = *position - translation;
+ }
+ }
};
if matches!(
@@ -768,7 +781,7 @@ where
modifiers,
)) = event
{
- state.keyboard_modifiers = modifiers;
+ state.keyboard_modifiers = *modifiers;
return;
}
@@ -779,7 +792,7 @@ where
return;
}
- let delta = match delta {
+ let delta = match *delta {
mouse::ScrollDelta::Lines { x, y } => {
let is_shift_pressed =
state.keyboard_modifiers.shift();
diff --git a/widget/src/shader.rs b/widget/src/shader.rs
index 8ec57482..06254a1c 100644
--- a/widget/src/shader.rs
+++ b/widget/src/shader.rs
@@ -9,7 +9,6 @@ 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, Event, Length, Rectangle, Shell, Size};
use crate::renderer::wgpu::primitive;
@@ -89,7 +88,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: crate::core::Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -105,21 +104,12 @@ where
{
let (message, redraw_request, event_status) = action.into_inner();
+ shell.request_redraw_at(redraw_request);
+
if let Some(message) = message {
shell.publish(message);
}
- if let Some(redraw_request) = redraw_request {
- match redraw_request {
- window::RedrawRequest::NextFrame => {
- shell.request_redraw();
- }
- window::RedrawRequest::At(at) => {
- shell.request_redraw_at(at);
- }
- }
- }
-
if event_status == event::Status::Captured {
shell.capture_event();
}
@@ -184,7 +174,7 @@ where
fn update(
&self,
state: &mut Self::State,
- event: Event,
+ event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs
index 0fc110af..81ecc9b1 100644
--- a/widget/src/shader/program.rs
+++ b/widget/src/shader/program.rs
@@ -26,7 +26,7 @@ pub trait Program<Message> {
fn update(
&self,
_state: &mut Self::State,
- _event: shader::Event,
+ _event: &shader::Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Option<Action<Message>> {
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index 52500854..1908abc9 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -245,7 +245,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/stack.rs b/widget/src/stack.rs
index 12ed941d..df9f6162 100644
--- a/widget/src/stack.rs
+++ b/widget/src/stack.rs
@@ -207,7 +207,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
mut cursor: mouse::Cursor,
renderer: &Renderer,
@@ -216,43 +216,34 @@ where
viewport: &Rectangle,
) {
let is_over = cursor.is_over(layout.bounds());
- let is_mouse_movement =
- matches!(event, Event::Mouse(mouse::Event::CursorMoved { .. }));
+ let end = self.children.len() - 1;
- for ((child, state), layout) in self
+ for (i, ((child, state), layout)) in self
.children
.iter_mut()
.rev()
.zip(tree.children.iter_mut().rev())
.zip(layout.children().rev())
+ .enumerate()
{
child.as_widget_mut().update(
- state,
- event.clone(),
- layout,
- cursor,
- renderer,
- clipboard,
- shell,
+ state, event, layout, cursor, renderer, clipboard, shell,
viewport,
);
- if is_over
- && !is_mouse_movement
- && cursor != mouse::Cursor::Unavailable
- {
+ if shell.is_event_captured() {
+ return;
+ }
+
+ if i < end && is_over && !cursor.is_levitating() {
let interaction = child.as_widget().mouse_interaction(
state, layout, cursor, viewport, renderer,
);
if interaction != mouse::Interaction::None {
- cursor = mouse::Cursor::Unavailable;
+ cursor = cursor.levitate();
}
}
-
- if shell.is_event_captured() {
- return;
- }
}
}
diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs
index 0b499ec6..4d4a2861 100644
--- a/widget/src/text/rich.rs
+++ b/widget/src/text/rich.rs
@@ -14,8 +14,13 @@ use crate::core::{
/// A bunch of [`Rich`] text.
#[allow(missing_debug_implementations)]
-pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
-where
+pub struct Rich<
+ 'a,
+ Link,
+ Message,
+ Theme = crate::Theme,
+ Renderer = crate::Renderer,
+> where
Link: Clone + 'static,
Theme: Catalog,
Renderer: core::text::Renderer,
@@ -31,9 +36,11 @@ where
wrapping: Wrapping,
class: Theme::Class<'a>,
hovered_link: Option<usize>,
+ on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
}
-impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer>
+ Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: Catalog,
@@ -54,6 +61,7 @@ where
wrapping: Wrapping::default(),
class: Theme::default(),
hovered_link: None,
+ on_link_click: None,
}
}
@@ -127,6 +135,16 @@ where
self
}
+ /// Sets the message that will be produced when a link of the [`Rich`] text
+ /// is clicked.
+ pub fn on_link_click(
+ mut self,
+ on_link_clicked: impl Fn(Link) -> Message + 'a,
+ ) -> Self {
+ self.on_link_click = Some(Box::new(on_link_clicked));
+ self
+ }
+
/// Sets the default style of the [`Rich`] text.
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -164,7 +182,8 @@ where
}
}
-impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer> Default
+ for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
@@ -182,8 +201,8 @@ struct State<Link, P: Paragraph> {
paragraph: P,
}
-impl<Link, Theme, Renderer> Widget<Link, Theme, Renderer>
- for Rich<'_, Link, Theme, Renderer>
+impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Rich<'_, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: Catalog,
@@ -252,7 +271,8 @@ where
let style = theme.style(&self.class);
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
- let is_hovered_link = Some(index) == self.hovered_link;
+ let is_hovered_link = self.on_link_click.is_some()
+ && Some(index) == self.hovered_link;
if span.highlight.is_some()
|| span.underline
@@ -358,14 +378,18 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
- shell: &mut Shell<'_, Link>,
+ shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
+ let Some(on_link_clicked) = &self.on_link_click else {
+ return;
+ };
+
let was_hovered = self.hovered_link.is_some();
if let Some(position) = cursor.position_in(layout.bounds()) {
@@ -414,7 +438,7 @@ where
.get(span)
.and_then(|span| span.link.clone())
{
- shell.publish(link);
+ shell.publish(on_link_clicked(link));
}
}
_ => {}
@@ -509,8 +533,9 @@ where
})
}
-impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
- for Rich<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer>
+ FromIterator<Span<'a, Link, Renderer::Font>>
+ for Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'a,
Theme: Catalog,
@@ -524,16 +549,18 @@ where
}
}
-impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>>
- for Element<'a, Link, Theme, Renderer>
+impl<'a, Link, Message, Theme, Renderer>
+ From<Rich<'a, Link, Message, Theme, Renderer>>
+ for Element<'a, Message, Theme, Renderer>
where
+ Message: 'a,
Link: Clone + 'a,
Theme: Catalog + 'a,
Renderer: core::text::Renderer + 'a,
{
fn from(
- text: Rich<'a, Link, Theme, Renderer>,
- ) -> Element<'a, Link, Theme, Renderer> {
+ text: Rich<'a, Link, Message, Theme, Renderer>,
+ ) -> Element<'a, Message, Theme, Renderer> {
Element::new(text)
}
}
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index f1ec589b..7e40a56a 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -33,6 +33,7 @@
//! ```
use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
+use crate::core::input_method;
use crate::core::keyboard;
use crate::core::keyboard::key;
use crate::core::layout::{self, Layout};
@@ -46,14 +47,15 @@ use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{
- Background, Border, Color, Element, Event, Length, Padding, Pixels, Point,
- Rectangle, Shell, Size, SmolStr, Theme, Vector,
+ Background, Border, Color, Element, Event, InputMethod, Length, Padding,
+ Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector,
};
use std::borrow::Cow;
use std::cell::RefCell;
use std::fmt;
use std::ops::DerefMut;
+use std::ops::Range;
use std::sync::Arc;
pub use text::editor::{Action, Edit, Line, LineEnding, Motion};
@@ -322,6 +324,47 @@ where
self.class = class.into();
self
}
+
+ fn input_method<'b>(
+ &self,
+ state: &'b State<Highlighter>,
+ renderer: &Renderer,
+ layout: Layout<'_>,
+ ) -> InputMethod<&'b str> {
+ let Some(Focus {
+ is_window_focused: true,
+ ..
+ }) = &state.focus
+ else {
+ return InputMethod::Disabled;
+ };
+
+ let bounds = layout.bounds();
+ let internal = self.content.0.borrow_mut();
+
+ let text_bounds = bounds.shrink(self.padding);
+ let translation = text_bounds.position() - Point::ORIGIN;
+
+ let cursor = match internal.editor.cursor() {
+ Cursor::Caret(position) => position,
+ Cursor::Selection(ranges) => {
+ ranges.first().cloned().unwrap_or_default().position()
+ }
+ };
+
+ let line_height = self.line_height.to_absolute(
+ self.text_size.unwrap_or_else(|| renderer.default_size()),
+ );
+
+ let position =
+ cursor + translation + Vector::new(0.0, f32::from(line_height));
+
+ InputMethod::Enabled {
+ position,
+ purpose: input_method::Purpose::Normal,
+ preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
+ }
+ }
}
/// The content of a [`TextEditor`].
@@ -450,6 +493,7 @@ where
#[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> {
focus: Option<Focus>,
+ preedit: Option<input_method::Preedit>,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
partial_scroll: f32,
@@ -458,7 +502,7 @@ pub struct State<Highlighter: text::Highlighter> {
highlighter_format_address: usize,
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
struct Focus {
updated_at: Instant,
now: Instant,
@@ -524,6 +568,7 @@ where
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
focus: None,
+ preedit: None,
last_click: None,
drag_click: None,
partial_scroll: 0.0,
@@ -602,10 +647,10 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _renderer: &Renderer,
+ renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
@@ -637,17 +682,18 @@ where
Event::Window(window::Event::RedrawRequested(now)) => {
if let Some(focus) = &mut state.focus {
if focus.is_window_focused {
- focus.now = now;
+ focus.now = *now;
let millis_until_redraw =
Focus::CURSOR_BLINK_INTERVAL_MILLIS
- - (now - focus.updated_at).as_millis()
+ - (focus.now - focus.updated_at).as_millis()
% Focus::CURSOR_BLINK_INTERVAL_MILLIS;
shell.request_redraw_at(
- now + Duration::from_millis(
- millis_until_redraw as u64,
- ),
+ focus.now
+ + Duration::from_millis(
+ millis_until_redraw as u64,
+ ),
);
}
}
@@ -701,6 +747,28 @@ where
}));
shell.capture_event();
}
+ Update::InputMethod(update) => match update {
+ Ime::Toggle(is_open) => {
+ state.preedit =
+ is_open.then(input_method::Preedit::new);
+
+ shell.request_redraw();
+ }
+ Ime::Preedit { content, selection } => {
+ state.preedit = Some(input_method::Preedit {
+ content,
+ selection,
+ text_size: self.text_size,
+ });
+
+ shell.request_redraw();
+ }
+ Ime::Commit(text) => {
+ shell.publish(on_edit(Action::Edit(Edit::Paste(
+ Arc::new(text),
+ ))));
+ }
+ },
Update::Binding(binding) => {
fn apply_binding<
H: text::Highlighter,
@@ -827,6 +895,10 @@ where
if is_redraw {
self.last_status = Some(status);
+
+ shell.request_input_method(
+ &self.input_method(state, renderer, layout),
+ );
} else if self
.last_status
.is_some_and(|last_status| status != last_status)
@@ -1129,12 +1201,22 @@ enum Update<Message> {
Drag(Point),
Release,
Scroll(f32),
+ InputMethod(Ime),
Binding(Binding<Message>),
}
+enum Ime {
+ Toggle(bool),
+ Preedit {
+ content: String,
+ selection: Option<Range<usize>>,
+ },
+ Commit(String),
+}
+
impl<Message> Update<Message> {
fn from_event<H: Highlighter>(
- event: Event,
+ event: &Event,
state: &State<H>,
bounds: Rectangle,
padding: Padding,
@@ -1191,6 +1273,28 @@ impl<Message> Update<Message> {
}
_ => None,
},
+ Event::InputMethod(event) => match event {
+ input_method::Event::Opened | input_method::Event::Closed => {
+ Some(Update::InputMethod(Ime::Toggle(matches!(
+ event,
+ input_method::Event::Opened
+ ))))
+ }
+ input_method::Event::Preedit(content, selection)
+ if state.focus.is_some() =>
+ {
+ Some(Update::InputMethod(Ime::Preedit {
+ content: content.clone(),
+ selection: selection.clone(),
+ }))
+ }
+ input_method::Event::Commit(content)
+ if state.focus.is_some() =>
+ {
+ Some(Update::InputMethod(Ime::Commit(content.clone())))
+ }
+ _ => None,
+ },
Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
@@ -1206,9 +1310,9 @@ impl<Message> Update<Message> {
};
let key_press = KeyPress {
- key,
- modifiers,
- text,
+ key: key.clone(),
+ modifiers: *modifiers,
+ text: text.clone(),
status,
};
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 57ebe46a..ae3dfe4c 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -42,6 +42,7 @@ use editor::Editor;
use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
+use crate::core::input_method;
use crate::core::keyboard;
use crate::core::keyboard::key;
use crate::core::layout;
@@ -56,8 +57,8 @@ use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{
- Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels,
- Point, Rectangle, Shell, Size, Theme, Vector, Widget,
+ Background, Border, Color, Element, Event, InputMethod, Layout, Length,
+ Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
use crate::runtime::task::{self, Task};
use crate::runtime::Action;
@@ -391,6 +392,54 @@ where
}
}
+ fn input_method<'b>(
+ &self,
+ state: &'b State<Renderer::Paragraph>,
+ layout: Layout<'_>,
+ value: &Value,
+ ) -> InputMethod<&'b str> {
+ let Some(Focus {
+ is_window_focused: true,
+ ..
+ }) = &state.is_focused
+ else {
+ return InputMethod::Disabled;
+ };
+
+ let secure_value = self.is_secure.then(|| value.secure());
+ let value = secure_value.as_ref().unwrap_or(value);
+
+ let text_bounds = layout.children().next().unwrap().bounds();
+
+ let caret_index = match state.cursor.state(value) {
+ cursor::State::Index(position) => position,
+ cursor::State::Selection { start, end } => start.min(end),
+ };
+
+ let text = state.value.raw();
+ let (cursor_x, scroll_offset) =
+ measure_cursor_and_scroll_offset(text, text_bounds, caret_index);
+
+ let alignment_offset = alignment_offset(
+ text_bounds.width,
+ text.min_width(),
+ self.alignment,
+ );
+
+ let x = (text_bounds.x + cursor_x).floor() - scroll_offset
+ + alignment_offset;
+
+ InputMethod::Enabled {
+ position: Point::new(x, text_bounds.y + text_bounds.height),
+ purpose: if self.is_secure {
+ input_method::Purpose::Secure
+ } else {
+ input_method::Purpose::Normal
+ },
+ preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
+ }
+ }
+
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided.
///
@@ -529,7 +578,13 @@ where
};
let draw = |renderer: &mut Renderer, viewport| {
- let paragraph = if text.is_empty() {
+ let paragraph = if text.is_empty()
+ && state
+ .preedit
+ .as_ref()
+ .map(|preedit| preedit.content.is_empty())
+ .unwrap_or(true)
+ {
state.placeholder.raw()
} else {
state.value.raw()
@@ -639,7 +694,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -1197,6 +1252,52 @@ where
state.keyboard_modifiers = *modifiers;
}
+ Event::InputMethod(event) => match event {
+ input_method::Event::Opened | input_method::Event::Closed => {
+ let state = state::<Renderer>(tree);
+
+ state.preedit =
+ matches!(event, input_method::Event::Opened)
+ .then(input_method::Preedit::new);
+
+ shell.request_redraw();
+ }
+ input_method::Event::Preedit(content, selection) => {
+ let state = state::<Renderer>(tree);
+
+ if state.is_focused.is_some() {
+ state.preedit = Some(input_method::Preedit {
+ content: content.to_owned(),
+ selection: selection.clone(),
+ text_size: self.size,
+ });
+
+ shell.request_redraw();
+ }
+ }
+ input_method::Event::Commit(text) => {
+ let state = state::<Renderer>(tree);
+
+ if let Some(focus) = &mut state.is_focused {
+ let Some(on_input) = &self.on_input else {
+ return;
+ };
+
+ let mut editor =
+ Editor::new(&mut self.value, &mut state.cursor);
+ editor.paste(Value::new(text));
+
+ focus.updated_at = Instant::now();
+ state.is_pasting = None;
+
+ let message = (on_input)(editor.contents());
+ shell.publish(message);
+ shell.capture_event();
+
+ update_cache(state, &self.value);
+ }
+ }
+ },
Event::Window(window::Event::Unfocused) => {
let state = state::<Renderer>(tree);
@@ -1218,23 +1319,30 @@ where
let state = state::<Renderer>(tree);
if let Some(focus) = &mut state.is_focused {
- if focus.is_window_focused
- && matches!(
+ if focus.is_window_focused {
+ if matches!(
state.cursor.state(&self.value),
cursor::State::Index(_)
- )
- {
- focus.now = *now;
-
- let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
- - (*now - focus.updated_at).as_millis()
- % CURSOR_BLINK_INTERVAL_MILLIS;
-
- shell.request_redraw_at(
- *now + Duration::from_millis(
- millis_until_redraw as u64,
- ),
- );
+ ) {
+ focus.now = *now;
+
+ let millis_until_redraw =
+ CURSOR_BLINK_INTERVAL_MILLIS
+ - (*now - focus.updated_at).as_millis()
+ % CURSOR_BLINK_INTERVAL_MILLIS;
+
+ shell.request_redraw_at(
+ *now + Duration::from_millis(
+ millis_until_redraw as u64,
+ ),
+ );
+ }
+
+ shell.request_input_method(&self.input_method(
+ state,
+ layout,
+ &self.value,
+ ));
}
}
}
@@ -1419,6 +1527,7 @@ pub struct State<P: text::Paragraph> {
is_focused: Option<Focus>,
is_dragging: bool,
is_pasting: Option<Value>,
+ preedit: Option<input_method::Preedit>,
last_click: Option<mouse::Click>,
cursor: Cursor,
keyboard_modifiers: keyboard::Modifiers,
@@ -1431,7 +1540,7 @@ fn state<Renderer: text::Renderer>(
tree.state.downcast_mut::<State<Renderer::Paragraph>>()
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
struct Focus {
updated_at: Instant,
now: Instant,
@@ -1614,7 +1723,7 @@ fn replace_paragraph<Renderer>(
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Top,
+ vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
diff --git a/widget/src/themer.rs b/widget/src/themer.rs
index 769cc4ca..4e583882 100644
--- a/widget/src/themer.rs
+++ b/widget/src/themer.rs
@@ -113,7 +113,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
@@ -220,7 +220,7 @@ where
fn update(
&mut self,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index 56c2be1f..b711432e 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -309,7 +309,7 @@ where
fn update(
&mut self,
_state: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index a0ffe392..5bebeeac 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -192,7 +192,7 @@ where
fn update(
&mut self,
tree: &mut widget::Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index 2ed9419a..6f878fde 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -249,7 +249,7 @@ where
fn update(
&mut self,
tree: &mut Tree,
- event: Event,
+ event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
@@ -379,7 +379,7 @@ where
if state.keyboard_modifiers.control() =>
{
if cursor.is_over(layout.bounds()) {
- let delta = match delta {
+ let delta = match *delta {
mouse::ScrollDelta::Lines { x: _, y } => y,
mouse::ScrollDelta::Pixels { x: _, y } => y,
};
@@ -411,7 +411,7 @@ where
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
- state.keyboard_modifiers = modifiers;
+ state.keyboard_modifiers = *modifiers;
}
_ => {}
}