Use slider toggles for boolean controls

This commit is contained in:
Georg Bauer
2026-07-28 18:38:30 +02:00
parent 9cf1c70a67
commit 00af0335ff
5 changed files with 28 additions and 23 deletions

View File

@@ -12,6 +12,7 @@
- Follow the Codex macOS dark UI: near-black backgrounds, subtly raised surfaces, quiet borders, rounded corners, and restrained contrast. - Follow the Codex macOS dark UI: near-black backgrounds, subtly raised surfaces, quiet borders, rounded corners, and restrained contrast.
- Present overviews as one bordered panel with divided rows, a clear primary label, muted supporting text, and compact trailing actions. - Present overviews as one bordered panel with divided rows, a clear primary label, muted supporting text, and compact trailing actions.
- Reuse the shared surface and action-button styles in `src/app/view.rs`; keep button shape, padding, typography, hover, and disabled states consistent across windows. Destructive actions may differ by color only, while navigation controls may remain flat. - Reuse the shared surface and action-button styles in `src/app/view.rs`; keep button shape, padding, typography, hover, and disabled states consistent across windows. Destructive actions may differ by color only, while navigation controls may remain flat.
- Render boolean controls as macOS-style slider toggles through the shared `toggle` helper, not checkboxes.
- Prefer generous spacing and clear hierarchy over decoration; avoid one-off colors, card stacks, oversized controls, and screen-specific button styling. - Prefer generous spacing and clear hierarchy over decoration; avoid one-off colors, card stacks, oversized controls, and screen-specific button styling.
## Commit gates ## Commit gates

View File

@@ -20,9 +20,9 @@ use crate::model::{
use crate::settings::{GIB, REASONING_MODES}; use crate::settings::{GIB, REASONING_MODES};
use iced::theme::{Palette, palette}; use iced::theme::{Palette, palette};
use iced::widget::{ use iced::widget::{
Button, Space, Svg, Tooltip, button, checkbox, column, container, image, markdown, mouse_area, Button, Space, Svg, Tooltip, button, column, container, image, markdown, mouse_area, opaque,
opaque, pick_list, progress_bar, row, rule, scrollable, slider, stack, svg, text, text_editor, pick_list, progress_bar, row, rule, scrollable, slider, stack, svg, text, text_editor,
text_input, tooltip, text_input, toggler, tooltip,
}; };
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window}; use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -984,6 +984,10 @@ fn danger_button<'a>(content: impl Into<Element<'a, Message>>) -> Button<'a, Mes
button(content).padding([8, 14]).style(danger_button_style) button(content).padding([8, 14]).style(danger_button_style)
} }
fn toggle<'a>(checked: bool) -> iced::widget::Toggler<'a, Message> {
toggler(checked).size(18).spacing(8)
}
fn action_button_style(_: &Theme, status: button::Status) -> button::Style { fn action_button_style(_: &Theme, status: button::Status) -> button::Style {
let (background, text_color) = match status { let (background, text_color) = match status {
button::Status::Active | button::Status::Pressed => { button::Status::Active | button::Status::Pressed => {

View File

@@ -495,10 +495,10 @@ impl App {
&surface.data, &surface.data,
context, context,
)); ));
let checkbox = checkbox(checked).label(label); let control = toggle(checked).label(label);
if let Some(path) = local_path(binding_path(binding), context_path.as_deref()) { if let Some(path) = local_path(binding_path(binding), context_path.as_deref()) {
let surface_id = surface.id.clone(); let surface_id = surface.id.clone();
checkbox control
.on_toggle(move |value| { .on_toggle(move |value| {
Message::A2uiDataChanged( Message::A2uiDataChanged(
surface_id.clone(), surface_id.clone(),
@@ -508,7 +508,7 @@ impl App {
}) })
.into() .into()
} else { } else {
checkbox.into() control.into()
} }
} }
"Slider" => { "Slider" => {
@@ -617,7 +617,7 @@ impl App {
} }
chips = chips.push(choice); chips = chips.push(choice);
} else if multiple { } else if multiple {
let mut choice = checkbox(is_selected).label(label); let mut choice = toggle(is_selected).label(label);
if let Some(path) = path.clone() { if let Some(path) = path.clone() {
let surface_id = surface.id.clone(); let surface_id = surface.id.clone();
let option_value = option_value.clone(); let option_value = option_value.clone();

View File

@@ -59,7 +59,7 @@ impl App {
file_list = file_list.push(rule::horizontal(1)).push( file_list = file_list.push(rule::horizontal(1)).push(
container( container(
row![ row![
checkbox(selected).on_toggle({ toggle(selected).on_toggle({
let path = path.clone(); let path = path.clone();
move |_| Message::ToggleGitFile(path.clone()) move |_| Message::ToggleGitFile(path.clone())
}), }),

View File

@@ -9,7 +9,7 @@ impl App {
.supports_dspark() .supports_dspark()
.then_some(Message::PreferenceLegacyMtpChanged); .then_some(Message::PreferenceLegacyMtpChanged);
let legacy_mtp = hint( let legacy_mtp = hint(
checkbox(self.preference_draft.legacy_mtp_enabled) toggle(self.preference_draft.legacy_mtp_enabled)
.label("Enable legacy MTP for this model") .label("Enable legacy MTP for this model")
.on_toggle_maybe(legacy_mtp_toggle), .on_toggle_maybe(legacy_mtp_toggle),
"Uses the managed one-stage MTP support GGUF. The target model verifies every drafted token; it is mutually exclusive with DSpark.", "Uses the managed one-stage MTP support GGUF. The target model verifies every drafted token; it is mutually exclusive with DSpark.",
@@ -20,7 +20,7 @@ impl App {
.supports_dspark() .supports_dspark()
.then_some(Message::PreferenceDsparkChanged); .then_some(Message::PreferenceDsparkChanged);
let dspark = hint( let dspark = hint(
checkbox(self.preference_draft.dspark_enabled) toggle(self.preference_draft.dspark_enabled)
.label("Enable DSpark for this model") .label("Enable DSpark for this model")
.on_toggle_maybe(dspark_toggle), .on_toggle_maybe(dspark_toggle),
"Speculative decoding with the managed DSpark draft artifact: a small model proposes tokens that the main model verifies in one pass. Usually a large speedup; the target model may also stream routed experts from SSD.", "Speculative decoding with the managed DSpark draft artifact: a small model proposes tokens that the main model verifies in one pass. Usually a large speedup; the target model may also stream routed experts from SSD.",
@@ -121,7 +121,7 @@ impl App {
.align_y(Alignment::Center), .align_y(Alignment::Center),
text("Enter a whole number from 1 to 1440.").size(12), text("Enter a whole number from 1 to 1440.").size(12),
hint( hint(
checkbox(self.preference_draft.a2ui_enabled) toggle(self.preference_draft.a2ui_enabled)
.label("Enable interactive A2UI chat surfaces") .label("Enable interactive A2UI chat surfaces")
.on_toggle(Message::PreferenceA2uiChanged), .on_toggle(Message::PreferenceA2uiChanged),
"Lets the local model build validated native charts, tables, forms and other interactive chat surfaces. Turning it off removes the A2UI catalog from the system prompt.", "Lets the local model build validated native charts, tables, forms and other interactive chat surfaces. Turning it off removes the A2UI catalog from the system prompt.",
@@ -134,7 +134,7 @@ impl App {
"LOCAL ENDPOINT", "LOCAL ENDPOINT",
column![ column![
hint( hint(
checkbox(self.preference_draft.endpoint_enabled) toggle(self.preference_draft.endpoint_enabled)
.label("Enable OpenAI-compatible endpoint") .label("Enable OpenAI-compatible endpoint")
.on_toggle(Message::PreferenceEndpointEnabledChanged), .on_toggle(Message::PreferenceEndpointEnabledChanged),
"Serves the loaded model over an OpenAI-style HTTP API, so editors, scripts and agents on this machine can use it. Turned off, only this window can generate.", "Serves the loaded model over an OpenAI-style HTTP API, so editors, scripts and agents on this machine can use it. Turned off, only this window can generate.",
@@ -146,7 +146,7 @@ impl App {
.on_input(Message::PreferenceEndpointPortChanged), .on_input(Message::PreferenceEndpointPortChanged),
), ),
hint( hint(
checkbox(self.preference_draft.endpoint_cors) toggle(self.preference_draft.endpoint_cors)
.label("Allow browser clients (CORS)") .label("Allow browser clients (CORS)")
.on_toggle(Message::PreferenceEndpointCorsChanged), .on_toggle(Message::PreferenceEndpointCorsChanged),
"Answers with permissive CORS headers so JavaScript running in a web page may call the endpoint. Leave it off when only native tools connect.", "Answers with permissive CORS headers so JavaScript running in a web page may call the endpoint. Leave it off when only native tools connect.",
@@ -161,7 +161,7 @@ impl App {
"DEV BRAIN", "DEV BRAIN",
column![ column![
hint( hint(
checkbox(self.preference_draft.dev_brain_enabled) toggle(self.preference_draft.dev_brain_enabled)
.label("Enable project-backed LLM wiki") .label("Enable project-backed LLM wiki")
.on_toggle(Message::PreferenceDevBrainEnabledChanged), .on_toggle(Message::PreferenceDevBrainEnabledChanged),
"Lets the agent use its normal file tools on managed pages in a dedicated Obsidian vault, with indexed search and validation. Disabled means no Dev Brain access or prompt instructions.", "Lets the agent use its normal file tools on managed pages in a dedicated Obsidian vault, with indexed search and validation. Disabled means no Dev Brain access or prompt instructions.",
@@ -228,13 +228,13 @@ impl App {
.spacing(12) .spacing(12)
.align_y(Alignment::Center), .align_y(Alignment::Center),
hint( hint(
checkbox(self.preference_draft.git_indent_heuristic) toggle(self.preference_draft.git_indent_heuristic)
.label("Use indentation heuristic") .label("Use indentation heuristic")
.on_toggle(Message::PreferenceGitIndentHeuristicChanged), .on_toggle(Message::PreferenceGitIndentHeuristicChanged),
"Shift ambiguous hunk boundaries toward indentation changes, which usually makes source-code diffs easier to read.", "Shift ambiguous hunk boundaries toward indentation changes, which usually makes source-code diffs easier to read.",
), ),
hint( hint(
checkbox(self.preference_draft.git_ignore_blank_lines) toggle(self.preference_draft.git_ignore_blank_lines)
.label("Ignore blank-line changes") .label("Ignore blank-line changes")
.on_toggle(Message::PreferenceGitIgnoreBlankLinesChanged), .on_toggle(Message::PreferenceGitIgnoreBlankLinesChanged),
"Hide hunks whose changed lines are all blank.", "Hide hunks whose changed lines are all blank.",
@@ -348,13 +348,13 @@ impl App {
prefill, prefill,
), ),
hint( hint(
checkbox(self.preference_draft.quality) toggle(self.preference_draft.quality)
.label("Prefer exact quality kernels") .label("Prefer exact quality kernels")
.on_toggle(Message::PreferenceQualityChanged), .on_toggle(Message::PreferenceQualityChanged),
"Runs the exact Metal kernels instead of the fast approximations. Slightly slower, and it removes the small numeric differences those approximations introduce.", "Runs the exact Metal kernels instead of the fast approximations. Slightly slower, and it removes the small numeric differences those approximations introduce.",
), ),
hint( hint(
checkbox(self.preference_draft.warm_weights) toggle(self.preference_draft.warm_weights)
.label("Warm mapped weights at load time") .label("Warm mapped weights at load time")
.on_toggle(Message::PreferenceWarmWeightsChanged), .on_toggle(Message::PreferenceWarmWeightsChanged),
"Reads every mapped weight page once at load, so the first reply is not interrupted by page faults from disk. Loading takes longer and memory pressure rises immediately.", "Reads every mapped weight page once at load, so the first reply is not interrupted by page faults from disk. Loading takes longer and memory pressure rises immediately.",
@@ -402,13 +402,13 @@ impl App {
.on_input(Message::PreferenceMtpMarginChanged), .on_input(Message::PreferenceMtpMarginChanged),
), ),
hint( hint(
checkbox(self.preference_draft.glm_mtp) toggle(self.preference_draft.glm_mtp)
.label("Enable integrated GLM MTP") .label("Enable integrated GLM MTP")
.on_toggle_maybe(glm_mtp_toggle), .on_toggle_maybe(glm_mtp_toggle),
"Uses the prediction head built into GLM 5.2 for speculative decoding, so no separate draft model is loaded. Available for GLM 5.2 only.", "Uses the prediction head built into GLM 5.2 for speculative decoding, so no separate draft model is loaded. Available for GLM 5.2 only.",
), ),
hint( hint(
checkbox(self.preference_draft.glm_mtp_timing) toggle(self.preference_draft.glm_mtp_timing)
.label("Log GLM MTP timing counters") .label("Log GLM MTP timing counters")
.on_toggle_maybe(glm_mtp_timing_toggle), .on_toggle_maybe(glm_mtp_timing_toggle),
"Records per-stage timings of the speculative path to the log, to show where the acceleration actually goes. A diagnostic aid that costs a little throughput.", "Records per-stage timings of the speculative path to the log, to show where the acceleration actually goes. A diagnostic aid that costs a little throughput.",
@@ -421,7 +421,7 @@ impl App {
dspark_confidence, dspark_confidence,
), ),
hint( hint(
checkbox(self.preference_draft.dspark_strict) toggle(self.preference_draft.dspark_strict)
.label("DSpark target-only decode") .label("DSpark target-only decode")
.on_toggle_maybe(dspark_strict_toggle), .on_toggle_maybe(dspark_strict_toggle),
"Lets the draft model only propose, never decide: every token is sampled by the full model. Gives up some of the speedup in exchange for output identical to non-speculative decoding.", "Lets the draft model only propose, never decide: every token is sampled by the full model. Gives up some of the speedup in exchange for output identical to non-speculative decoding.",
@@ -457,13 +457,13 @@ impl App {
Space::new().height(6), Space::new().height(6),
text("SSD STREAMING").size(11).color(muted_text()), text("SSD STREAMING").size(11).color(muted_text()),
hint( hint(
checkbox(self.preference_draft.ssd_streaming) toggle(self.preference_draft.ssd_streaming)
.label("Enable SSD-backed model streaming") .label("Enable SSD-backed model streaming")
.on_toggle(Message::PreferenceSsdChanged), .on_toggle(Message::PreferenceSsdChanged),
"Leaves the routed expert weights on disk and pages them in as they are needed, so a model larger than this machine's memory still runs. Every cache miss waits for the SSD; speculative support weights remain resident while target experts stream.", "Leaves the routed expert weights on disk and pages them in as they are needed, so a model larger than this machine's memory still runs. Every cache miss waits for the SSD; speculative support weights remain resident while target experts stream.",
), ),
hint( hint(
checkbox(self.preference_draft.ssd_streaming_cold) toggle(self.preference_draft.ssd_streaming_cold)
.label("Skip automatic expert preload") .label("Skip automatic expert preload")
.on_toggle(Message::PreferenceSsdColdChanged), .on_toggle(Message::PreferenceSsdColdChanged),
"Starts with an empty expert cache instead of reading the likely experts up front. The model is ready sooner and uses less memory, at the price of slow first replies.", "Starts with an empty expert cache instead of reading the likely experts up front. The model is ready sooner and uses less memory, at the price of slow first replies.",