Gfret 2.0 and beyond

Here we are 2 patch releases in and I never did finish documenting the release of gfret or the finishing of the work. I'm going to keep this post brief and just summarize where the project stands and where it's heading.

Feature parity, ish?

On the day that I released 2.0 into the wild it had essentially achieved feature parity with the 1.x branch, plus a couple nice additional features. There were, however, a couple of regressions that needed fixing. There was also, it turned out, a good bit of optimization left to be done and a few things which I noticed right off that could be improved in the UX department.

The largest regression related to the preferences dialog, specifically revolving around setting a default for an external editor. The Gtk+ toolkit has a nice and easy to use interface around creating an application chooser, which can take the form of a standalone dialog, an embedded widget, and a button which displays the default selection and opens a full dialog when pressed. The button is a nice idea in theory, but I find the interface a bit lacking.

Specifically, in order to set the default item one must create a custom entry and set that as the default. But there is no easy way to match up to an application from the generated list and the command which was saved in the config file. I could probably iterate over the generated list and match the saved string against the commandline returned by each item, but we're talking a pretty tedious bit of code and wasted cpu cycles for little gain here. In the 1.x branch I got around the problem by using a text entry box which was filled in by launching the full dialog via a button next to it.

When I did the initial port of the old gtk3 interface to gtk4, however, the preferences dialog was pretty much rewritten from scratch, and by that time the original problem was a fairly distant memory, so I used the AppChooserButton. Consequently, one could set the default program, but the next time the dialog was opened the button would revert to whatever your system thinks should be the default for mime type "image/svg+xml". The solution, which came in this second point release, was to revert back to the way it was done originally in the 1.x branch.

Too much IO

Rust doesn't have global variables. You can actually define a static, which is by default immutable, and you can even make it a static mut, but that puts us very much into unsafe territory.

This matters because I made a really dumb design choice right at the beginning of the project. There are two kinds of parameters in Gfret, the specifications which are given via the widgets in the main window, which are used to generate the Specs struct, and the mostly styling related options which reside in the Config struct. Both structs are used when creating a document, but the backend crate fretboard_layout will accept None for the config, in which case it just uses default values.

My dumb decision revolved around how the Config values were stored and how they were then retrieved when it is time to create a new image. With no globals I had the program read the Config values off of disk each time an image was to be generated. This might not sound bad, but lets keep going here. Each time one of the widgets in the main window gets wiggled, slid, selected, or in any way has new input we regenerate the preview. The problem should be really apparent now.

It gets even worse on a slow disk or a network share. No, I never saw an issue and the gui has always been nice and responsive for me. But I simply can't abide touching the disk any more than absolutely necessary. So I changed it.

I did consider, and even tried out, just going ahead and using static mut and wrapping each access in an unsafe block. After all, the data is only going to be written to in one place, which is when we hit the Accept button on the preferences window. But it didn't sit well with me, and upon investigation there is a still active issue which could well mean the deprecation of that entire interface. Upon reading through the issue and a few good blog posts on the subject I decided to go another way. So here's what we get.

// main.rs - here we're defining our config as a static ref to a `Mutex` lock
#[macro_use]
extern crate lazy_static;

lazy_static! {
    static ref CONFIG: Mutex<Config> = Mutex::new(
        config::GfretConfig::from_file()
        .unwrap_or_default()
        .to_config()
    );
}

// gui/mod.rs - and here we're getting that value and using it to create our
// preview image
    fn draw_preview(&self, swap: bool) {
        let cfg = CONFIG.lock().unwrap().clone();
        let image = self.get_specs().create_document(Some(cfg)).to_string();
        // more...

// gui/dialogs/mod.rs - updating the ref, and saving to disk
    pub fn save_prefs(&self) {
        let config_file = crate::config::get_config_file();
        let config_data = self.config_from_widgets();
        let mut cfg = CONFIG.lock().unwrap();
        *cfg = config_data.to_config();
        config_data.save_to_file(&config_file);
    }

Backend cleanup

The backend crate, fretboard_layout, saw a lot of cleanup prior to 2.0. However, there was more that could be done. I went through the crate and wound up merging one of the modules back into lib.rs, and significantly slimmed down the public interface by removing a lot of needless pub declarations.

The way the frets were being generated originally was also sub-optimal. To generate a fret, one must plug the scale length for each side into a function which generates the lengths to that fret from the bridge. Those lengths must then be plugged into a function which uses some trigonometry to plot them in 2d space. Finally, the resulting points must be turned into svg data. The math was always correct, but in my original pipeline I was iterating over each fret number and doing all of the math, arriving at my 2d points for each fret end, and then collecting them into a vec and interating over that vec to generate the svg group of fret lines. That vec involves a needless allocation, when we can simply iterate over the number of frets and take each result to generate the next step, all the way to svg. So I got rid of it in the 0.2.0 release of fretboard_layout, which made it's way into gfret for the first patch release (2.0.1).

Units

2.0 saw two new features, the ability to generate left-handed output for a multiscale design and the ability to switch freely between Metric and Imperial measurements. 2.0.2 sees one or two UX improvements around that feature. When Metric units are selected, making our default unit of measurement the millimeter, any spinbuttons in the interface which display those measurements will have two decimal places of precision. When Imperial units are selected, and we're measuring in Inches, those fields will now have three decimal places of precision. Not a huge change, but it keeps the interface more consistent.

The future

  • Currently, when generating left handed output, the image is reversed top to bottom. This will be changed in the backend to flip the output left to right instead for the 2.1.0 release.
  • Make file IO async
  • Investigate doing calculations async and in parallel
  • Optionally add inlays
  • Optionally add gradient fill
  • Optionally show string lines