kael 0.2.0

GPU-accelerated native UI framework for Rust — build desktop apps with Metal, DirectX, and Vulkan rendering
Documentation
use crate::{
    Action, App, DispatchPhase, FocusHandle, Global, KeyBinding, KeyContext, Redo, Undo,
    UndoRedoManager, UndoableChange, Window,
};
use std::{
    any::{Any, TypeId},
    cell::RefCell,
    fmt::{self, Debug},
    marker::PhantomData,
    rc::Rc,
};

pub(crate) const LOCAL_UNDO_REDO_CONTEXT: &str = "LocalUndoRedo";

#[derive(Default)]
struct LocalUndoRedoBindingsInstalled;

impl Global for LocalUndoRedoBindingsInstalled {}

pub(crate) fn ensure_local_undo_redo_bindings(cx: &mut App) {
    if cx.has_global::<LocalUndoRedoBindingsInstalled>() {
        return;
    }

    cx.bind_keys([
        KeyBinding::new("secondary-z", Undo, Some(LOCAL_UNDO_REDO_CONTEXT)),
        KeyBinding::new("secondary-shift-z", Redo, Some(LOCAL_UNDO_REDO_CONTEXT)),
        KeyBinding::new("secondary-y", Redo, Some(LOCAL_UNDO_REDO_CONTEXT)),
    ]);
    cx.set_global(LocalUndoRedoBindingsInstalled);
}

pub(crate) fn local_undo_redo_key_context() -> KeyContext {
    KeyContext::parse(LOCAL_UNDO_REDO_CONTEXT).expect("valid local undo/redo context")
}

pub(crate) fn register_focused_action_handler<A: Action + 'static>(
    window: &mut Window,
    focus_handle: FocusHandle,
    handler: impl Fn(&A, &mut Window, &mut App) + 'static,
) {
    window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
        if phase != DispatchPhase::Bubble || !focus_handle.is_focused(window) {
            return;
        }

        let Some(action) = action.downcast_ref::<A>() else {
            return;
        };

        handler(action, window, cx);
        cx.stop_propagation();
    });
}

pub(crate) fn register_focused_action_handler_when<A: Action + 'static>(
    window: &mut Window,
    enabled: bool,
    focus_handle: FocusHandle,
    handler: impl Fn(&A, &mut Window, &mut App) + 'static,
) {
    if enabled {
        register_focused_action_handler(window, focus_handle, handler);
    }
}

#[cfg(test)]
pub(crate) struct ValueHistory<T> {
    manager: UndoRedoManager,
    description: String,
    _marker: PhantomData<T>,
}

#[cfg(test)]
impl<T: 'static> Default for ValueHistory<T> {
    fn default() -> Self {
        Self::new("Edit")
    }
}

#[cfg(test)]
impl<T> Debug for ValueHistory<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ValueHistory")
            .field("description", &self.description)
            .field("can_undo", &self.manager.can_undo())
            .field("can_redo", &self.manager.can_redo())
            .finish()
    }
}

#[cfg(test)]
struct ValueChange<T> {
    previous: T,
    next: T,
    description: String,
}

#[cfg(test)]
impl<T> ValueChange<T> {
    fn new(previous: T, next: T, description: String) -> Self {
        Self {
            previous,
            next,
            description,
        }
    }
}

#[cfg(test)]
impl<T> Debug for ValueChange<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ValueChange")
            .field("description", &self.description)
            .finish()
    }
}

#[cfg(test)]
impl<T: 'static> UndoableChange for ValueChange<T> {
    fn apply(&mut self) {}

    fn revert(&mut self) {}

    fn description(&self) -> &str {
        &self.description
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

struct WindowValueChange<T> {
    previous: T,
    next: T,
    description: String,
    source_id: crate::FocusId,
}

impl<T> WindowValueChange<T> {
    fn new(previous: T, next: T, description: String, source_id: crate::FocusId) -> Self {
        Self {
            previous,
            next,
            description,
            source_id,
        }
    }
}

impl<T> Debug for WindowValueChange<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("WindowValueChange")
            .field("description", &self.description)
            .field("source_id", &self.source_id)
            .finish()
    }
}

impl<T: 'static> UndoableChange for WindowValueChange<T> {
    fn apply(&mut self) {}

    fn revert(&mut self) {}

    fn description(&self) -> &str {
        &self.description
    }

    fn source_id(&self) -> Option<crate::FocusId> {
        Some(self.source_id)
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[cfg(test)]
#[allow(dead_code)]
impl<T: 'static> ValueHistory<T> {
    pub(crate) fn new(description: impl Into<String>) -> Self {
        Self {
            manager: UndoRedoManager::default(),
            description: description.into(),
            _marker: PhantomData,
        }
    }

    pub(crate) fn clear(&mut self) {
        self.manager.clear();
    }

    pub(crate) fn can_undo(&self) -> bool {
        self.manager.can_undo()
    }

    pub(crate) fn can_redo(&self) -> bool {
        self.manager.can_redo()
    }

    pub(crate) fn record(&mut self, previous: T, next: T) {
        self.manager.push(Box::new(ValueChange::new(
            previous,
            next,
            self.description.clone(),
        )));
    }

    pub(crate) fn replace_last(&mut self, previous: T, next: T) -> bool {
        self.manager.replace_last(Box::new(ValueChange::new(
            previous,
            next,
            self.description.clone(),
        )))
    }

    pub(crate) fn undo(&mut self) -> Option<T>
    where
        T: Clone,
    {
        let change = self.manager.undo()?;
        let change = change
            .as_any()
            .downcast_ref::<ValueChange<T>>()
            .expect("value history changes stay typed");
        Some(change.previous.clone())
    }

    pub(crate) fn redo(&mut self) -> Option<T>
    where
        T: Clone,
    {
        let change = self.manager.redo()?;
        let change = change
            .as_any()
            .downcast_ref::<ValueChange<T>>()
            .expect("value history changes stay typed");
        Some(change.next.clone())
    }
}

pub(crate) struct WindowValueHistory<T> {
    manager: Rc<RefCell<UndoRedoManager>>,
    description: String,
    source_id: crate::FocusId,
    _marker: PhantomData<T>,
}

impl<T> Debug for WindowValueHistory<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let manager = self.manager.borrow();
        f.debug_struct("WindowValueHistory")
            .field("description", &self.description)
            .field("source_id", &self.source_id)
            .field("can_undo", &manager.can_undo_for_source(self.source_id))
            .field("can_redo", &manager.can_redo_for_source(self.source_id))
            .finish()
    }
}

impl<T: 'static> WindowValueHistory<T> {
    pub(crate) fn new(
        manager: Rc<RefCell<UndoRedoManager>>,
        focus_handle: &FocusHandle,
        description: impl Into<String>,
    ) -> Self {
        Self {
            manager,
            description: description.into(),
            source_id: focus_handle.id,
            _marker: PhantomData,
        }
    }

    pub(crate) fn clear(&mut self) {
        self.manager.borrow_mut().clear_for_source(self.source_id);
    }

    pub(crate) fn can_undo(&self) -> bool {
        self.manager.borrow().has_undo_for_source(self.source_id)
    }

    pub(crate) fn can_redo(&self) -> bool {
        self.manager.borrow().has_redo_for_source(self.source_id)
    }

    pub(crate) fn record(&mut self, previous: T, next: T) {
        self.manager
            .borrow_mut()
            .push(Box::new(WindowValueChange::new(
                previous,
                next,
                self.description.clone(),
                self.source_id,
            )));
    }

    pub(crate) fn replace_last(&mut self, previous: T, next: T) -> bool {
        let manager = self.manager.borrow();
        if !manager.can_undo_for_source(self.source_id) {
            return false;
        }
        drop(manager);

        self.manager
            .borrow_mut()
            .replace_last(Box::new(WindowValueChange::new(
                previous,
                next,
                self.description.clone(),
                self.source_id,
            )))
    }

    pub(crate) fn undo(&mut self) -> Option<T>
    where
        T: Clone,
    {
        let manager = self.manager.borrow();
        if !manager.can_undo_for_source(self.source_id) {
            return None;
        }
        drop(manager);

        let mut manager = self.manager.borrow_mut();
        let change = manager.undo_for_source(self.source_id)?;
        let change = change
            .as_any()
            .downcast_ref::<WindowValueChange<T>>()
            .expect("window value history changes stay typed");
        Some(change.previous.clone())
    }

    pub(crate) fn redo(&mut self) -> Option<T>
    where
        T: Clone,
    {
        let manager = self.manager.borrow();
        if !manager.can_redo_for_source(self.source_id) {
            return None;
        }
        drop(manager);

        let mut manager = self.manager.borrow_mut();
        let change = manager.redo_for_source(self.source_id)?;
        let change = change
            .as_any()
            .downcast_ref::<WindowValueChange<T>>()
            .expect("window value history changes stay typed");
        Some(change.next.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::ValueHistory;

    #[test]
    fn value_history_undo_redo_round_trips_values() {
        let mut history = ValueHistory::new("Number change");
        history.record(0, 1);
        history.record(1, 2);
        history.record(2, 3);

        assert_eq!(history.undo(), Some(2));
        assert_eq!(history.undo(), Some(1));
        assert_eq!(history.redo(), Some(2));
        assert_eq!(history.redo(), Some(3));
    }

    #[test]
    fn value_history_clears_redo_on_new_record() {
        let mut history = ValueHistory::new("Number change");
        history.record(0, 1);
        history.record(1, 2);

        assert_eq!(history.undo(), Some(1));
        history.record(1, 9);

        assert_eq!(history.redo(), None);
    }

    #[test]
    fn value_history_replace_last_overwrites_latest_entry() {
        let mut history = ValueHistory::new("Number change");
        history.record(0, 1);
        history.record(1, 2);

        assert!(history.replace_last(1, 3));
        assert_eq!(history.undo(), Some(1));
        assert_eq!(history.redo(), Some(3));
    }
}