Event handler in FLTK

Just starting to get my head around GUI programming in Rust. I've decided to go with FLTK but my problem is not really FLTK.
This is basically a set of digits where individual digits can be scrolled up/down. For each digit I need to handle mouse events.
This is the digit type.

pub struct VFODigit{
    id : i32,
    pub frame : Frame,
    i_cc : Arc<Mutex<protocol::cc_out::CCDataMutex>>,
}

// Implementation methods on UDPRData
impl VFODigit {
	// Create a new instance and initialise the default arrays
    pub fn new( id : i32, label : &String, font : Font, size : i32, color : Color, i_cc : Arc<Mutex<protocol::cc_out::CCDataMutex>>) -> VFODigit {

        let mut frame = Frame::default().with_label(label);
        frame.set_label_color(color);
        frame.set_label_font(font);
        frame.set_label_size(size);

        // Object state
        VFODigit {
            id : id,
            frame : frame,
            i_cc : i_cc,
        }
    }

    pub fn get_id(&self) -> i32 {
        return self.id;
    }

    pub fn set_label(&mut self, label : &String) {
        self.frame.set_label(label);
    }
}

The function that creates the digits (which is their parent) adds them to a grid and also to a HashMap works fine and I can retrieve the widgets from the map and update them. However when I try to add the handler I can't seem to get round the error.

fn create_digits(&mut self) {

        let mut index = 0;
        for i in 0..11 {
            if (i == 3) || (i == 7) {
                // Add a separator
                let mut sep = self.new_sep();
                self.grid.insert(&mut sep, 0, i);
            } else {
                // Add the next digit
                let mut digit = VFODigit::new(index, &String::from("0"), Font::Times, 20, Color::DarkCyan, self.i_cc.clone());
                self.grid.insert(&mut digit.frame, 0, i);
                self.digit_map.insert(index as i32, digit);
                digit.frame.handle(move |f, e| self.digit_handler(f, e));
                index += 1;
            }
        }
    }

and the test handler

fn digit_handler(&mut self, f: &mut Frame, e: Event) -> bool {
        if e == Event::Enter {
            println!("Enter");
        } else if e == Event::Leave {
            println!("Leave");
        } else if e == Event::MouseWheel {
            println!("Wheel");
        }
        return true;
    }

The error trace.
error[E0521]: borrowed data escapes outside of associated function
--> src\app\ui\components\vfo.rs:124:22
|
112 | fn create_digits(&mut self) {
| ---------
| |
| self is a reference that is only valid in the associated function body
| let's call the lifetime of this reference '1
...
124 | &mut digit.frame.handle(move |f, e| self.digit_handler(f, e));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| self escapes the associated function body here
| argument requires that '1 must outlive 'static

error[E0382]: borrow of moved value: self
--> src\app\ui\components\vfo.rs:125:17
|
112 | fn create_digits(&mut self) {
| --------- move occurs because self has type &mut VFOState, which does not implement the Copy trait
...
124 | &mut digit.frame.handle(move |f, e| self.digit_handler(f, e));
| ----------- ---- variable moved due to use in closure
| |
| value moved into closure here
125 | self.digit_map.insert(index as i32, digit);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ value bor

You can't capture self in that closure. You aren't actually using self in digit_handler. If you don't actually need it you can just make digit_handler a function instead of a method and it should work.

1 Like

Unfortunately I will need to use self when I do the real implementation so I can't leave it out so what's the proper way to do this.

You already have one field in a Mutex, and the frame is passed in to the closure. If id is constant (and you never set a new Arc on i_cc), you can just capture the individual fields instead of the reference to self.

The handle closure gets a &mut to the widget so it's possible you could make your type implement the widget trait instead? I'm not familiar with fltk so I'm not sure if that's feasible, and it's possible there are other fltk specific ways to handle this setup that I'm not aware of.

1 Like

I've implemented this as far as I can. I just need to call one method from the MouseWheel event (commented out). Any advice on how to do this please.

use std::ops::Neg;
use std::sync::{Arc, Mutex};
use std::collections::HashMap;

use fltk::{prelude::*, frame::Frame};
use fltk::enums::{Font, Color, Event};
use fltk::app::MouseWheel;
use fltk_grid::Grid;

use crate::app::protocol;

//==================================================================================
// VFO State
pub struct VFOState{
    i_cc : Arc<Mutex<protocol::cc_out::CCDataMutex>>,
    freq_inc_map : HashMap<u32, u32>,
    current_freq_in_hz : u32,
    digit_map : HashMap<i32, Frame>,
    pub frame : Frame,
    pub grid : Grid,
}

// Implementation methods on VFOState
impl VFOState {
	// Create a new instance and initialise the default arrays
    pub fn new(i_cc : Arc<Mutex<protocol::cc_out::CCDataMutex>>) -> VFOState {

        // Lookup for digit number to frequency increment 100MHz to 1Hz
        let freq_inc_map = HashMap::from([
            (0, 100000000),
            (1, 10000000),
            (2, 1000000),
            (3, 100000),
            (4, 10000),
            (5, 1000),
            (6, 100),
            (7, 10),
            (8, 1),
        ]);

        // Hold refs to all digits
        let mut digit_map = HashMap::new();

        // Somewhere to create the widgets
        let mut frame = Frame::default();
        let mut grid = Grid::default_fill();

        // Object state
        VFOState {
            i_cc : i_cc,
            freq_inc_map : freq_inc_map,
            current_freq_in_hz : 0,
            digit_map : digit_map,
            frame : frame,
            grid : grid,
        }
    }

    //=========================================================================================
    // Initialise and create widgets
    pub fn init_vfo(&mut self) {

        // Initialise the grid
        // Accomodate 9 digits and 2 separators
        self.grid.set_layout(1, 11);
        // Create our set of digits
        self.create_digits();
    }

    // Set frequency
    pub fn set_freq(&mut self, freq: u32) {
        self.current_freq_in_hz = freq;
        let new_freq : String = freq.to_string();
        // Need to make this a 9 digit string with leading zeros
        let num_zeros = 9 - new_freq.len();
        let mut zeros_str = String::from("");

        for _i in 0..num_zeros {
            zeros_str += "0";
        }
        let mut freq_str = String::from(zeros_str + &new_freq);
        self.set_display_freq(&freq_str);
    }

    //=========================================================================================
    // Create the set of 9 digits in 3 sets with separators
    fn create_digits(&mut self) {

        let mut index = 0;
        for i in 0..11 {
            if (i == 3) || (i == 7) {
                // Add a separator
                let mut sep = self.new_sep();
                self.grid.insert(&mut sep, 0, i);
            } else {
                // Add the next digit
                let mut digit = self.create_digit(
                        index, 
                        &String::from("0"), 
                        Font::Times, 
                        20, 
                        Color::DarkCyan);
                self.grid.insert(&mut digit, 0, i);
                self.digit_map.insert(index as i32, digit);
                index += 1;
            }
        }
    }

    // Create a new separator 
    fn new_sep(&mut self) -> Frame {
        let mut frame = Frame::default().with_label("_");
        frame.set_label_color(Color::DarkBlue);
        frame.set_label_font(Font::CourierBold);
        frame.set_label_size(20);
        return frame;
    }

    // Create a new digit
    fn create_digit(&mut self,
            id : i32, 
            label : &String, 
            font : Font, 
            size : i32, 
            color : Color) -> Frame {
        let mut frame = Frame::default().with_label(label);
        frame.set_label_color(color);
        frame.set_label_font(font);
        frame.set_label_size(size);
        // Handle mouse events
        frame.handle({
            // Bring variables into closure
            // CC_out instance
            let cc = self.i_cc.clone();
            // id for this digit
            let w_id: i32 = id;
            // freq increment for this digit
            let freq_inc = (self.freq_inc_map[&(w_id as u32)]) as i32;
            let freq_dec = freq_inc.neg();
            move |f, ev| match ev {
                Event::Enter => {
                    // Grow the label when we mouse over
                    f.set_label_size(30);
                    f.redraw_label();
                    true
                }
                Event::Leave => {
                    // Shrink the label back again
                    f.set_label_size(20);
                    f.redraw_label();
                    true
                }
                Event::MouseWheel => {
                    // Here we need to increment/decrement the frequency
                    // This will also reset the display and update the radio
                    let mut inc_or_dec: i32 = 0;
                    match fltk::app::event_dy() {
                        MouseWheel::None => (),
                        MouseWheel::Up => inc_or_dec = freq_inc,
                        MouseWheel::Down => inc_or_dec = freq_dec,
                        MouseWheel::Right => (),
                        MouseWheel::Left => (),
                    }
                    println!("Scroll {}", inc_or_dec);
                    // How do I call this method?
                    // self.inc_dec_freq(inc_or_dec);
                    true
                }
                _ => false
            }
        });
        return frame;
    }

    // Increment or Decrement frequency by the amount of the digit weight
    fn inc_dec_freq(&mut self, inc_or_dec: u32) {
        // Update current freq holder
        self.current_freq_in_hz = self.current_freq_in_hz + inc_or_dec;
        // Update the display
        self.set_freq(self.current_freq_in_hz);
        // Update the radio
        self.i_cc.lock().unwrap().cc_set_rx_tx_freq(self.current_freq_in_hz);
    }

    // Set display frequency
    fn set_display_freq(&mut self, freq : &String) {
        for i in 0..freq.len() {
            let mut digit = self.digit_map.get_mut(&(i as i32)).unwrap();
            digit.set_label(&freq.chars().nth(i).unwrap().to_string());
        }

    }

}

Ahh you're doing a lot more work in the full version, so just moving fields into the closure won't be enough.

The fltk-rs book has some options for state management.

One option would be to wrap VFOState itself in Arc<Mutex<VFOState>> or Rc<RefCell<VFOState>>. That's relatively simple, but I wouldn't really recommend doing that if you're building a complex UI though. It can be pretty easy to end up with deadlocks/panics from accessing the same instance later in the call stack. If you aren't careful.

The calculator example has a concrete way to set up something that responds to interaction by keeping the state entirely in main. The downside of a strategy like that where you keep all of your state separate from the widgets is that it doesn't handle dynamic data as nicely. You end up needing to identify which part of the state each message needs to mutate, which can get quite complicated[1].


  1. That problem pops up a lot in Redux-like patterns for frontend JS development ↩ī¸Ž

1 Like

Thanks. As you say it must be a common requirement. I did wonder if I could create a user event and field it in the parent. I also looked at app.wait() to see if there was some kind of messaging that might work which I could pick up in the event loop. Looks like calculator does something like that. I guess I need to think through all the options and try them out until something works.

I think the simplest thing to do is to use a global (wrapped in once_cell or lazy_static), since VFOState seems like a singleton object. I would probably remove the widgets into a different struct.

You could altenatively wrap current_freq_in_hz inside an Arc Mutex, clone it into the handle method and replicate the functionality of set_freq, so some code duplication.

Another option is to use a channel and send a message from the handle method. The method can be handled in app.wait() or app::add_idle(). You could store the app object and the receiver in VFOState and provide your own run() method.

Thanks all. I eventually got this to work using a channel which I think is the best solution. It was a pain passing the send end from the main window module down to the component vfo and then into the closure. For some reason using the form.emit() method did not work but channel.send() did. I am now able to tune the radio which is kind of essential.