use crate::core::alignment;
use crate::core::clipboard::{self, Clipboard};
use crate::core::event::{self, Event};
use crate::core::keyboard;
use crate::core::keyboard::key;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::editor::{Cursor, Editor as _};
use crate::core::text::highlighter::{self, Highlighter};
use crate::core::text::{self, LineHeight, Text, Wrapping};
use crate::core::time::{Duration, Instant};
use crate::core::widget::operation;
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{
Background, Border, Color, Element, Length, Padding, Pixels, Point,
Rectangle, Shell, Size, SmolStr, Theme, Vector,
};
use crate::runtime::{task, Action as RuntimeAction, Task};
use std::cell::RefCell;
use std::fmt;
use std::ops::DerefMut;
use std::sync::Arc;
pub use text::editor::{Action, Edit, Motion};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Id(widget::Id);
impl From<widget::Id> for Id {
fn from(value: widget::Id) -> Self {
Id(value)
}
}
#[allow(missing_debug_implementations)]
pub struct TextEditor<
'a,
Highlighter,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Highlighter: text::Highlighter,
Theme: Catalog,
Renderer: text::Renderer,
{
id: Option<Id>,
content: &'a Content<Renderer>,
placeholder: Option<text::Fragment<'a>>,
font: Option<Renderer::Font>,
text_size: Option<Pixels>,
line_height: LineHeight,
width: Length,
height: Length,
padding: Padding,
wrapping: Wrapping,
class: Theme::Class<'a>,
key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings,
highlighter_format: fn(
&Highlighter::Highlight,
&Theme,
) -> highlighter::Format<Renderer::Font>,
}
impl<'a, Message, Theme, Renderer>
TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
pub fn new(content: &'a Content<Renderer>) -> Self {
Self {
id: None,
content,
placeholder: None,
font: None,
text_size: None,
line_height: LineHeight::default(),
width: Length::Fill,
height: Length::Shrink,
padding: Padding::new(5.0),
wrapping: Wrapping::default(),
class: Theme::default(),
key_binding: None,
on_edit: None,
highlighter_settings: (),
highlighter_format: |_highlight, _theme| {
highlighter::Format::default()
},
}
}
pub fn id(mut self, id: impl Into<Id>) -> Self {
self.id = Some(id.into());
self
}
}
pub fn focus<T>(id: impl Into<Id>) -> Task<T> {
task::effect(RuntimeAction::widget(operation::focusable::focus(
id.into().0,
)))
}
impl<'a, Highlighter, Message, Theme, Renderer>
TextEditor<'a, Highlighter, Message, Theme, Renderer>
where
Highlighter: text::Highlighter,
Theme: Catalog,
Renderer: text::Renderer,
{
pub fn placeholder(
mut self,
placeholder: impl text::IntoFragment<'a>,
) -> Self {
self.placeholder = Some(placeholder.into_fragment());
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
self.width = Length::from(width.into());
self
}
pub fn on_action(
mut self,
on_edit: impl Fn(Action) -> Message + 'a,
) -> Self {
self.on_edit = Some(Box::new(on_edit));
self
}
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
self.font = Some(font.into());
self
}
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.text_size = Some(size.into());
self
}
pub fn line_height(
mut self,
line_height: impl Into<text::LineHeight>,
) -> Self {
self.line_height = line_height.into();
self
}
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
self.wrapping = wrapping;
self
}
#[cfg(feature = "highlighter")]
pub fn highlight(
self,
syntax: &str,
theme: iced_highlighter::Theme,
) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer>
where
Renderer: text::Renderer<Font = crate::core::Font>,
{
self.highlight_with::<iced_highlighter::Highlighter>(
iced_highlighter::Settings {
theme,
token: syntax.to_owned(),
},
|highlight, _theme| highlight.to_format(),
)
}
pub fn highlight_with<H: text::Highlighter>(
self,
settings: H::Settings,
to_format: fn(
&H::Highlight,
&Theme,
) -> highlighter::Format<Renderer::Font>,
) -> TextEditor<'a, H, Message, Theme, Renderer> {
TextEditor {
id: self.id,
content: self.content,
placeholder: self.placeholder,
font: self.font,
text_size: self.text_size,
line_height: self.line_height,
width: self.width,
height: self.height,
padding: self.padding,
wrapping: self.wrapping,
class: self.class,
key_binding: self.key_binding,
on_edit: self.on_edit,
highlighter_settings: settings,
highlighter_format: to_format,
}
}
pub fn key_binding(
mut self,
key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
) -> Self {
self.key_binding = Some(Box::new(key_binding));
self
}
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
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,
{
pub fn new() -> Self {
Self::with_text("")
}
pub fn with_text(text: &str) -> Self {
Self(RefCell::new(Internal {
editor: R::Editor::with_text(text),
is_dirty: true,
}))
}
pub fn perform(&mut self, action: Action) {
let internal = self.0.get_mut();
internal.editor.perform(action);
internal.is_dirty = true;
}
pub fn line_count(&self) -> usize {
self.0.borrow().editor.line_count()
}
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()
}
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,
}
}
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
}
pub fn selection(&self) -> Option<String> {
self.0.borrow().editor.selection()
}
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()
}
}
#[derive(Debug)]
pub struct State<Highlighter: text::Highlighter> {
focus: Option<Focus>,
last_click: Option<mouse::Click>,
drag_click: Option<mouse::click::Kind>,
partial_scroll: f32,
highlighter: RefCell<Highlighter>,
highlighter_settings: Highlighter::Settings,
highlighter_format_address: usize,
}
#[derive(Debug, Clone, Copy)]
struct Focus {
updated_at: Instant,
now: Instant,
is_window_focused: bool,
}
impl Focus {
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
fn now() -> Self {
let now = Instant::now();
Self {
updated_at: now,
now,
is_window_focused: true,
}
}
fn is_cursor_visible(&self) -> bool {
self.is_window_focused
&& ((self.now - self.updated_at).as_millis()
/ Self::CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0
}
}
impl<Highlighter: text::Highlighter> State<Highlighter> {
pub fn is_focused(&self) -> bool {
self.focus.is_some()
}
}
impl<Highlighter: text::Highlighter> operation::Focusable
for State<Highlighter>
{
fn is_focused(&self) -> bool {
self.focus.is_some()
}
fn focus(&mut self) {
self.focus = Some(Focus::now());
}
fn unfocus(&mut self) {
self.focus = None;
}
}
impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for TextEditor<'a, Highlighter, Message, Theme, Renderer>
where
Highlighter: text::Highlighter,
Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> widget::tree::Tag {
widget::tree::Tag::of::<State<Highlighter>>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(State {
focus: None,
last_click: None,
drag_click: None,
partial_scroll: 0.0,
highlighter: RefCell::new(Highlighter::new(
&self.highlighter_settings,
)),
highlighter_settings: self.highlighter_settings.clone(),
highlighter_format_address: self.highlighter_format as usize,
})
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> iced_renderer::core::layout::Node {
let mut internal = self.content.0.borrow_mut();
let state = tree.state.downcast_mut::<State<Highlighter>>();
if state.highlighter_format_address != self.highlighter_format as usize
{
state.highlighter.borrow_mut().change_line(0);
state.highlighter_format_address = self.highlighter_format as usize;
}
if state.highlighter_settings != self.highlighter_settings {
state
.highlighter
.borrow_mut()
.update(&self.highlighter_settings);
state.highlighter_settings = self.highlighter_settings.clone();
}
let limits = limits.width(self.width).height(self.height);
internal.editor.update(
limits.shrink(self.padding).max(),
self.font.unwrap_or_else(|| renderer.default_font()),
self.text_size.unwrap_or_else(|| renderer.default_size()),
self.line_height,
self.wrapping,
state.highlighter.borrow_mut().deref_mut(),
);
match self.height {
Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => {
layout::Node::new(limits.max())
}
Length::Shrink => {
let min_bounds = internal.editor.min_bounds();
layout::Node::new(
limits
.height(min_bounds.height)
.max()
.expand(Size::new(0.0, self.padding.vertical())),
)
}
}
}
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>>();
match event {
Event::Window(window::Event::Unfocused) => {
if let Some(focus) = &mut state.focus {
focus.is_window_focused = false;
}
}
Event::Window(window::Event::Focused) => {
if let Some(focus) = &mut state.focus {
focus.is_window_focused = true;
focus.updated_at = Instant::now();
shell.request_redraw(window::RedrawRequest::NextFrame);
}
}
Event::Window(window::Event::RedrawRequested(now)) => {
if let Some(focus) = &mut state.focus {
if focus.is_window_focused {
focus.now = now;
let millis_until_redraw =
Focus::CURSOR_BLINK_INTERVAL_MILLIS
- (now - focus.updated_at).as_millis()
% Focus::CURSOR_BLINK_INTERVAL_MILLIS;
shell.request_redraw(window::RedrawRequest::At(
now + Duration::from_millis(
millis_until_redraw as u64,
),
));
}
}
}
_ => {}
}
let Some(update) = Update::from_event(
event,
state,
layout.bounds(),
self.padding,
cursor,
self.key_binding.as_deref(),
) 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.focus = Some(Focus::now());
state.last_click = Some(click);
state.drag_click = Some(click.kind());
shell.publish(on_edit(action));
}
Update::Drag(position) => {
shell.publish(on_edit(Action::Drag(position)));
}
Update::Release => {
state.drag_click = None;
}
Update::Scroll(lines) => {
let bounds = self.content.0.borrow().editor.bounds();
if bounds.height >= i32::MAX as f32 {
return event::Status::Ignored;
}
let lines = lines + state.partial_scroll;
state.partial_scroll = lines.fract();
shell.publish(on_edit(Action::Scroll {
lines: lines as i32,
}));
}
Update::Binding(binding) => {
fn apply_binding<
H: text::Highlighter,
R: text::Renderer,
Message,
>(
binding: Binding<Message>,
content: &Content<R>,
state: &mut State<H>,
on_edit: &dyn Fn(Action) -> Message,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let mut publish = |action| shell.publish(on_edit(action));
match binding {
Binding::Unfocus => {
state.focus = None;
state.drag_click = None;
}
Binding::Copy => {
if let Some(selection) = content.selection() {
clipboard.write(
clipboard::Kind::Standard,
selection,
);
}
}
Binding::Cut => {
if let Some(selection) = content.selection() {
clipboard.write(
clipboard::Kind::Standard,
selection,
);
publish(Action::Edit(Edit::Delete));
}
}
Binding::Paste => {
if let Some(contents) =
clipboard.read(clipboard::Kind::Standard)
{
publish(Action::Edit(Edit::Paste(Arc::new(
contents,
))));
}
}
Binding::Move(motion) => {
publish(Action::Move(motion));
}
Binding::Select(motion) => {
publish(Action::Select(motion));
}
Binding::SelectWord => {
publish(Action::SelectWord);
}
Binding::SelectLine => {
publish(Action::SelectLine);
}
Binding::SelectAll => {
publish(Action::SelectAll);
}
Binding::Insert(c) => {
publish(Action::Edit(Edit::Insert(c)));
}
Binding::Enter => {
publish(Action::Edit(Edit::Enter));
}
Binding::Backspace => {
publish(Action::Edit(Edit::Backspace));
}
Binding::Delete => {
publish(Action::Edit(Edit::Delete));
}
Binding::Sequence(sequence) => {
for binding in sequence {
apply_binding(
binding, content, state, on_edit,
clipboard, shell,
);
}
}
Binding::Custom(message) => {
shell.publish(message);
}
}
}
apply_binding(
binding,
self.content,
state,
on_edit,
clipboard,
shell,
);
if let Some(focus) = &mut state.focus {
focus.updated_at = Instant::now();
}
}
}
event::Status::Captured
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
_defaults: &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>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
internal.editor.highlight(
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 status = if is_disabled {
Status::Disabled
} else if state.focus.is_some() {
Status::Focused
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
};
let style = theme.style(&self.class, status);
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
..renderer::Quad::default()
},
style.background,
);
let text_bounds = bounds.shrink(self.padding);
if internal.editor.is_empty() {
if let Some(placeholder) = self.placeholder.clone() {
renderer.fill_text(
Text {
content: placeholder.into_owned(),
bounds: text_bounds.size(),
size: self
.text_size
.unwrap_or_else(|| renderer.default_size()),
line_height: self.line_height,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrapping: self.wrapping,
},
text_bounds.position(),
style.placeholder,
text_bounds,
);
}
} else {
renderer.fill_editor(
&internal.editor,
text_bounds.position(),
style.value,
text_bounds,
);
}
let translation = text_bounds.position() - Point::ORIGIN;
if let Some(focus) = state.focus.as_ref() {
match internal.editor.cursor() {
Cursor::Caret(position) if focus.is_cursor_visible() => {
let cursor =
Rectangle::new(
position + translation,
Size::new(
1.0,
self.line_height
.to_absolute(self.text_size.unwrap_or_else(
|| renderer.default_size(),
))
.into(),
),
);
if let Some(clipped_cursor) =
text_bounds.intersection(&cursor)
{
renderer.fill_quad(
renderer::Quad {
bounds: clipped_cursor,
..renderer::Quad::default()
},
style.value,
);
}
}
Cursor::Selection(ranges) => {
for range in ranges.into_iter().filter_map(|range| {
text_bounds.intersection(&(range + translation))
}) {
renderer.fill_quad(
renderer::Quad {
bounds: range,
..renderer::Quad::default()
},
style.selection,
);
}
}
Cursor::Caret(_) => {}
}
}
}
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()
}
}
fn operate(
&self,
tree: &mut widget::Tree,
_layout: Layout<'_>,
_renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
let state = tree.state.downcast_mut::<State<Highlighter>>();
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
}
fn id(&self) -> Option<widget::Id> {
self.id.as_ref().map(|id| id.0.clone())
}
fn set_id(&mut self, id: widget::Id) {
self.id = Some(Id(id));
}
}
impl<'a, Highlighter, Message, Theme, Renderer>
From<TextEditor<'a, Highlighter, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Highlighter: text::Highlighter,
Message: 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer,
{
fn from(
text_editor: TextEditor<'a, Highlighter, Message, Theme, Renderer>,
) -> Self {
Self::new(text_editor)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Binding<Message> {
Unfocus,
Copy,
Cut,
Paste,
Move(Motion),
Select(Motion),
SelectWord,
SelectLine,
SelectAll,
Insert(char),
Enter,
Backspace,
Delete,
Sequence(Vec<Self>),
Custom(Message),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyPress {
pub key: keyboard::Key,
pub modifiers: keyboard::Modifiers,
pub text: Option<SmolStr>,
pub status: Status,
}
impl<Message> Binding<Message> {
pub fn from_key_press(event: KeyPress) -> Option<Self> {
let KeyPress {
key,
modifiers,
text,
status,
} = event;
if status != Status::Focused {
return None;
}
match key.as_ref() {
keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter),
keyboard::Key::Named(key::Named::Backspace) => {
Some(Self::Backspace)
}
keyboard::Key::Named(key::Named::Delete)
if text.is_none() || text.as_deref() == Some("\u{7f}") =>
{
Some(Self::Delete)
}
keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus),
keyboard::Key::Character("c") if modifiers.command() => {
Some(Self::Copy)
}
keyboard::Key::Character("x") if modifiers.command() => {
Some(Self::Cut)
}
keyboard::Key::Character("v")
if modifiers.command() && !modifiers.alt() =>
{
Some(Self::Paste)
}
keyboard::Key::Character("a") if modifiers.command() => {
Some(Self::SelectAll)
}
_ => {
if let Some(text) = text {
let c = text.chars().find(|c| !c.is_control())?;
Some(Self::Insert(c))
} else if let keyboard::Key::Named(named_key) = key.as_ref() {
let motion = motion(named_key)?;
let motion = if modifiers.macos_command() {
match motion {
Motion::Left => Motion::Home,
Motion::Right => Motion::End,
_ => motion,
}
} else {
motion
};
let motion = if modifiers.jump() {
motion.widen()
} else {
motion
};
Some(if modifiers.shift() {
Self::Select(motion)
} else {
Self::Move(motion)
})
} else {
None
}
}
}
}
}
enum Update<Message> {
Click(mouse::Click),
Drag(Point),
Release,
Scroll(f32),
Binding(Binding<Message>),
}
impl<Message> Update<Message> {
fn from_event<H: Highlighter>(
event: Event,
state: &State<H>,
bounds: Rectangle,
padding: Padding,
cursor: mouse::Cursor,
key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>,
) -> Option<Self> {
let binding = |binding| Some(Update::Binding(binding));
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,
mouse::Button::Left,
state.last_click,
);
Some(Update::Click(click))
} else if state.focus.is_some() {
binding(Binding::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);
Some(Update::Drag(cursor_position))
}
_ => None,
},
mouse::Event::WheelScrolled { delta }
if cursor.is_over(bounds) =>
{
Some(Update::Scroll(match delta {
mouse::ScrollDelta::Lines { y, .. } => {
if y.abs() > 0.0 {
y.signum() * -(y.abs() * 4.0).max(1.0)
} else {
0.0
}
}
mouse::ScrollDelta::Pixels { y, .. } => -y / 4.0,
}))
}
_ => None,
},
Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
text,
..
}) => {
let status = if state.focus.is_some() {
Status::Focused
} else {
Status::Active
};
let key_press = KeyPress {
key,
modifiers,
text,
status,
};
if let Some(key_binding) = key_binding {
key_binding(key_press)
} else {
Binding::from_key_press(key_press)
}
.map(Self::Binding)
}
_ => None,
}
}
}
fn motion(key: key::Named) -> Option<Motion> {
match key {
key::Named::ArrowLeft => Some(Motion::Left),
key::Named::ArrowRight => Some(Motion::Right),
key::Named::ArrowUp => Some(Motion::Up),
key::Named::ArrowDown => Some(Motion::Down),
key::Named::Home => Some(Motion::Home),
key::Named::End => Some(Motion::End),
key::Named::PageUp => Some(Motion::PageUp),
key::Named::PageDown => Some(Motion::PageDown),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Active,
Hovered,
Focused,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
pub background: Background,
pub border: Border,
pub icon: Color,
pub placeholder: Color,
pub value: Color,
pub selection: Color,
}
pub trait Catalog {
type Class<'a>;
fn default<'a>() -> Self::Class<'a>;
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(default)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let active = Style {
background: Background::Color(palette.background.base.color),
border: Border {
radius: 2.0.into(),
width: 1.0,
color: palette.background.strong.color,
},
icon: palette.background.weak.text,
placeholder: palette.background.strong.color,
value: palette.background.base.text,
selection: palette.primary.weak.color,
};
match status {
Status::Active => active,
Status::Hovered => Style {
border: Border {
color: palette.background.base.text,
..active.border
},
..active
},
Status::Focused => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
},
..active
},
Status::Disabled => Style {
background: Background::Color(palette.background.weak.color),
value: active.placeholder,
..active
},
}
}