tui: ControlNet picker (unit configuration) #150

Merged
charles merged 2 commits from tui/controlnet-picker into main 2026-04-16 08:56:23 +00:00
Collaborator

Closes charles/loom#141

Summary

  • Adds a ControlNet units section to the Generate screen, rendered when mode is ControlNet.
  • a adds a unit (up to 4), dd removes the focused unit. Enter opens an inline editor that cycles through image path / module / model / weight / guidance start / guidance end.
  • Module + model are picked via StringPickerOverlay variants backed by bridge.list_controlnet_modules() and bridge.list_controlnet_models().
  • submission_params() strips controlnet_units when the active mode is not ControlNet.

Test plan

  • cargo fmt --all
  • cargo build (full workspace)
  • cargo test -p loom-tui (178 pass)
  • cargo clippy -p loom-tui -- -D warnings
  • Manual: open Generate, switch to mode 4, press a, type an image path, Enter on module → picker opens, Enter on model → picker opens, F5 submits.
Closes charles/loom#141 ## Summary - Adds a ControlNet units section to the Generate screen, rendered when mode is ControlNet. - `a` adds a unit (up to 4), `dd` removes the focused unit. Enter opens an inline editor that cycles through image path / module / model / weight / guidance start / guidance end. - Module + model are picked via `StringPickerOverlay` variants backed by `bridge.list_controlnet_modules()` and `bridge.list_controlnet_models()`. - `submission_params()` strips `controlnet_units` when the active mode is not ControlNet. ## Test plan - [x] `cargo fmt --all` - [x] `cargo build` (full workspace) - [x] `cargo test -p loom-tui` (178 pass) - [x] `cargo clippy -p loom-tui -- -D warnings` - [ ] Manual: open Generate, switch to mode 4, press `a`, type an image path, Enter on module → picker opens, Enter on model → picker opens, F5 submits.
feat(tui): #141 ControlNet picker (unit configuration)
Some checks failed
qa / qa (pull_request) Has been cancelled
763b9aea18
Render a "ControlNet units" panel on the Generate screen when mode is
ControlNet. Supports up to 4 units; each unit carries image path,
preprocessor module, model, weight, and guidance start/end. `a` adds a
unit and opens an inline editor that cycles through sub-fields; `dd`
removes the focused unit; Enter on module/model opens a picker overlay
backed by `bridge.list_controlnet_modules()` /
`bridge.list_controlnet_models()`. Weight steps ±0.1 in [0, 2];
guidance start/end step ±0.05 in [0, 1] with start <= end preserved.

Units are kept in screen state across mode switches but stripped at
submission when mode != ControlNet via `submission_params()`.

Closes charles/loom#141
claude-desktop left a comment

Good test coverage (cap, clamp, strip-on-submit, selection reclamp). Some review points:

Correctness

  • submission_params strips units when mode ≠ ControlNet, but units stay in params.controlnet_units. If the user configures units, switches to t2i, and switches back, units return — that's nice. But render_controlnet_panel is only rendered when show_cn so configured units become invisible yet persist in state. Consider rendering a disabled/dim hint when there are units but mode is not ControlNet, so the user doesn't think they vanished.
  • adjust_focused_cn_guidance_start computes next = clamp(0..1) then next.min(u.guidance_end) — correct. Symmetric end path also correct.
  • dd chord has no timeout. If the user presses d, gets distracted, comes back minutes later and presses d again, the second unit is deleted. Either add a timeout (e.g. 800 ms) or require an explicit confirm. The current controlnet_pending_d resets on any other key, which helps, but idle time doesn't clear it.
  • handle_controlnet_list_key always returns true for j/k/Up/Down even when the list is empty — that's fine (eats the key) but on an empty list those keys should probably fall through to field navigation, otherwise the user is stuck on ControlNetList focus with no way to move using arrows. Test this.

Style

  • default_controlnet_unit() duplicates the model's default. If ControlNetUnit: Default isn't already defined, add it there rather than a free function in the TUI crate.
  • render_controlnet_editor's fields array is recomputed every frame — fine, but consider moving the label+value derivation into an iterator over ControlNetEditField::ORDER for consistency with next/prev.
  • GenerateFocus::ORDER length bumped from 13 → 14 manually — prefer ORDER.len() wherever possible.
Good test coverage (cap, clamp, strip-on-submit, selection reclamp). Some review points: **Correctness** - `submission_params` strips units when mode ≠ ControlNet, but units stay in `params.controlnet_units`. If the user configures units, switches to t2i, and switches back, units return — that's nice. But `render_controlnet_panel` is only rendered when `show_cn` so configured units become invisible yet persist in state. Consider rendering a disabled/dim hint when there are units but mode is not ControlNet, so the user doesn't think they vanished. - `adjust_focused_cn_guidance_start` computes `next = clamp(0..1)` then `next.min(u.guidance_end)` — correct. Symmetric end path also correct. - `dd` chord has no timeout. If the user presses `d`, gets distracted, comes back minutes later and presses `d` again, the second unit is deleted. Either add a timeout (e.g. 800 ms) or require an explicit confirm. The current `controlnet_pending_d` resets on any other key, which helps, but idle time doesn't clear it. - `handle_controlnet_list_key` always returns `true` for `j`/`k`/Up/Down even when the list is empty — that's fine (eats the key) but on an empty list those keys should probably fall through to field navigation, otherwise the user is stuck on ControlNetList focus with no way to move using arrows. Test this. **Style** - `default_controlnet_unit()` duplicates the model's default. If `ControlNetUnit: Default` isn't already defined, add it there rather than a free function in the TUI crate. - `render_controlnet_editor`'s `fields` array is recomputed every frame — fine, but consider moving the label+value derivation into an iterator over `ControlNetEditField::ORDER` for consistency with `next`/`prev`. - `GenerateFocus::ORDER` length bumped from 13 → 14 manually — prefer `ORDER.len()` wherever possible.
- dd chord now enforces an 800ms idle timeout via `controlnet_pending_d_at: Instant`; stale
  first-presses are treated as fresh so a distracted user can't silently delete a unit minutes
  later.
- j/k/Up/Down on an empty ControlNet unit list fall through to field navigation instead of
  being swallowed.
- When mode ≠ ControlNet but configured units exist, render a dim one-line hint so the user
  doesn't think units vanished.
- Add `impl Default for ControlNetUnit` in loom-core and drop the free
  `default_controlnet_unit()` helper from the TUI crate.
- Tests: dd timeout (within and past window) plus empty-/non-empty-list consume behaviour.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Collaborator

Review addressed in bbffd57:

  • dd idle timeout — added controlnet_pending_d_at: Option<Instant> and an 800ms CONTROLNET_DD_CHORD_TIMEOUT. A stale first d past the window re-arms a fresh chord instead of silently deleting. Covered by controlnet_dd_chord_deletes_focused_unit_within_timeout and controlnet_dd_chord_expires_after_timeout.
  • Empty-list fall-throughj/k/Up/Down now return false when controlnet_units is empty so the key moves to the next/prev field instead of getting swallowed. Covered by controlnet_empty_list_lets_navigation_keys_fall_through and controlnet_non_empty_list_consumes_navigation_keys.
  • Hidden-units hint — when mode ≠ ControlNet but units exist, a one-line dim hint (N ControlNet unit(s) hidden — switch to ControlNet mode to edit) renders where the panel would be, so configured units don't appear to vanish.
  • ControlNetUnit: Default — added in loom-core/src/models/generation.rs (reusing the existing super::defaults::* helpers); the TUI-crate default_controlnet_unit() free function is gone.
  • Focus ORDER — no hard-coded 13/14 usages remain outside the array-size literal itself (which must stay a usize constant).

cargo fmt --all, cargo build, cargo test -p loom-tui, cargo clippy -p loom-tui -- -D warnings all pass.

Review addressed in `bbffd57`: - **`dd` idle timeout** — added `controlnet_pending_d_at: Option<Instant>` and an 800ms `CONTROLNET_DD_CHORD_TIMEOUT`. A stale first `d` past the window re-arms a fresh chord instead of silently deleting. Covered by `controlnet_dd_chord_deletes_focused_unit_within_timeout` and `controlnet_dd_chord_expires_after_timeout`. - **Empty-list fall-through** — `j`/`k`/`Up`/`Down` now return `false` when `controlnet_units` is empty so the key moves to the next/prev field instead of getting swallowed. Covered by `controlnet_empty_list_lets_navigation_keys_fall_through` and `controlnet_non_empty_list_consumes_navigation_keys`. - **Hidden-units hint** — when mode ≠ ControlNet but units exist, a one-line dim hint `(N ControlNet unit(s) hidden — switch to ControlNet mode to edit)` renders where the panel would be, so configured units don't appear to vanish. - **`ControlNetUnit: Default`** — added in `loom-core/src/models/generation.rs` (reusing the existing `super::defaults::*` helpers); the TUI-crate `default_controlnet_unit()` free function is gone. - **Focus `ORDER`** — no hard-coded `13`/`14` usages remain outside the array-size literal itself (which must stay a `usize` constant). `cargo fmt --all`, `cargo build`, `cargo test -p loom-tui`, `cargo clippy -p loom-tui -- -D warnings` all pass.
charles deleted branch tui/controlnet-picker 2026-04-16 08:56:23 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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
charles/loom!150
No description provided.