Lagrange v1.7: Bookmark Folders and Popup Menus

This release adds folders for better bookmark organization, and addresses a long-standing issue with popup menus being constrained inside the main window. That's not all, though! There is a large number of smaller improvements throughout the app: new UI languages, better Windows 10 title bar colors, WebP support, image colorization, focus cycling buttons with the Tab key, and more.

⚠️ Breaking changes

Please note the following when upgrading:

Bookmarks

The bookmarks sidebar has remained at the Minimum Viable stage for quite some time now as it has been largely unchanged since the very first releases of Lagrange. A simple alphabetic list becomes awkward to use when you have a larger set of bookmarks. I've been wanting to do something about this for a while now, so v1.7 revises bookmark management in a big way.

Manual sorting

The forced alphabetic sorting has been replaced with manual sorting. This way you can keep items that belong together next to each other. It also becomes de facto chronological when you don't modify the order. To help clean things up, I've included a menu item that sorts bookmarks (and folders) alphabetically.

It was fun implementing item drag-and-drop logic for the list widget; once grabbed, items can be dropped between other items, or over them. The grabbed item needs to be drawn with half opacity, and the list needs to scroll when hovering near the top and bottom. The list widget isn't just used for bookmarks. It's the basis for all the sidebar tabs and quick lookup results, too. I'm expecting to find uses for dragging items elsewhere as well.

Folders

Remote bookmarks have already provided a sort of a way to make folders, if you didn't mind creating separate .gmi files for each folder. However, having actual nested folders makes all the difference when it comes to organizing your bookmarks. If you're anything like me, you'll find it quite enjoyable to come up with a hierarchy of folders and categorize your bookmarks accordingly.

Each folder can be opened or closed. The fold states are saved persistently, and you can have a different set of folders open in the left and right sidebars.

The original thinking behind bookmark organization was to lean more heavily on user-defined category tags. This is still a possibility for the future, but I feel traditional folders are easier to work with for now.

New file format

The first thing I actually did regarding bookmarks was to implement a new, more robust internal file format to support storing more information about each bookmark. The old bookmarks format was rigid and brittle, as it relied on having exactly three lines of text per bookmark, without any structural markup.

A better format was needed, but which one? I could make my own, but then the file could only be parsed by Lagrange. XML would be an obvious choice for bookmarks: there are existing schemas defined for this. However, XML is cumbersome to write β€” it would be nice if the format is usable for humans, too. Ultimately I'd like to have a single format that's a good fit for applying to all the internal configuration files. JSON would be easier to work with, but not optimal either for human-readability or writability. YAML is a mess.

I settled on a subset of TOML. I can't deny that a factor here was nostalgia for early versions of MS-DOS and Windows (circa 3.1) where I first encountered INI files. The full TOML spec is quite overkill for my needs, but the basic syntax is human-readable not overly complex and still powerful enough to support different data types and object/table-oriented data. Implementing a parser for the chosen subset of TOML only took a couple of hours β€” a much better experience than implementing, say, the XML parser for Atom feeds. Using TOML also neatly solves the data export issue, since it's useful as an export format as-is. (Importing will still involve merging two sets of data, so that'll need extra work.)

Popup menus

As I've discussed on the gemlog before, Lagrange uses a custom UI toolkit that I've been developing according to the most pressing needs of the app. One of most severe limitations has been that only a single window can be used. Everything including dialogs and popup menus must only appear inside the one window.

The split view mode introduced in v1.4 represents an important step forward as it made it possible to have multiple UI "roots", i.e., independent top-level widgets. You typically have one root widget per window; in split view mode, you get two fully-fledged root widgets inside a single window. Now in v1.7, I've expanded window management to enable creating new windows that take control of a widget, treating it as their root. This way, a context menu can choose whether it wants to appear inside the main window, or be moved to a separate window for unconstrained placement. There's definitely overhead in creating new windows: due to the internal workings of the text and SDL renderers, windows may not be able to share any resources, especially if they appear on displays whose pixel density is different. (This can naturally be further optimized down the road.)

A simple rule is now applied: if a popup menu doesn't fit in its root, it will be moved to a new window. This solves the obvious usability problem of having a partially visible menu that didn't even indicate some of the items were outside the visible area.

macOS context menus

A central challenge with custom UIs is how they integrate with native OS frameworks. This is perhaps most pronounced on macOS and iOS, where the operating system has a strong and consistent design voice. An app that doesn't conform will look and feel out of place. With that in mind, I've taken the extra step of constructing native AppKit NSMenus on macOS, so these appear instead of the default custom popups. (The same could be done with UIKit on iOS, but I've left that for another time.)

Contents of Lagrange's menus have been defined using abstract "MenuItems" from the get-go, so substituting their concrete instantiation with a different type of widget is pretty trivial. The challenge comes in keeping the communication flowing between the custom widgets and the native ones. I've already made a few tweaks to SDL before to better deal with trackpad scrolling, and now I had to add a small API function to sync mouse and keyboard button state after a context menu is dismissed. Otherwise, SDL thinks any modifiers and buttons that were down when the menu appeared were never released.

As a Mac user, I find these native menus delightful, especially since the main application menu has always been a native one and now there aren't two different kinds of menus in use any more. However, I wonder how far this delves into the uncanny valley between a fully custom UI and a fully native one. The discrepancy between the two can get jarring at times, especially where dropdown menus first show the current value as a custom label and then as a native menu item. At a minimum, this makes me want to switch the UI font to San Francisco on macOS.

Media

WebP image support

If you aren't familiar, WebP is an image compression format that is slightly/moderately better than JPG and PNG depending on the content. Its biggest strength is perhaps supporting an alpha channel with JPG-level compression.

Version 1.7 adds an optional dependency on libwebp so that WebP images can be viewed inside the app. You may find that libwebp is already installed on your system as many other programs use it, so if `pkg-config` finds the library you will get WebP support automatically. I'm including WebP in the prebuilt binaries.

Should you use this for your images? WebP may not be as widely viewable as the ubiquitous JPG/PNG, but it's a pretty good way to squeeze a bunch of kilobytes off your images. Gemini is good for slower internet connections β€” well-compressed images go nicely with that.

Image colorization

A feature first implemented by Γ–ppen for Ariane:

It's pretty straightforward: when colorization is enabled, a color filter is applied to all images viewed in the app. You can choose between a grayscale filter and different page theme based filters.

Subscriptions

The Feeds tab has seen some improvements since its introduction, but over time a couple of glitches have surfaced in the tracking of discovered entries. This release primarly addresses problems with "New Headings" subscriptions: the unread count should be correct after subscribing to a new page, and old headings will not be discarded from the entry database until they actually are removed from the source page. Previously, headings were forgotten after two months, and they would then annoyingly reappear as new entries on a subsequent refresh.

There have been similar problems with regular feed entries. While not fully resolved in v1.7.0, a relatively simple fix should be possible: feed contents should only become stale after the entries are removed from the source. Unlike headings, though, the page URLs of feed entries most likely continue to be valid even after the feed forgets about them, so Lagrange should cache them for some period of time. I'll return to this in a future patch release.

UI refinements

This release has a veritable smΓΆrgΓ₯sbord of little UI improvements.

A darker shade of Windows

We begin with a grim tale of Win32 APIs. Ever since Lagrange v0.1, I've been wondering why is it that some Windows apps get to have a dark title bar in dark mode and some don't. I was finally annoyed enough to search for the answer: it's because dark mode is not officially available in the plain old Win32 API, only in the modern Microsoft UI frameworks. For compatibility reasons, SDL naturally uses the plainest Win32 APIs you can find. However, the functions that control dark mode and title bar colors can still be found in system DLLs, if you dare call them without official headers provided by Microsoft.

The risk is obvious: since these APIs are undocumented, Microsoft may at any point change the DLL entrypoints and trying to call the functions will crash the app or just not do anything useful. Knowing Microsoft, though, Win32 APIs are pretty stable so I'm going to go ahead and use these functions in Lagrange. The benefit is that the window title bar now nicely reflects the app's chosen color scheme. Personally, this tips my preference back to using the standard window frame, as it behaves better than the custom frame.

Let me know if you encounter launch-time crashes on Windows. I've only tested this on recent versions of Windows 10.

Phone/tablet UI build options

I've added CMake build options `ENABLE_MOBILE_PHONE` and `ENABLE_MOBILE_TABLET` that affect which UI variant is used in the app. This was super helpful with developing the latest mobile UI improvements because I didn't have to run the code on a device for rapid iteration.

You will probably be interested in trying out the mobile phone UI option if you use a Linux-based phone (such as PinePhone) that isn't automatically supported by Lagrange.

More efficient UI drawing

In the mobile version, UI transition animations are crucial for communicating state changes. However, mobile devices should also not waste CPU/GPU time pointlessly redrawing all UI elements during such animations. For this purpose I've added a draw buffer to UI widgets that allows them to efficiently move around the screen.

New UI languages

A handful of people have requested new UI languages, and while I've added them in Weblate I haven't yet included all the new ones in the app. That is why there are five new ones this time around: Esperanto, Spanish (Mexico), Galician, Interslavic, and Slovak. (Note that some of these are still works in progress.) Many thanks to the translators! πŸ™

Miscellaneous

What's next?

My fall schedule has proven to be a bit busier than expected, so I'll refrain from planning next steps very concretely. Among the most urgent topics are updating the mobile bookmarks UI, adding some sort of multiple selection feature in lists, a better image viewer, and of course, improved font support. Especially on macOS, UI elements that don't use the system font now clearly clash with the native context menus, putting more pressure on integrating with OS text rendering APIs. It remains to be seen which of these will make it to the next update.

With the latest popup window changes, I've done some in-depth refactoring of window management, as we're moving toward full multi-window support. I'm expecting that a number of new bugs were introduced as a result β€” window management seems to be quite platform-specific and very quirky, in ways that SDL cannot abstract away.

πŸ“… 2021-09-28

🏷 Lagrange

CC-BY-SA 4.0