Drag-reorder sessions in the sidebar #1

Open
opened 2026-08-19 14:27:33 +00:00 by dusan · 1 comment
Owner

Context

Terminalko is a GTK4/libadwaita terminal emulator in Rust, organised around a
vertical session sidebar instead of a tab bar. Sessions live in folders (one
level, by design) and their order is user-visible and persisted.

Relevant code:

  • crates/terminalko/src/window.rs — builds the sidebar (rebuild_sidebar,
    sidebar_item), holds imp.order (session id per row, in display order) and
    the session actions.
  • crates/terminalko-config/src/session.rs — the model. Order and membership
    live only in each folder's sessions list key (and scratch-sessions for
    unfiled ones). Store::move_session(id, folder, position) and
    Store::shift_session(id, delta) already exist and are unit-tested.
  • docs/spec/session-sidebar.md — the sidebar's specification, which records
    this gap.

Problem

Reordering sessions, and moving them between folders, is currently only possible
through the row's context menu (Move Up / Move Down / Move to folder).
Dragging a row is the obvious gesture and does nothing.

Why it was not done

adw::Sidebar (libadwaita 1.9) exposes the drop side only —
connect_drop, connect_drop_enter, connect_drop_value_loaded,
setup_drop_target — and has no per-row drag source, so a row cannot be
picked up. Verified against libadwaita 0.9.2 bindings; AdwSidebarItem is a
plain GObject with no widget and no setters, so nothing can be attached to a
row either.

Reordering by dragging therefore means replacing adw::Sidebar with a
hand-built list.

Suggested approach

Replace the sidebar widget with GtkListView + GtkSelectionModel over a
GListStore of row objects, keeping the same visual language:

  1. A small TkSessionRow GObject holding session id, name, kind icon, accent
    and state, so a row widget can be bound to it and updated in place (this also
    removes the current rebuild-everything approach).
  2. GtkListItemFactory producing a row widget: 4px accent bar, kind icon, name,
    badge cluster, state dot, close-on-hover — see accent::row_suffix.
  3. GtkDragSource on each row carrying the session id, and GtkDropTarget on
    rows and on folder headers. Drop above/below is decided from the pointer
    position within the row.
  4. On drop call the existing Store::move_session(id, folder, position); the
    model is already the single source of truth, so nothing else needs to change.
  5. Sections: folders become headers, either with GtkListView's header factory
    or a flattened model carrying header rows.

Keep the existing keyboard reordering: dragging must not become the only way.

Acceptance criteria

  • A session can be dragged within a folder and between folders, and the new
    order survives restarting the application.
  • Dropping onto a folder header appends to that folder.
  • Sessions can still be reordered from the context menu and by keyboard.
  • Selection follows the session, not the position, across a reorder.
  • ./build-aux/ci.sh passes (fmt, clippy with -D warnings, tests, release
    build) and ./build-aux/smoke.sh passes (needs a display).
  • docs/spec/session-sidebar.md updated: the "drag-and-drop reordering is not
    implemented" paragraph replaced with what it now does.

Notes

  • The project is BSD-2-Clause and must stay clean of GPL code: do not copy
    from GNOME Console or Ptyxis. See CONTRIBUTING.md for the rule and the
    spec-first process.
  • Prefer tests that do not need a display: the model side is already testable
    headless (GSETTINGS_BACKEND=memory), so put reorder logic there rather than
    in the widget where possible.
## Context Terminalko is a GTK4/libadwaita terminal emulator in Rust, organised around a vertical **session sidebar** instead of a tab bar. Sessions live in folders (one level, by design) and their order is user-visible and persisted. Relevant code: - `crates/terminalko/src/window.rs` — builds the sidebar (`rebuild_sidebar`, `sidebar_item`), holds `imp.order` (session id per row, in display order) and the session actions. - `crates/terminalko-config/src/session.rs` — the model. Order and membership live **only** in each folder's `sessions` list key (and `scratch-sessions` for unfiled ones). `Store::move_session(id, folder, position)` and `Store::shift_session(id, delta)` already exist and are unit-tested. - `docs/spec/session-sidebar.md` — the sidebar's specification, which records this gap. ## Problem Reordering sessions, and moving them between folders, is currently only possible through the row's context menu (*Move Up* / *Move Down* / *Move to folder*). Dragging a row is the obvious gesture and does nothing. ## Why it was not done `adw::Sidebar` (libadwaita 1.9) exposes the **drop** side only — `connect_drop`, `connect_drop_enter`, `connect_drop_value_loaded`, `setup_drop_target` — and has **no per-row drag source**, so a row cannot be picked up. Verified against libadwaita 0.9.2 bindings; `AdwSidebarItem` is a plain `GObject` with no widget and no setters, so nothing can be attached to a row either. Reordering by dragging therefore means replacing `adw::Sidebar` with a hand-built list. ## Suggested approach Replace the sidebar widget with `GtkListView` + `GtkSelectionModel` over a `GListStore` of row objects, keeping the same visual language: 1. A small `TkSessionRow` GObject holding session id, name, kind icon, accent and state, so a row widget can be bound to it and updated in place (this also removes the current rebuild-everything approach). 2. `GtkListItemFactory` producing a row widget: 4px accent bar, kind icon, name, badge cluster, state dot, close-on-hover — see `accent::row_suffix`. 3. `GtkDragSource` on each row carrying the session id, and `GtkDropTarget` on rows and on folder headers. Drop above/below is decided from the pointer position within the row. 4. On drop call the existing `Store::move_session(id, folder, position)`; the model is already the single source of truth, so nothing else needs to change. 5. Sections: folders become headers, either with `GtkListView`'s header factory or a flattened model carrying header rows. Keep the existing keyboard reordering: dragging must not become the only way. ## Acceptance criteria - A session can be dragged within a folder and between folders, and the new order survives restarting the application. - Dropping onto a folder header appends to that folder. - Sessions can still be reordered from the context menu and by keyboard. - Selection follows the session, not the position, across a reorder. - `./build-aux/ci.sh` passes (fmt, clippy with `-D warnings`, tests, release build) and `./build-aux/smoke.sh` passes (needs a display). - `docs/spec/session-sidebar.md` updated: the "drag-and-drop reordering is not implemented" paragraph replaced with what it now does. ## Notes - The project is **BSD-2-Clause** and must stay clean of GPL code: do not copy from GNOME Console or Ptyxis. See `CONTRIBUTING.md` for the rule and the spec-first process. - Prefer tests that do not need a display: the model side is already testable headless (`GSETTINGS_BACKEND=memory`), so put reorder logic there rather than in the widget where possible.
Author
Owner

Numbers from #17, since they change what this issue is worth and one of its design choices.

rebuild_sidebar is 152.6 ms at five hundred sessions across twenty folders on a release build, and one rebuild stops fitting in a 60 Hz frame at about sixty sessions. So the list view here is not only about dragging — it is the fix for the rebuild cost too, and the second reason to do it.

Two findings that bear on the approach:

  • Building widgets is 99% of the cost. Store::layout plus every per-row settings read is 1.7 ms of the 152.6 ms, so the model side needs nothing. A GtkListView that builds widgets only for visible rows fixes essentially all of it, and TkSessionRow need not cache anything from the store to be fast.
  • Sections are expensive. The same five hundred sessions in a single section cost 83 ms against 153 ms across twenty. Whatever replaces AdwSidebarSection — header factory or flattened model — should not build a widget per folder eagerly. This is a point in favour of GtkListView's header factory over a flattened model carrying header rows.

Reproduce with cargo run -p terminalko-config --example gen-tree (added in #27); it refuses to run unless GSETTINGS_BACKEND is keyfile or memory, because it erases the tree first.

Numbers from #17, since they change what this issue is worth and one of its design choices. `rebuild_sidebar` is **152.6 ms** at five hundred sessions across twenty folders on a release build, and one rebuild stops fitting in a 60 Hz frame at about sixty sessions. So the list view here is not only about dragging — it is the fix for the rebuild cost too, and the second reason to do it. Two findings that bear on the approach: - **Building widgets is 99% of the cost.** `Store::layout` plus every per-row settings read is 1.7 ms of the 152.6 ms, so the model side needs nothing. A `GtkListView` that builds widgets only for visible rows fixes essentially all of it, and `TkSessionRow` need not cache anything from the store to be fast. - **Sections are expensive.** The same five hundred sessions in a single section cost 83 ms against 153 ms across twenty. Whatever replaces `AdwSidebarSection` — header factory or flattened model — should not build a widget per folder eagerly. This is a point in favour of `GtkListView`'s header factory over a flattened model carrying header rows. Reproduce with `cargo run -p terminalko-config --example gen-tree` (added in #27); it refuses to run unless `GSETTINGS_BACKEND` is `keyfile` or `memory`, because it erases the tree first.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dusan/terminalko#1
No description provided.