Gtk4 - how to deploy complex event handlers

I have a large application - a mini-programming language with lots of graphic facilities - written intially in C# with Gtk2, later partially ported to C++ with Gtk3; now I want to upgrade and enhance it in Rust with Gtk4, and make it publicly available. And here I have hit a major obstacle.
In the original C# version there are 112 menu items; these and other main window widgets invoke 96 event handlers. The handlers for these are often quite long and complex (e.g. refined searches of TextView text: reformatting of displayed text using markup tags). The window's 'build' function has maybe a hundred parameters, many of which can be altered via events during runtime (e.g. TextView appearance parameters). But now in Rust - Gtk4 I find that I can't handle any but trivial events using functions, as they cannot access window parameters ("error - can't capture dynamic environment in a fn item"). So I have to use closures inside the build function. That is fine for a button handler which just changes its title to "I'm clicked!", but not with long functions that may need to address some window parameters and process widget contents. Is there a way around this limitation?
Thanks for help.

I'm not entirely clear on how your C# code was working, so it's hard to suggest solutions. Could you include an example of how you were doing things before? An example the equivalent Rust code that doesn't work might be useful too.

1 Like

Answer to semicoleon:

Thanks a lot for replying. As you suggested, I am supplying two code snippets.
The first gives some idea of how the existing C#-Gtk2 codeworks. I have cut code down to bare bones in the following.
(Remarks containing square brackets stand in for extents of code that are not relevant for the purpose.)
The second code is an example of what doesn't work in Rust.

(1) The C# code:

namespace Shrdlu {
public partial class MainWindow : Gtk.Window {
// STATIC FIELDS:
// [about 60 static fields follow: system parameters that are general to all window instances raised]
// INSTANCE FIELDS
// [about 170 instance fields follow, being dynamic parameters relevant to this window instance.]
public MainWindow () : base(Gtk.WindowType.Toplevel) {
Build();
// [ Detect and parse command line arguments ]
// [ Create window instance + build its widgets + assign event handlers ]
// [Assign initial values to dynamic collections: navigation stack, 'undo' stack, list of graph ID's ...]
}

Event handlers (90+) follow. They are inside class MainWindow but outside the Build() method above.
I give the outline of just one such handler. It has about 80 lines of code (remarks not included), and:

  • References fields and methods of the window's Gtk.TextView and Gtk.TextBuffer and Gtk.Label widgets.
  • Uses helper functions which act on e.g. any Gtk.TextView (there can be several open at once);
    such functions are called with pointers to this particular window's widget.
  • Uses dialog functions from a separate library which contains home-made dialogs of all sorts.
    Unnecessary detailed code is omitted, being replaced by a summary in square brackets.
// Raise a dialog box for choice of text to replace, inclusive of several types of wildcard. Offer several choices
// (e.g. for case-insensitive search, to conform replacing text to the letter case of what it replaced).
// Process the choices, then do replacement (either all at once, or one at a time with dialog to OK each).
// Finally, in most cases have the screen display the first replaced text, centred in the screen.
protected virtual void OnReplaceActionActivated (object sender, System.EventArgs e) {
// Set up the REPLACE TEXT message box:
// [Initiate dialog box parameters are set]
// Show the modal dialog box, and so input search parameters. ("JD.SearchBox" below is custom-built, in another library.)
int button = JD.SearchBox(ref soughtText, ref replacemt, isSelection, out matching_conditions, true);
// [Set various search parameters from the returned 'ref' arguments in the above call]
// The dialog box allows wildcards for several scenarios (small subset of Regex-type codes). These human-readable codes
// are now replaced with single special characters, ready for the 'FindTextAndTagFinds' that follows:
string soughtText1 = soughtText.Replace(JS.QmarkCode, JS.QmarkStr).Replace(JS.AmarkCode, JS.AmarkStr).Replace(JS.BmarkCode, JS.BmarkStr);
// Use a library function to search this particular TextBuffer ("BuffAss" below), returning (in effect) tuples, the first value
// being the index of the start of text to be replaced, the second being the length (taking into account wildcards for multiple chars).
int[] findings = JTV.FindTextAndTagFinds(BuffAss, soughtText1, find_text, startPtr, endPtr,
matchCase, wholeWord, false, P.IdentifierChars);
// [ If replacement is to be one at a time with user vetting, then several TextView and TextBuffer commands are used,
// to scroll to the current word text, centre it on the screen, select it, and raise a new dialog box - 'replace this one?]
// [ Further TextView / TextBuffer calls to colour the replaced text - using static class fields for colours.]
// Remove colouring text tags in the TextBuffer which have been applied to text finds, refinds and replacements.
RecolourAllAssignmentsText("remove only", find_text, findmore_text, replaced_text);
// [ Further TextView / TextBuffer calls to scroll to the first replaced word, and position it halfway up the screen.]
// Set the Comments label text (label at bottom of window, which will e.g. display "2 replacements"):
LblComments.Text = CommentAfterSearch(soughtText, noReplaced, true, false, matchCase, fromCursor, wholeWord);
}

// [Rest of class MainWindow code, including other event handlers, methods that service event-handlers,
// and unrelated methods; approximately 6000 lines in total, inclusive of remarks.]

} // END of class MainWindow
} // END of namespace Shrdlu

Further thoughts...
I have just found a site which approaches the problem by assigning event handlers to threads:
https://mmstick.github.io/gtkrs-tutorials/1x05-window.html
If that is the only way out of this dilemma, I will try that approach.

Somehow the Rust code got excluded from the above. Here it is again:
The following is based on https://gtk-rs.org/gtk4-rs/git/book/g_object_memory_management.html.
It differs in that I tried to do away with these two lines by using functions:

button_increase.connect_clicked(|_| number += 1); // my event-handlers are far too large to fit into such a closure!
button_decrease.connect_clicked(|_| number -= 1); // Forget for now that we must use Rc and Cell(.) - not relevant to this problem.

So here is the code|:

fn main() {
let app = Application::builder()
.application_id("org.example.HelloWorld")
.build();

// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
app.run();
}

fn build_ui(app: &Application) {
// Create a button with label and margins
let button1 = Button::builder()
.label("Button 1")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
button1.connect_clicked(btn1_clicked);

let button2 = Button::builder()
.label("Button 2")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
button2.connect_clicked(btn2_clicked);

// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button1);
gtk_box.append(&button2);

// Create the main window and set the title
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(&gtk_box)
.build();

// Present window
window.present();

Next comes my naive attempt to use event handlers as functions. The error message in "RustEnhanced"
(the coder didn't get as far as the compiler) was: "can't capture dynamic environment in a fn item".

fn btn1_clicked(btn: &gtk4::Button) {
btn.set_label("Btn 1 clicked");
let ss = window.title;
println!("{}", ss);
}
fn btn2_clicked(btn: &gtk4::Button) {
btn.set_label("Btn 2 clicked");
}

}

Generally the approach described in that guide will be less of a pain than doing it another way, but you could also call functions from the closure, passing the data you need to the function manually.

use gtk::{ApplicationWindow, Orientation, Window};
use gtk4::{self as gtk, prelude::*, Application, Button};

fn main() {
    let app = Application::builder()
        .application_id("org.example.HelloWorld")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);
    app.run();
}

fn build_ui(app: &Application) {
    // Create a button with label and margins
    let button1 = Button::builder()
        .label("Button 1")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    let button2 = Button::builder()
        .label("Button 2")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // Add buttons to `gtk_box`
    let gtk_box = gtk::Box::builder()
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button1);
    gtk_box.append(&button2);

    // Create the main window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present window
    window.present();

    // Have to move the connects to after the window is created so we can capture it in the closures.
    button1.connect_clicked(
        // This block is essentially what the gtk::glib::clone! macro does. I'm just doing it manually here for readability.
        {
            let window = window.clone();
            move |b| btn1_clicked(b, window.upcast_ref())
        },
    );

    // Don't need to clone the window here because it isn't used after this point.
    button2.connect_clicked(move |b| btn2_clicked(b, window.upcast_ref()));
}

fn btn1_clicked(btn: &gtk4::Button, window: &Window) {
    btn.set_label("Btn 1 clicked");
    let ss = window.title();
    println!("{}", ss.unwrap_or_else(|| "".into()));
}

fn btn2_clicked(btn: &gtk4::Button, window: &Window) {
    btn.set_label("Btn 2 clicked");
    println!("{}", window.title().unwrap_or_else(|| "".into()))
}

I think in the long run an event driven architecture is probably less frustrating to use than doing all of the closure and clone shuffling.


Also note that Rust's async system doesn't necessarily involve spawning new threads.

1 Like

Many thanks, help greatly appreciated. I will try both approaches. At last I have a way ahead!