Search code examples
rustgtk3undo-redogtk-rs

Rust and GTK Text Editor: Troubleshooting Undo and Redo Operations with Shared Data


I have been working hard on my own text editor as a way to learn Rust. But even with the help of ChatGPT and the entirety of Stack Overflow already asked questions I have reached a roadblock. I cant do undo and redo operations.

Currently only an Rc<RefCell> and an Arc<Mutex> will compile, but when going to use the undo and redo system with an Rc<RefCell> it panics, and with an Arc<Mutex> it freezes till I kill it.

I have tried references to the data, but since the stacks need to mutate it does not work, also the need to pass it through contexts because of how gtk works adds massive complexity to the problem.

I am trying to do a normal state stack and undo stack system where it needs to work across the entire app as shared data.

I have the full code over on GitLab here

But I know it must be somewhere here: main.rs

pub mod structs;
// ...
pub mod unre_handler_impl;
// ...
pub mod undo_redo;

mod user_interface;

use gtk::prelude::*;
use gtk::{Application, TextBuffer};
use user_interface::build_ui;
use std::sync::{Arc, Mutex};
use crate::structs::{HeaderBarItems, WindowContentItems, UndoRedoHandler};
use crate::save_load_backend::{save_buffer_to_file, load_file_into_buffer};
use crate::undo_redo::{character_change_handler, undo_handler, redo_handler};

fn main() {
    gtk::init().expect("Failed to start GTK");

    let app = Application::builder()
        .application_id("com.savagedevs.rust_writer")
        .build();

    let cloned_app = app.clone();
    app.connect_activate(move |_| {
        let undo_redo_handler = Arc::new(Mutex::new(UndoRedoHandler::new()));

        // ...

        let undo_redo_text = text_buffer.clone();
        let undo_redo_data: Arc<Mutex<UndoRedoHandler>> = Arc::clone(&undo_redo_handler);
        text_buffer.connect_changed(move |_| {
            let mut undo_redo_data = undo_redo_data.lock().unwrap();
            character_change_handler(&undo_redo_text, &mut undo_redo_data);
            drop(undo_redo_data);
        });

        let undo_text = text_buffer.clone();
        let undo_data: Arc<Mutex<UndoRedoHandler>> = Arc::clone(&undo_redo_handler);
        edit_menu.edit_undo.connect_activate(move |_| {
            let mut undo_data = undo_data.lock().unwrap();
            undo_handler(&undo_text, &mut undo_data);
            drop(undo_data);
        });

        let redo_text = text_buffer.clone();
        let redo_data: Arc<Mutex<UndoRedoHandler>> = Arc::clone(&undo_redo_handler);
        edit_menu.edit_redo.connect_activate(move |_| {
            let mut redo_data = redo_data.lock().unwrap();
            redo_handler(&redo_text, &mut redo_data);
            drop(redo_data);
        });
    });
    app.run();
}

undo_redo.rs

use gtk::prelude::*;
use gtk::TextBuffer;
use crate::structs::UndoRedoHandler;

pub fn character_change_handler(text_buffer: &TextBuffer, undo_redo: &mut UndoRedoHandler) {
    println!("Text Changed");
    
    let start_iter = text_buffer.start_iter();
    let end_iter = text_buffer.end_iter();

    let raw_text = text_buffer.text(&start_iter, &end_iter, true).unwrap();
    let text_slice = raw_text.to_string();

    if text_slice.len() <= 2 {
        return;
    }
    let last_two_chars = &text_slice[text_slice.len() - 2..];

    if !last_two_chars.starts_with(' ') && last_two_chars.chars().nth(1) == Some(' ') {
        undo_redo.push(text_slice);
        println!("Word detected");
    }
}

pub fn undo_handler(text_buffer: &TextBuffer, undo_redo: &mut UndoRedoHandler) {
    if let Some(text) = undo_redo.undo() {
        text_buffer.set_text(text.as_str());
    } else {
        println!("Nothing to undo.");
    }
}

pub fn redo_handler(text_buffer: &TextBuffer, undo_redo: &mut UndoRedoHandler) {
    if let Some(text) = undo_redo.redo() {
        text_buffer.set_text(text.as_str());
    } else {
        println!("Nothing to redo.");
    }
}

structs.rs

// ...
#[derive(Debug, Clone, PartialEq)]
pub struct UndoRedoHandler {
    pub history: Vec<String>,
    pub undo_stack: Vec<String>,
}

unre_handler_impl.rs

use crate::structs::UndoRedoHandler;

impl UndoRedoHandler {
    pub fn new() -> Self {
        UndoRedoHandler {
            history: Vec::new(),
            undo_stack: Vec::new(),
        }
    }

    pub fn push(&mut self, item: String) {
        self.history.push(item);
        self.undo_stack.clear();
    }

    pub fn undo(&mut self) -> Option<String> {
        if let Some(item) = self.history.pop() {
            self.undo_stack.push(item.clone());
            self.history.last().cloned()
        } else {
            None
        }
    }

    pub fn redo(&mut self) -> Option<String> {
        if let Some(item) = self.undo_stack.pop() {
            self.history.push(item.clone());
            self.history.last().cloned()
        } else {
            None
        }
    }
}

impl Default for UndoRedoHandler {
    fn default() -> Self {
        Self::new()
    }
}

Any advice would be much appreciated, I am at the point where I want to pull my hair out. It should not be so complex to just share a common undo redo buffer that can be used in all parts of the app.


Solution

  • I found the problem! Whenever I would trigger an undo event, it would change the text, triggering a connect_changed() event from GTK, which would try to double lock the mutex, causing a deadlock. To fix this I had to change:

    let undo_redo_text = text_buffer.clone();
    let undo_redo_data: Arc<Mutex<UndoRedoHandler>> = Arc::clone(&undo_redo_handler);
    text_buffer.connect_changed(move |_| {
        let mut undo_redo_data = undo_redo_data.lock().unwrap();
        character_change_handler(&undo_redo_text, &mut undo_redo_data);
        drop(undo_redo_data);
    });
    

    to:

    let undo_redo_text = text_buffer.clone();
    let undo_redo_data: Arc<Mutex<UndoRedoHandler>> = Arc::clone(&undo_redo_handler);
    text_buffer.connect_changed(move |_| {
        let mut undo_redo_data = match undo_redo_data.try_lock() {
            Ok(data) => data,
            Err(_) => return, // Return early if lock cannot be acquired
        };
        character_change_handler(&undo_redo_text, &mut undo_redo_data);
        drop(undo_redo_data);
    });
    

    That way any time the mutex is busy it wont try to relock. Now undo and redo act exactly like they should. There probably is an edge case if you are super humanly fast where this may make it drop a state, but I dont think any human could write enough for a state in the maybe 200 nanoseconds the lock would last.