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