Research: settings drawer as tabs (General / Keybindings / Advanced) — libcosmic primitives, first-party patterns, and a plan for DropTerm

Date: 2026-10-05 Question: ROADMAP.md:31 asks for “Settings sidebar as tabs - General, Keybindings, Advance”. Which libcosmic widgets at the pinned rev actually implement tabbed navigation, how do the first-party COSMIC apps do it, and what is the concrete design for DropTerm’s in-drawer settings pages — including the new Keybindings editor and the existing Profiles sub-page?

Recommendation

Put a horizontal widget::tab_bar (segmented_button::Model<SingleSelect>, SegmentedButton::TabBar style) in the context drawer’s header slot — the pinned slot above the drawer’s scrollable — and drive it from a small SettingsTab enum stored next to today’s settings_page. Regroup by view only: General = the existing Appearance + Geometry + Behavior + Terminal cards plus the Profiles entry row, Advanced = the existing Advanced card, Keybindings = a brand-new editor page transplanted from cosmic-term’s KeyboardShortcuts drawer. Profiles stays a drill-down page layered over the tabs, exactly like today.

Why this shape:

Alternatives considered and rejected:

Findings

All libcosmic citations are from the local cargo checkout of pop-os/libcosmic @ 08a09e3dfa1f729d52585fcedd6e731130235363 at ~/.cargo/git/checkouts/libcosmic-41009aea1d72760b/08a09e3/ — the rev DropTerm’s Cargo.toml pins. Upstream app citations are from fresh clones (fetched 2026-10-05): cosmic-edit @ 13987165088d991d3c024b5bbb8cd7eb89b4c2ad, cosmic-files @ f7679791f4fbf82ba7c9a01cf3f8ae0e21265975, cosmic-term @ 3ef31d750d3b09a73f266923b0d64c5df038cf9f — all three pin libcosmic 08a09e3 byte-identical to DropTerm — and cosmic-settings @ 82492410584c7c010892d877ed7d19c10578aadb (master; pins the newer d77e99f — cited for pattern only, see finding 2’s caveat).

1. What “sidebar as tabs” maps to in libcosmic @ 08a09e3 (Q1)

There is no widget::sidebar and no free-standing widget::tabs. Every tabbed/sidebar UI in the ecosystem composes from one engine: widget::segmented_button (src/widget/segmented_button/mod.rs), whose Model<SelectionMode> holds rows (text, icon, arbitrary data<Data> payloads, dividers, activation) and whose single SegmentedButton widget renders horizontally or vertically via type-state variants. Everything else is a preset over it:

Preset Constructor Look (theme variant) Sizing defaults Fits a 344-480px drawer?
App sidebar widget::nav_bar(model, on_activate) — src/widget/nav_bar.rs:25-32; types nav_bar::Id/nav_bar::Model = segmented_button::{Entity, SingleSelectModel} — :17-19 SegmentedButton::NavBar, primary-layer bg, own scrollable — src/widget/nav_bar.rs:169-193 32px buttons, height(Length::Fill), width shrink-to-content (From<NavBar> for Container, :169-193) No — a real sidebar column; needs a window-sized pane next to it
Document tabs widget::tab_bar::horizontal(model) — src/widget/tab_bar.rs:17-34; vertical(model) at :36-54 SegmentedButton::TabBar min button width 76, max 250, height 44, padding space_s/space_xs (overridable via derived setters) Yes — 3 × ~76-100px ≈ 230-300px
Choice control widget::segmented_control::horizontal(model) — src/widget/segmented_control.rs:17-37 SegmentedButton::Control height 32, dividers Yes, but wrong semantics (see Recommendation)
Raw engine segmented_button::horizontal/vertical(model) — src/widget/segmented_button/{horizontal,vertical}.rs:24/:23 any via .style(theme::SegmentedButton::…) (variants: TabBar default, Control, NavBar, FileNav, Custom — src/theme/style/segmented_button.rs:15-26) new(): width Length::Fill, height Length::Shrink, font 14, font_active semibold — src/widget/segmented_button/widget.rs:216-264 Yes — but the presets already encode the house look

API shape that matters for wiring (all from the checkout):

Drawer vs full window. nav_bar is designed for the window’s left edge: libcosmic’s view_main inserts the nav_bar() trait hook’s element there (src/app/mod.rs:644-660), and its From impl pins height(Length::Fill) with a primary background (src/widget/nav_bar.rs:169-193) — a pane, not a strip. tab_bar/segmented_control are embeddable rows that size to their buttons; they are the only one of the three that belongs inside a 344-480px drawer pane. Crucially, the drawer already provides a home for a pinned strip: the pane is column![header, scrollable(content), footer] where header = close-row + optional title + the caller’s header element (src/widget/context_drawer/widget.rs:95-106, pane assembly :107-119) — so a tab strip passed as the header never scrolls away. The pane’s scrollable is the framework’s (:115-118); apps do not add their own around page content (confirmed in use: cosmic-term’s settings/shortcuts pages are bare settings::view_columns).

2. How the first-party apps do it (Q2)

3. What the tabs ride in DropTerm (verified locally)

The drawer hosting, page-turn, and input seams the roadmap item needs already exist:

4. The Keybindings tab: brand-new, ported from cosmic-term (Q3, part 2)

Nothing existing covers it: keybinds.rs’s own module doc says “Unlike cosmic-term, these are not user-customizable: there is no shortcuts settings page yet” (src/keybinds.rs:5-7); DropTermConfig has no binding fields (the ConfigKey list at src/settings.rs:108-133 is the whole UI surface, 23 keys, none a binding); there is no capture UI, display-string helper, or groups/labels anywhere in the repo. So the tab is new build-out in three layers, each with a same-rev template:

Scope trim worth making explicit: DropTerm needs one binding per action (its Actions are single-key semantics today), so the cosmic-term multi-binding list plus Disable marker can be simplified to replace-on-capture with per-row delete restoring the default — or kept verbatim if multi-bind is wanted later. Decide in the module spec, not here.

5. The design, concretely (Q3, part 1)

Widget. widget::tab_bar::horizontal(&model) (TabBar preset) at 32px height like cosmic-edit (src/main.rs:3056 override), placed in the drawer’s header slot:

// settings.rs — the tab strip, built from the stored model
pub fn tab_header(model: &TabModel, on_pick: fn(segmented_button::Entity) -> SettingsEvent)
    -> Element<'static, SettingsEvent>
{
    widget::tab_bar::horizontal(model)
        .button_height(32)
        .on_activate(on_pick)  // emits PickTab(Entity)
        .into()
}

passed as the third argument at src/app.rs:4014 (today None), so it renders pinned under the title and above the scrollable (libcosmic src/widget/context_drawer/widget.rs:95-119 @ 08a09e3).

State. One enum plus one stored model field, mirroring cosmic-edit’s tab_model (cosmic-edit src/main.rs:459):

Regrouping. Tab membership is a pure function, the section registry untouched:

Rejected regrouping: hiding Terminal under Advanced (Shell/Args is everyday configuration) — and any regrouping that edits Section::ALL/keys(), which would churn the pinned control→key map for zero user value. drawer_view becomes general_view (loop over the tab’s sections + entry card); the drawer_section/drawer_item/drawer_control chain (src/settings.rs:1664-1707) is reused verbatim.

Profiles interaction. Unchanged mechanics, one new rule: OpenProfiles flips settings_page = Tab(<current>) → Profiles (remember nothing — Back always returns to General, where the entry row lives; if the entry row ever appears on other tabs, store the origin tab instead). BackFromProfiles sets Tab(General); leave_profiles_page (src/app.rs:1632-1639) resets to Tab(General) on explicit close; focus-loss hides still preserve tab + selection + draft (docs/settings.md). The drawer title follows the page, as today (src/app.rs:3998-4011): “Settings” while on any tab (the tabs disambiguate), “Profiles” on the drill-down.

Docs. docs/settings.md’s “Sections and commit points” and “Profiles page” sections need a rewrite for the tab structure; the “When each edit takes effect” table gains the keybinding row (“instant — the very next key press”, mirroring cosmic-term’s save_shortcuts_custom → immediate key_binds rebuild, cosmic-term src/main.rs:614-630).

6. File layout (Q4): stay one src/settings.rs

Keep settings.rs a single file; do not create src/settings/. The case fails the repo’s own bar (AGENTS.md “File creation”: split only when sections are unrelated, a one-word concept names the unit, and contents change independently). What the file actually is: one cohesive module — the settings drawer’s write contract plus every page it hosts — whose halves change together:

The one extraction that would be justified later: the keybinding model if it grows grouping/labeling/search metadata beyond the editor’s needs — but that lands in keybinds.rs, not a new file.

7. Risks / gotchas (Q5)

Sources