bos-settings/frontend/src/lib/views/Appearance.svelte
Breadway 93c3b88b10 Migrate bos-settings from GTK4 to Tauri + Svelte 5
Ports every remaining view (Keybinds, the last one — dual Flat/MultiLayout
schema detection, per-action dedicated fields with a raw-JSON fallback for
anything not modeled) and removes the now-fully-dead GTK4 crate (root
Cargo.toml + src/), leaving src-tauri/ as the single Rust binary crate.

Also folds in the UX pass done alongside the migration: responsive
multi-column layout instead of a single centered column, chip/dropdown
pickers replacing free-typed fields (Wi-Fi networks, Bar style, keyboard
layout, tag lists), consistent pill-switch toggles and boxed-list card
styling across every page, and a sidebar scroll-chaining fix.
2026-07-22 19:23:02 +08:00

128 lines
4.7 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import ViewScaffold from "$lib/components/ViewScaffold.svelte";
import Group from "$lib/components/Group.svelte";
import SwitchField from "$lib/components/SwitchField.svelte";
import SelectField from "$lib/components/SelectField.svelte";
import NumberField from "$lib/components/NumberField.svelte";
import HyprColorField from "$lib/components/HyprColorField.svelte";
import Row from "$lib/components/Row.svelte";
import Hint from "$lib/components/Hint.svelte";
import SaveButton from "$lib/components/SaveButton.svelte";
// Common XKB layout codes. Not exhaustive — if the config holds
// something else, it's merged in below so it's never dropped from the
// dropdown.
const COMMON_KB_LAYOUTS = [
"us", "gb", "de", "fr", "es", "it", "pt", "nl", "se", "no", "dk", "fi",
"pl", "cz", "sk", "hu", "ro", "gr", "tr", "ru", "ua", "jp", "kr", "cn",
"br", "ca", "ch", "be", "at", "ie",
];
// Hyprland's follow_mouse variable (see the wiki's Variables page):
// 0 = cursor movement never changes focus; 1 = focus always follows the
// window under the cursor; 2 = focus follows the cursor, but clicking a
// window keeps keyboard focus there until the mouse moves again; 3 =
// cursor and keyboard focus are fully independent.
const FOLLOW_MOUSE_MODES: { value: number; label: string }[] = [
{ value: 0, label: "Off — click to focus" },
{ value: 1, label: "Follow mouse" },
{ value: 2, label: "Follow mouse, detach on click" },
{ value: 3, label: "Fully detached from keyboard focus" },
];
interface Appearance {
gaps_in: number;
gaps_out: number;
border_size: number;
active_border: string;
inactive_border: string;
layout: string;
resize_on_border: boolean;
rounding: number;
blur_enabled: boolean;
blur_size: number;
blur_passes: number;
shadow_enabled: boolean;
shadow_range: number;
shadow_render_power: number;
kb_layout: string;
follow_mouse: number;
natural_scroll: boolean;
}
let cfg = $state<Appearance | null>(null);
let kbLayoutOptions = $derived(
cfg && !COMMON_KB_LAYOUTS.includes(cfg.kb_layout) ? [...COMMON_KB_LAYOUTS, cfg.kb_layout] : COMMON_KB_LAYOUTS,
);
onMount(async () => {
cfg = await invoke<Appearance>("get_appearance");
});
async function save() {
await invoke("save_appearance", { appearance: cfg });
}
</script>
<ViewScaffold title="Appearance">
{#if cfg}
<Group title="Windows & borders" hint="Gaps, borders, and tiling — the same settings.json Hyprland reads at login.">
<NumberField label="Gaps between windows" bind:value={cfg.gaps_in} min={0} max={50} />
<NumberField label="Gaps around screen edge" bind:value={cfg.gaps_out} min={0} max={50} />
<NumberField label="Border width" bind:value={cfg.border_size} min={0} max={10} />
<HyprColorField label="Active border color" bind:value={cfg.active_border} />
<HyprColorField label="Inactive border color" bind:value={cfg.inactive_border} />
<SelectField label="Tiling layout" bind:value={cfg.layout} options={["dwindle", "master"]} />
<SwitchField label="Resize by dragging borders" bind:value={cfg.resize_on_border} />
</Group>
<Group title="Effects">
<NumberField label="Corner rounding" bind:value={cfg.rounding} min={0} max={30} />
<SwitchField label="Blur" bind:value={cfg.blur_enabled} />
<NumberField label="Blur size" bind:value={cfg.blur_size} min={0} max={20} />
<NumberField label="Blur passes" bind:value={cfg.blur_passes} min={1} max={5} />
<SwitchField label="Window shadows" bind:value={cfg.shadow_enabled} />
<NumberField label="Shadow range" bind:value={cfg.shadow_range} min={0} max={40} />
<NumberField label="Shadow render power" bind:value={cfg.shadow_render_power} min={1} max={4} />
</Group>
<Group title="Input">
<SelectField label="Keyboard layout" bind:value={cfg.kb_layout} options={kbLayoutOptions} />
<Row label="Focus-follows-mouse mode">
<select bind:value={cfg.follow_mouse}>
{#each FOLLOW_MOUSE_MODES as mode (mode.value)}
<option value={mode.value}>{mode.label}</option>
{/each}
</select>
</Row>
<SwitchField label="Natural scrolling (touchpad)" bind:value={cfg.natural_scroll} />
</Group>
<Hint text="Changes apply on next login or Hyprland reload — this saves settings.json, it doesn't reload Hyprland live." />
<SaveButton onSave={save} />
{/if}
</ViewScaffold>
<style>
select {
color-scheme: dark;
background-color: var(--bg);
color: var(--on-surface);
border: 1px solid transparent;
border-radius: var(--radius-secondary, 6px);
padding: var(--space-xs, 4px) var(--space-sm, 8px);
}
option {
background-color: var(--bg);
color: var(--on-surface);
}
select:focus {
outline: none;
border-color: var(--accent);
}
</style>