feat: more native menus with expected reactions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use iced::advanced::layout;
|
||||
@@ -22,10 +22,26 @@ pub(crate) enum EditCommand {
|
||||
|
||||
pub(crate) type EditCommandQueue = Arc<Mutex<VecDeque<EditCommand>>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct EditAvailability {
|
||||
pub(crate) undo: bool,
|
||||
pub(crate) redo: bool,
|
||||
pub(crate) cut: bool,
|
||||
pub(crate) copy: bool,
|
||||
pub(crate) paste: bool,
|
||||
pub(crate) select_all: bool,
|
||||
}
|
||||
|
||||
pub(crate) type EditAvailabilityState = Arc<Mutex<EditAvailability>>;
|
||||
|
||||
pub(crate) fn command_queue() -> EditCommandQueue {
|
||||
Arc::new(Mutex::new(VecDeque::new()))
|
||||
}
|
||||
|
||||
pub(crate) fn availability_state() -> EditAvailabilityState {
|
||||
Arc::new(Mutex::new(EditAvailability::default()))
|
||||
}
|
||||
|
||||
pub(crate) fn queue_command(queue: &EditCommandQueue, command: EditCommand) {
|
||||
queue
|
||||
.lock()
|
||||
@@ -44,22 +60,131 @@ fn pop_command(queue: &EditCommandQueue) -> Option<EditCommand> {
|
||||
pub(crate) struct NativeEdit<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
commands: EditCommandQueue,
|
||||
availability: EditAvailabilityState,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> NativeEdit<'a, Message, Theme, Renderer> {
|
||||
fn new(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
commands: EditCommandQueue,
|
||||
availability: EditAvailabilityState,
|
||||
) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
commands,
|
||||
availability,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
enum ControlId {
|
||||
Named(iced::widget::Id),
|
||||
Position(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct History {
|
||||
values: Vec<String>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State;
|
||||
struct State {
|
||||
histories: HashMap<ControlId, History>,
|
||||
active: Option<ControlId>,
|
||||
window_focused: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FocusedControl {
|
||||
current: usize,
|
||||
pending_text: Option<String>,
|
||||
focused: Option<(ControlId, Option<String>)>,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for FocusedControl {
|
||||
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
|
||||
operate(self);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
_id: Option<&iced::widget::Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn iced::advanced::widget::operation::TextInput,
|
||||
) {
|
||||
self.pending_text = Some(state.text().to_owned());
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&iced::widget::Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn iced::advanced::widget::operation::Focusable,
|
||||
) {
|
||||
if state.is_focused() {
|
||||
self.focused = Some((
|
||||
id.cloned()
|
||||
.map_or(ControlId::Position(self.current), ControlId::Named),
|
||||
self.pending_text.take(),
|
||||
));
|
||||
} else {
|
||||
self.pending_text = None;
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn observe(&mut self, focused: Option<&(ControlId, Option<String>)>) {
|
||||
self.active = focused.map(|(control, _)| control.clone());
|
||||
let Some((control, Some(value))) = focused else {
|
||||
return;
|
||||
};
|
||||
let history = self
|
||||
.histories
|
||||
.entry(control.clone())
|
||||
.or_insert_with(|| History {
|
||||
values: vec![value.clone()],
|
||||
current: 0,
|
||||
});
|
||||
if history.values[history.current] != *value {
|
||||
history.values.truncate(history.current + 1);
|
||||
history.values.push(value.clone());
|
||||
history.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn replacement(&mut self, command: EditCommand) -> Option<String> {
|
||||
let history = self.histories.get_mut(self.active.as_ref()?)?;
|
||||
match command {
|
||||
EditCommand::Undo if history.current > 0 => history.current -= 1,
|
||||
EditCommand::Redo if history.current + 1 < history.values.len() => {
|
||||
history.current += 1;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some(history.values[history.current].clone())
|
||||
}
|
||||
|
||||
fn availability(&self, focused: Option<&(ControlId, Option<String>)>) -> EditAvailability {
|
||||
let Some((control, text)) = focused else {
|
||||
return EditAvailability::default();
|
||||
};
|
||||
let editable = text.is_some();
|
||||
let history = self.histories.get(control);
|
||||
EditAvailability {
|
||||
undo: editable && history.is_some_and(|history| history.current > 0),
|
||||
redo: editable
|
||||
&& history.is_some_and(|history| history.current + 1 < history.values.len()),
|
||||
cut: editable,
|
||||
copy: true,
|
||||
paste: editable,
|
||||
select_all: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn modifier_sync_event(event: &Event) -> Option<Event> {
|
||||
let Event::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) = event else {
|
||||
@@ -117,7 +242,7 @@ where
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State)
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
@@ -166,11 +291,44 @@ where
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if matches!(
|
||||
event,
|
||||
Event::Window(iced::window::Event::RedrawRequested(_))
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
match event {
|
||||
Event::Window(iced::window::Event::Focused) => state.window_focused = true,
|
||||
Event::Window(iced::window::Event::Unfocused) => state.window_focused = false,
|
||||
_ => {}
|
||||
}
|
||||
if state.window_focused
|
||||
&& matches!(
|
||||
event,
|
||||
Event::Window(iced::window::Event::RedrawRequested(_))
|
||||
)
|
||||
{
|
||||
while let Some(command) = pop_command(&self.commands) {
|
||||
if let Some(replacement) = state.replacement(command) {
|
||||
let previous = clipboard.read(iced::advanced::clipboard::Kind::Standard);
|
||||
clipboard.write(iced::advanced::clipboard::Kind::Standard, replacement);
|
||||
for command in [EditCommand::SelectAll, EditCommand::Paste] {
|
||||
for command_event in command_events(command) {
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
&command_event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(previous) = previous {
|
||||
clipboard.write(iced::advanced::clipboard::Kind::Standard, previous);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if matches!(command, EditCommand::Undo | EditCommand::Redo) {
|
||||
continue;
|
||||
}
|
||||
for command_event in command_events(command) {
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
@@ -209,6 +367,19 @@ where
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
|
||||
let mut focused = FocusedControl::default();
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, &mut focused);
|
||||
if state.window_focused {
|
||||
state.observe(focused.focused.as_ref());
|
||||
*self
|
||||
.availability
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner()) =
|
||||
state.availability(focused.focused.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
@@ -282,8 +453,9 @@ where
|
||||
pub(crate) fn native_edit<'a, Message, Theme, Renderer>(
|
||||
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
commands: EditCommandQueue,
|
||||
availability: EditAvailabilityState,
|
||||
) -> NativeEdit<'a, Message, Theme, Renderer> {
|
||||
NativeEdit::new(content, commands)
|
||||
NativeEdit::new(content, commands, availability)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -310,4 +482,14 @@ mod tests {
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) if modifiers.is_empty()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_history_supports_undo_and_redo() {
|
||||
let control = ControlId::Position(0);
|
||||
let mut state = State::default();
|
||||
state.observe(Some(&(control.clone(), Some("a".into()))));
|
||||
state.observe(Some(&(control.clone(), Some("ab".into()))));
|
||||
assert_eq!(state.replacement(EditCommand::Undo).as_deref(), Some("a"));
|
||||
assert_eq!(state.replacement(EditCommand::Redo).as_deref(), Some("ab"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user