diff --git a/bin/omarchy-theme-set-templates b/bin/omarchy-theme-set-templates index ab89869a..ef0293bf 100755 --- a/bin/omarchy-theme-set-templates +++ b/bin/omarchy-theme-set-templates @@ -173,6 +173,25 @@ gradient_start_value() { fi } +shell_gradient_value() { + local spec index + + spec=$(resolve_theme_ref "$1" "${2:-}") + parse_gradient "$spec" + + if (( ${#GRADIENT_COLORS[@]} == 0 )); then + printf "%s" "$spec" + return + fi + + for index in "${!GRADIENT_COLORS[@]}"; do + (( index > 0 )) && printf " " + printf "%s" "${GRADIENT_COLORS[$index]}" + done + + [[ -n $GRADIENT_ANGLE ]] && printf " %sdeg" "$GRADIENT_ANGLE" +} + add_template_value() { local key="$1" local value="$2" @@ -247,6 +266,9 @@ add_gradient_function_value() { gradient_start) value=$(gradient_start_value "$key" "$fallback") ;; + shell_gradient) + value=$(shell_gradient_value "$key" "$fallback") + ;; *) return ;; @@ -264,7 +286,7 @@ add_gradient_function_values() { [[ -n ${seen[$token]:-} ]] && continue seen[$token]=1 add_gradient_function_value "$token" - done < <(grep -hEo '\{\{[[:space:]]*(hypr_gradient|gradient_start)[[:space:]]+[^}]+[[:space:]]*\}\}' "$tpl" 2>/dev/null || true) + done < <(grep -hEo '\{\{[[:space:]]*(hypr_gradient|gradient_start|shell_gradient)[[:space:]]+[^}]+[[:space:]]*\}\}' "$tpl" 2>/dev/null || true) done } diff --git a/default/themed/shell.toml.tpl b/default/themed/shell.toml.tpl index 30f84338..e506a418 100644 --- a/default/themed/shell.toml.tpl +++ b/default/themed/shell.toml.tpl @@ -1,6 +1,6 @@ # Omarchy shell surfaces. Colors derive from colors.toml; sizes and the # typographic scale come from the keys below. Themes can ship -# themes//shell.toml to override any individual key. +# themes//shell.toml to replace this generated file. [bar] # Alpha companions (where present) range from 0 (invisible) to 1 (opaque). @@ -20,15 +20,20 @@ size-vertical = 28 # Shared state tokens for interactive control chrome (buttons, dropdowns, # tab strips, etc). -# Normal: idle control chrome. +# Normal: idle control chrome. Border widths accept one CSS-style scalar/list: +# N, "Y X", "T X B", or "T R B L". Per-side keys like +# normal-border-width-left override the list. Each *-border accepts either a +# solid color or a Hyprland-style gradient, e.g. "rgba(...) rgba(...) 45deg". normal-color = "{{ foreground }}" normal-fill-alpha = 0.04 +normal-border = "{{ foreground }}" normal-border-width = 1 normal-border-alpha = 0.4 # Hover-cursor: mouse hover and the panel keyboard cursor. hover-cursor-color = "{{ foreground }}" hover-cursor-fill-alpha = 0.08 +hover-cursor-border = "{{ foreground }}" hover-cursor-border-width = 1 hover-cursor-border-alpha = 0.25 @@ -37,12 +42,14 @@ hover-cursor-border-alpha = 0.25 # — themes that want focus to stand out override these four lines. focus-color = "{{ foreground }}" focus-fill-alpha = 0.08 +focus-border = "{{ foreground }}" focus-border-width = 1 focus-border-alpha = 0.25 # Selected: persistent chosen/current state. selected-color = "{{ foreground }}" selected-fill-alpha = 0.18 +selected-border = "{{ foreground }}" selected-border-width = 0 selected-border-alpha = 1.0 @@ -107,11 +114,15 @@ base-size = 12 [popups] # Shared by every bar flyout (dropdowns, OSD, popup cards). +# Border accepts either a solid color or a Hyprland-style gradient. Border +# widths accept one CSS-style scalar/list: N, "Y X", "T X B", or "T R B L"; +# individual border-width-top/right/bottom/left keys override the list. background = "{{ background }}" background-alpha = 1.0 text = "{{ foreground }}" -border = "{{ accent }}" +border = "{{ shell_gradient hyprland_active_border accent }}" border-alpha = 1.0 +# border-width = 2 [tooltip] # Hover tooltips across the bar, panels, and buttons. background-alpha of @@ -119,18 +130,18 @@ border-alpha = 1.0 background = "{{ background }}" background-alpha = 0.97 text = "{{ foreground }}" -border = "{{ foreground }}" +border = "{{ shell_gradient hyprland_active_border foreground }}" border-alpha = 1.0 [notifications] background = "{{ background }}" background-alpha = 1.0 text = "{{ foreground }}" -# Conventionally matches the Hyprland active-window border. If a theme -# defines a gradient active border, the shell uses its first stop because -# QML Rectangle borders are flat colors. -border = "{{ gradient_start hyprland_active_border accent }}" +# Conventionally matches the Hyprland active-window border. Border accepts +# either a solid color or the full active-border gradient. +border = "{{ shell_gradient hyprland_active_border accent }}" border-alpha = 1.0 +# border-width = 2 countdown = "{{ accent }}" [launcher] @@ -141,14 +152,14 @@ countdown = "{{ accent }}" background = "{{ background }}" background-alpha = 0.95 text = "{{ foreground }}" -border = "{{ foreground }}" +border = "{{ shell_gradient hyprland_active_border foreground }}" border-alpha = 1.0 scrim = "{{ background }}" scrim-alpha = 0.5 selected-background = "{{ foreground }}" selected-background-alpha = 0.08 selected-text = "{{ accent }}" -selected-border = "{{ foreground }}" +selected-border = "{{ shell_gradient hyprland_active_border foreground }}" selected-border-alpha = 0.25 [menu] @@ -158,14 +169,14 @@ selected-border-alpha = 0.25 background = "{{ background }}" background-alpha = 1.0 text = "{{ foreground }}" -border = "{{ foreground }}" +border = "{{ shell_gradient hyprland_active_border foreground }}" border-alpha = 1.0 scrim = "{{ background }}" scrim-alpha = 0.5 selected-background = "{{ foreground }}" selected-background-alpha = 0.08 selected-text = "{{ accent }}" -selected-border = "{{ foreground }}" +selected-border = "{{ shell_gradient hyprland_active_border foreground }}" selected-border-alpha = 0.25 [polkit] @@ -178,7 +189,7 @@ background = "{{ background }}" background-alpha = 1.0 text = "{{ foreground }}" text-error = "{{ color1 }}" -border = "{{ accent }}" +border = "{{ shell_gradient hyprland_active_border accent }}" border-error = "{{ color1 }}" border-alpha = 1.0 scrim = "{{ background }}" @@ -196,8 +207,8 @@ background = "{{ background }}" background-alpha = 0.8 text = "{{ foreground }}" text-error = "{{ color1 }}" -border = "{{ foreground }}" -border-active = "{{ accent }}" +border = "{{ shell_gradient hyprland_active_border foreground }}" +border-active = "{{ shell_gradient hyprland_active_border accent }}" border-error = "{{ color1 }}" border-alpha = 1.0 # selection is the text-selection tint inside the input field. diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 02cf2c8e..55ed5481 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -126,10 +126,13 @@ customizes, `shell.json` is canonical — there is no deep-merge. ## Theme tokens +See [`theming.md`](theming.md) for the full theme/template workflow, +including generated `*.tpl` files, gradient helpers, and shell border syntax. + Themes ship colors in `themes//colors.toml` and surface roles + sizing in `themes//shell.toml`. Defaults are generated from `default/themed/shell.toml.tpl`; a theme may also drop a hand-written -`shell.toml` next to its `colors.toml` to override individual keys. +`shell.toml` next to its `colors.toml` to replace the generated file. The shell exposes these tokens to QML via two singletons in `qs.Commons`: @@ -143,29 +146,34 @@ The shell exposes these tokens to QML via two singletons in state tokens/helpers, spacing (`Style.spacing.*` / `Style.space(px)`), the type scale (`Style.font.*`), and bar dimensions (`Style.bar.sizeHorizontal` / `Style.bar.sizeVertical`). +- `Border` — border-spec helpers for QML surfaces. Use with + `BorderSurface` from `qs.Ui` when a border should honor shell theme + gradients or per-side widths. `Color.
.border` is only the + flat-color fallback for code that cannot render a real border. ### Interactive states `[controls]` standardizes reusable control chrome (buttons, dropdowns, tab strips, etc.) around four states: `normal`, `hover-cursor`, `focus`, -and `selected`. State colors accept palette roles (`foreground`, -`accent`, `urgent`, `background`) or hex strings; the default template -ships hex values. Fill/border alphas are applied to that state's color. +and `selected`. State colors and border tokens accept palette roles +(`foreground`, `accent`, `urgent`, `background`) or hex strings; border +values may also be gradients. Fill alpha applies to the state color; +border alpha applies to the state's border token. Surfaces like `[menu]`, `[launcher]`, and `[image-picker]` define their own `selected-*` tokens and do **not** inherit from `[controls]`. `[controls]` only governs the shared button/dropdown chrome. -| State | Color token | Fill alpha | Border width | Border alpha | -|-------|-------------|------------|--------------|--------------| -| Normal idle chrome | `normal-color` | `normal-fill-alpha` | `normal-border-width` | `normal-border-alpha` | -| Hover / keyboard cursor | `hover-cursor-color` | `hover-cursor-fill-alpha` | `hover-cursor-border-width` | `hover-cursor-border-alpha` | -| Qt activeFocus | `focus-color` | `focus-fill-alpha` | `focus-border-width` | `focus-border-alpha` | -| Persistent selected/current | `selected-color` | `selected-fill-alpha` | `selected-border-width` | `selected-border-alpha` | +| State | Color token | Fill alpha | Border token | Border width | Border alpha | +|-------|-------------|------------|--------------|--------------|--------------| +| Normal idle chrome | `normal-color` | `normal-fill-alpha` | `normal-border` | `normal-border-width` | `normal-border-alpha` | +| Hover / keyboard cursor | `hover-cursor-color` | `hover-cursor-fill-alpha` | `hover-cursor-border` | `hover-cursor-border-width` | `hover-cursor-border-alpha` | +| Qt activeFocus | `focus-color` | `focus-fill-alpha` | `focus-border` | `focus-border-width` | `focus-border-alpha` | +| Persistent selected/current | `selected-color` | `selected-fill-alpha` | `selected-border` | `selected-border-width` | `selected-border-alpha` | The template ships `focus-*` adjacent to `hover-cursor-*` with the same values so mouse hover, keyboard cursor, and tab focus read identically. -Themes that want focus to stand out override the four `focus-*` keys. +Themes that want focus to stand out override the `focus-*` keys. Border widths are the theme-level on/off switches for state borders; set a width to `0` to keep the fill while removing that state border. The @@ -178,6 +186,7 @@ default keeps selected borders off globally (`selected-border-width = hover-cursor-color = "accent" focus-color = "accent" selected-color = "foreground" +focus-border = "rgba(33ccffee) rgba(00ff99ee) 45deg" # Keep selected fills but remove selected-state borders. selected-border-width = 0 diff --git a/docs/theming.md b/docs/theming.md new file mode 100644 index 00000000..733ad73a --- /dev/null +++ b/docs/theming.md @@ -0,0 +1,296 @@ +# Omarchy theming + +Omarchy themes live under `themes//` in the source tree, with optional +user themes under `~/.config/omarchy/themes//`. A theme normally starts +with a `colors.toml`; Omarchy generates the rest of the theme files from +`default/themed/*.tpl` when `omarchy-theme-set ` runs. + +## Theme activation flow + +`omarchy-theme-set ` builds a clean staging directory at +`~/.config/omarchy/current/next-theme`: + +1. Copy the first-party theme from `themes//`. +2. Overlay any user theme files from `~/.config/omarchy/themes//`. +3. If needed, generate `colors.toml` from `alacritty.toml`. +4. Run `omarchy-theme-set-templates` to render templates into the staging + theme. +5. Move the staging theme into `~/.config/omarchy/current/theme` and notify the + running shell. + +Template rendering only happens when the staged theme has `colors.toml`. +Existing files are never overwritten by a template, so a hand-written +`themes//shell.toml` or `hyprland.lua` wins over +`default/themed/shell.toml.tpl` or `hyprland.lua.tpl`. + +User templates in `~/.config/omarchy/themed/*.tpl` are processed before the +built-in templates. If a user template has the same output filename as a +built-in template, the built-in output is skipped. + +## `colors.toml` + +`colors.toml` provides the palette keys used by templates. Common keys are: + +```toml +foreground = "#a9b1d6" +background = "#1a1b26" +accent = "#7aa2f7" +color1 = "#f7768e" +color4 = "#7aa2f7" +``` + +Any key can be referenced from a template with `{{ key }}`. The shell also +uses a few semantic palette keys directly: + +- `foreground` +- `background` +- `accent` — preferred when present; otherwise some places fall back to + `color4` +- `urgent` / `color1` + +## Template placeholders + +Templates are plain files ending in `.tpl`. `omarchy-theme-set-templates` +replaces placeholders with values from `colors.toml`. + +### Color placeholders + +For a color key such as `accent = "#7aa2f7"`: + +| Placeholder | Output | +|-------------|--------| +| `{{ accent }}` | `#7aa2f7` | +| `{{ accent_strip }}` | `7aa2f7` | +| `{{ accent_rgb }}` | `122,162,247` | + +### Color mixing + +`mix`, `mix_strip`, and `mix_rgb` blend two hex colors by a fraction or +percentage: + +```text +{{ mix background foreground 15% }} +{{ mix_strip background accent 0.35 }} +{{ mix_rgb color0 color7 50 }} +``` + +### Gradient helpers + +Some theme keys can be either a solid color or a Hyprland-style gradient: + +```toml +hyprland_active_border = "rgba(33ccffee) rgba(00ff99ee) 45deg" +``` + +Gradient helper placeholders understand those values: + +| Helper | Use | Example output | +|--------|-----|----------------| +| `{{ hypr_gradient hyprland_active_border accent }}` | Hyprland Lua config | `{ colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 }` | +| `{{ shell_gradient hyprland_active_border accent }}` | shell border tokens | `rgba(33ccffee) rgba(00ff99ee) 45deg` | +| `{{ gradient_start hyprland_active_border accent }}` | flat-color-only consumers | `#33ccff` | + +The second argument is a fallback. For example, +`{{ shell_gradient hyprland_active_border accent }}` means: use +`hyprland_active_border` if the theme defines it; otherwise use `accent`. +The helper does not choose the first color unless you use `gradient_start`. + +## `shell.toml` + +`shell.toml` contains shell surface roles, control states, spacing, typography, +and bar sizing. The default generated file comes from +`default/themed/shell.toml.tpl`. + +The running shell reads `shell.toml` into two QML singletons: + +- `Color` for palette and surface roles like `Color.menu.border`. +- `Style` for controls, spacing, font scale, corner radius, and bar sizing. + +### Borders + +Shell border tokens accept either a solid color or a gradient in the same key: + +```toml +[notifications] +border = "#7aa2f7" +``` + +or: + +```toml +[notifications] +border = "rgba(33ccffee) rgba(00ff99ee) 45deg" +``` + +Do not add a separate `border-gradient` key for new themes. The parser still +accepts `border-gradient` and `*-border-gradient` for compatibility with older +configs, but the canonical form is the border key itself. + +Border alphas apply to solid borders and to every gradient stop: + +```toml +[notifications] +border = "rgba(33ccffee) rgba(00ff99ee) 45deg" +border-alpha = 0.8 +``` + +If a color stop already includes alpha, the stop alpha and `border-alpha` are +combined. + +### Border widths + +Border widths accept CSS-style lists: + +```toml +border-width = 2 # all sides +border-width = "2 4" # top/bottom, right/left +border-width = "2 4 6" # top, right/left, bottom +border-width = "2 4 6 8" # top, right, bottom, left +``` + +Per-side keys override the list: + +```toml +[notifications] +border-width = 2 +border-width-left = 6 +``` + +That gives notifications a 2px border on the top, right, and bottom, and a 6px +left edge. + +State-specific borders follow the same pattern. A selected menu row can use a +different width from the card border: + +```toml +[menu] +selected-border = "accent" +selected-border-width = "1 1 1 4" +``` + +For state-specific surfaces such as lock and polkit, the token name prefixes +the width key: + +```toml +[lock] +border-active = "rgba(33ccffee) rgba(00ff99ee) 45deg" +border-active-width-left = 6 +``` + +### Control borders + +`[controls]` governs shared controls such as buttons, dropdowns, text fields, +toggles, and cursor rows. Each state has a fill color, optional border value, +border width, and border alpha: + +```toml +[controls] +normal-color = "#a9b1d6" +normal-border = "#a9b1d6" +normal-border-width = 1 +normal-border-alpha = 0.4 + +hover-cursor-color = "#a9b1d6" +hover-cursor-border = "#a9b1d6" +hover-cursor-border-width = 1 +hover-cursor-border-alpha = 0.25 +``` + +The `*-border` keys can also be gradients: + +```toml +[controls] +focus-border = "rgba(33ccffee) rgba(00ff99ee) 45deg" +focus-border-width = "2 2 2 4" +``` + +Set a border width to `0` to keep the fill but remove that state border. + +### Surface sections + +Common shell sections include: + +- `[bar]` +- `[controls]` +- `[popups]` +- `[tooltip]` +- `[notifications]` +- `[launcher]` +- `[menu]` +- `[polkit]` +- `[lock]` +- `[image-picker]` +- `[spacing]` +- `[font]` + +Clipboard and emojis inherit menu tokens. Popups are used by bar flyouts, +dropdowns, OSD, and popup cards. + +## QML border API + +Plugin and shell QML should use `BorderSurface` for theme-aware borders: + +```qml +import qs.Commons +import qs.Ui + +BorderSurface { + color: Color.popups.background + borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, 2) + padding: Style.spacing.popupPadding + + Item { + anchors.fill: parent + anchors.topMargin: parent.contentTopInset + anchors.rightMargin: parent.contentRightInset + anchors.bottomMargin: parent.contentBottomInset + anchors.leftMargin: parent.contentLeftInset + } +} +``` + +Use `Border.surfaceSpec(section, token, fallbackColor, fallbackWidth)` for +shell theme tokens, `Border.controlSpec(state, foreground, accent)` for shared +controls, and `Border.flat(color, width)` for a deliberate local border that +should not be overridden by the active theme. `Color.
.border` is the +flat first-stop color for consumers that cannot render full border specs. + +## Hyprland templates + +Hyprland theme output is generated from `default/themed/hyprland.lua.tpl`. +Use `hypr_gradient` for border values because Hyprland's Lua config wants a +Lua string for solid colors and a Lua table for gradients: + +```lua +local active_border_color = {{ hypr_gradient hyprland_active_border accent }} +``` + +For a solid fallback this renders: + +```lua +local active_border_color = "#7aa2f7" +``` + +For a gradient it renders: + +```lua +local active_border_color = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 } +``` + +## Adding or overriding theme files + +- Add palette values to `themes//colors.toml`. +- Prefer generated files when the theme can be expressed with templates. +- Add a hand-written file in `themes//` only when that theme needs to + override the generated output entirely. +- Add a new built-in template under `default/themed/.tpl` when every + theme should generate that file. +- Add a user-wide template under `~/.config/omarchy/themed/.tpl` when a + local customization should apply across themes. + +When changing templates or theme helpers, run focused tests such as: + +```bash +./test/cli +./test/shell +``` diff --git a/shell/Commons/Border.qml b/shell/Commons/Border.qml new file mode 100644 index 00000000..aba6eb85 --- /dev/null +++ b/shell/Commons/Border.qml @@ -0,0 +1,210 @@ +pragma Singleton +import QtQuick +import "BorderGeometry.js" as Geometry + +// Central border-spec factory for shell surfaces and controls. A spec carries +// color, optional gradient, and top/right/bottom/left widths so renderers can +// choose the cheap Rectangle path or the Shape-ring path without duplicating +// theme parsing logic. +QtObject { + id: root + + function none() { + return flat("transparent", 0) + } + + function flat(color, width) { + return { + color: color || "transparent", + widths: Geometry.parseWidthSpec(width, 0), + gradient: { colors: [], angle: 0, enabled: false }, + } + } + + function value(section, key) { + var v = Color.shellValues[section + "." + key] + return (v === undefined || v === null) ? "" : v + } + + function valueOr(section, keys) { + for (var i = 0; i < keys.length; i++) { + var v = value(section, keys[i]) + if (String(v).length > 0) return v + } + return "" + } + + function alpha(section, key, fallback) { + var raw = value(section, key) + if (String(raw).length === 0) return fallback + var n = Number(raw) + return isFinite(n) ? Geometry.clampAlpha(n) : fallback + } + + function cssColor(color, opacity) { + var a = opacity === undefined || opacity === null ? 1 : Geometry.clampAlpha(opacity) + if (color && typeof color === "object" && color.r !== undefined) { + if (typeof Qt !== "undefined" && Qt.rgba) { + return Qt.rgba(color.r, color.g, color.b, (color.a === undefined ? 1 : color.a) * a) + } + return "#" + + Geometry.padHex(color.r * 255) + + Geometry.padHex(color.g * 255) + + Geometry.padHex(color.b * 255) + + Geometry.padHex((color.a === undefined ? 1 : color.a) * a * 255) + } + + var s = String(color || "").replace(/^\s+|\s+$/g, "") + var role = s.toLowerCase() + if (role === "foreground" || role === "text") return cssColor(Color.foreground, a) + if (role === "accent") return cssColor(Color.accent, a) + if (role === "urgent") return cssColor(Color.urgent, a) + if (role === "background") return cssColor(Color.background, a) + if (role === "transparent") return "transparent" + return Geometry.canonicalColor(s, a) + } + + function resolvedGradient(raw, fallbackColor, opacity) { + var s = String(raw || "").replace(/^\s+|\s+$/g, "") + if (s.length === 0) return { colors: [], angle: 0, enabled: false } + + var parts = s.split(/\s+/) + var colors = [] + var angle = 0 + for (var i = 0; i < parts.length; i++) { + if (parts[i].match(/^-?\d+(?:\.\d+)?deg$/)) angle = Number(parts[i].replace(/deg$/, "")) + else colors.push(cssColor(parts[i], opacity)) + } + if (colors.length === 0) colors.push(cssColor(fallbackColor, opacity)) + return { colors: colors, angle: angle, enabled: colors.length > 1 } + } + + function sameColor(a, b) { + if (typeof Qt === "undefined" || !Qt.color) return String(a) === String(b) + var ca = typeof a === "string" ? Qt.color(a) : a + var cb = typeof b === "string" ? Qt.color(b) : b + if (!ca || !cb || ca.r === undefined || cb.r === undefined) return String(a) === String(b) + return Math.round(ca.r * 255) === Math.round(cb.r * 255) + && Math.round(ca.g * 255) === Math.round(cb.g * 255) + && Math.round(ca.b * 255) === Math.round(cb.b * 255) + && Math.round((ca.a === undefined ? 1 : ca.a) * 255) === Math.round((cb.a === undefined ? 1 : cb.a) * 255) + } + + function localOrSurfaceSpec(section, token, localColor, defaultColor, fallbackWidth, alphaKey) { + if (!sameColor(localColor, defaultColor)) return flat(localColor, fallbackWidth) + return surfaceSpec(section, token, localColor, fallbackWidth, alphaKey) + } + + function surfaceWidths(section, token, fallbackWidth) { + var base = valueOr(section, token === "border" ? ["border-width"] : [token + "-width", "border-width"]) + var widths = Geometry.parseWidthSpec(base, fallbackWidth) + return Geometry.withSideOverrides( + widths, + valueOr(section, token === "border" ? ["border-width-top"] : [token + "-width-top", "border-width-top"]), + valueOr(section, token === "border" ? ["border-width-right"] : [token + "-width-right", "border-width-right"]), + valueOr(section, token === "border" ? ["border-width-bottom"] : [token + "-width-bottom", "border-width-bottom"]), + valueOr(section, token === "border" ? ["border-width-left"] : [token + "-width-left", "border-width-left"]) + ) + } + + function borderValue(raw, fallbackColor, opacity, legacyGradientRaw) { + var fallback = cssColor(fallbackColor, opacity) + var primaryRaw = String(raw || "").replace(/^\s+|\s+$/g, "") + var primary = resolvedGradient(primaryRaw.length > 0 ? primaryRaw : fallbackColor, fallbackColor, opacity) + var color = primary.colors.length > 0 ? primary.colors[0] : fallback + var gradient = primary.enabled ? primary : { colors: [], angle: 0, enabled: false } + + // Backward compatibility for existing configs written during the separate + // border-gradient experiment. New configs put solid colors and gradients in + // the same border token. + if (!gradient.enabled && String(legacyGradientRaw || "").replace(/^\s+|\s+$/g, "").length > 0) { + var legacy = resolvedGradient(legacyGradientRaw, color, opacity) + if (legacy.enabled) gradient = legacy + } + + return { color: color, gradient: gradient } + } + + function surfaceSpec(section, token, fallbackColor, fallbackWidth, alphaKey) { + var opacity = alpha(section, alphaKey || token + "-alpha", 1.0) + var legacyGradientRaw = valueOr(section, token === "border" ? ["border-gradient"] : [token + "-gradient", "border-gradient"]) + var resolved = borderValue(value(section, token), fallbackColor, opacity, legacyGradientRaw) + + return { + color: resolved.color, + widths: surfaceWidths(section, token, fallbackWidth), + gradient: resolved.gradient, + } + } + + function controlPrefix(state) { + if (state === "hover" || state === "hot") return "hover-cursor" + return state || "normal" + } + + function controlColor(prefix, foreground, accent, urgent) { + if (prefix === "focus") return Style.focusStateColor(foreground, accent, urgent) + if (prefix === "hover-cursor") return Style.hoverStateColor(foreground, accent, urgent) + if (prefix === "selected") return Style.selectedStateColor(foreground, accent, urgent) + return Style.normalStateColor(foreground, accent, urgent) + } + + function controlAlpha(prefix) { + if (prefix === "focus") return Style.focusBorderAlpha + if (prefix === "hover-cursor") return Style.hoverBorderAlpha + if (prefix === "selected") return Style.selectedBorderAlpha + return Style.normalBorderAlpha + } + + function controlFallbackWidth(prefix) { + if (prefix === "focus") return Style.focusBorderWidth + if (prefix === "hover-cursor") return Style.hoverBorderWidth + if (prefix === "selected") return Style.selectedBorderWidth + return Style.normalBorderWidth + } + + function controlWidths(state) { + var prefix = controlPrefix(state) + var fallbackWidth = controlFallbackWidth(prefix) + var base = Style.styleOverrides[prefix + "-border-width"] + var widths = Geometry.parseWidthSpec(base, fallbackWidth) + return Geometry.withSideOverrides( + widths, + Style.styleOverrides[prefix + "-border-width-top"], + Style.styleOverrides[prefix + "-border-width-right"], + Style.styleOverrides[prefix + "-border-width-bottom"], + Style.styleOverrides[prefix + "-border-width-left"] + ) + } + + function controlHasWidth(state) { + return Geometry.maxWidth(controlWidths(state)) > 0 + } + + function controlSpec(state, foreground, accent, urgent) { + var prefix = controlPrefix(state) + var resolved = borderValue( + Style.styleOverrides[prefix + "-border"], + controlColor(prefix, foreground, accent, urgent), + controlAlpha(prefix), + Style.styleOverrides[prefix + "-border-gradient"] + ) + + return { color: resolved.color, widths: controlWidths(prefix), gradient: resolved.gradient } + } + + function withWidth(spec, width) { + if (!spec) return flat("transparent", 0) + return { color: spec.color, gradient: spec.gradient, widths: Geometry.parseWidthSpec(width, 0) } + } + + function isNone(spec) { return !spec || Geometry.maxWidth(spec.widths) <= 0 } + function needsOverlay(spec) { return Geometry.needsOverlay(spec) } + function canUseNative(spec) { return Geometry.canUseNative(spec) } + function top(spec) { return spec && spec.widths ? spec.widths.top : 0 } + function right(spec) { return spec && spec.widths ? spec.widths.right : 0 } + function bottom(spec) { return spec && spec.widths ? spec.widths.bottom : 0 } + function left(spec) { return spec && spec.widths ? spec.widths.left : 0 } + function uniformWidth(spec) { return spec && spec.widths ? spec.widths.top : 0 } + function color(spec) { return spec ? spec.color : "transparent" } +} diff --git a/shell/Commons/BorderGeometry.js b/shell/Commons/BorderGeometry.js new file mode 100644 index 00000000..38b372cc --- /dev/null +++ b/shell/Commons/BorderGeometry.js @@ -0,0 +1,257 @@ +.pragma library + +function clamp(value, min, max) { + var n = Number(value) + if (!isFinite(n)) return min + return Math.max(min, Math.min(max, n)) +} + +function clampAlpha(value) { + return clamp(value, 0, 1) +} + +function padHex(value) { + var n = clamp(Math.round(Number(value)), 0, 255) + var h = n.toString(16) + return h.length < 2 ? "0" + h : h +} + +function qmlHexColor(rgb, alphaByte) { + var rgbPart = String(rgb || "").replace(/^#/, "") + var a = clamp(Math.round(Number(alphaByte)), 0, 255) + + if (typeof Qt !== "undefined" && Qt.rgba && rgbPart.length >= 6) { + return Qt.rgba( + parseInt(rgbPart.substring(0, 2), 16) / 255, + parseInt(rgbPart.substring(2, 4), 16) / 255, + parseInt(rgbPart.substring(4, 6), 16) / 255, + a / 255 + ) + } + + var aHex = padHex(a) + return aHex.toLowerCase() === "ff" ? "#" + rgbPart : "#" + rgbPart + aHex +} + +function canonicalColor(value, alpha) { + var a = alpha === undefined || alpha === null ? 1 : clampAlpha(alpha) + var s = String(value || "").replace(/^\s+|\s+$/g, "") + var m + + m = s.match(/^#([0-9A-Fa-f]{3})$/) + if (m) { + var sh = m[1] + return qmlHexColor( + sh.charAt(0) + sh.charAt(0) + + sh.charAt(1) + sh.charAt(1) + + sh.charAt(2) + sh.charAt(2), + a * 255 + ) + } + + m = s.match(/^#([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/) + if (m) { + var colorAlpha = m[2] ? parseInt(m[2], 16) / 255 : 1 + return qmlHexColor(m[1], colorAlpha * a * 255) + } + + m = s.match(/^[Rr][Gg][Bb]\(([0-9A-Fa-f]{6})\)$/) + if (m) return qmlHexColor(m[1], a * 255) + + m = s.match(/^[Rr][Gg][Bb][Aa]\(([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})\)$/) + if (m) return qmlHexColor(m[1], (parseInt(m[2], 16) / 255) * a * 255) + + m = s.match(/^[Rr][Gg][Bb]\(([0-9]+),([0-9]+),([0-9]+)\)$/) + if (m) return qmlHexColor(padHex(m[1]) + padHex(m[2]) + padHex(m[3]), a * 255) + + m = s.match(/^[Rr][Gg][Bb][Aa]\(([0-9]+),([0-9]+),([0-9]+),([0-9.]+)\)$/) + if (m) return qmlHexColor(padHex(m[1]) + padHex(m[2]) + padHex(m[3]), clampAlpha(m[4]) * a * 255) + + m = s.match(/^0x([0-9A-Fa-f]{2})([0-9A-Fa-f]{6})$/) + if (m) return qmlHexColor(m[2], (parseInt(m[1], 16) / 255) * a * 255) + + return s +} + +function parseWidthSpec(value, fallback) { + var fb = Number(fallback) + if (!isFinite(fb) || fb < 0) fb = 0 + + if (value === undefined || value === null || value === "") { + return { top: fb, right: fb, bottom: fb, left: fb } + } + + var parts = String(value).match(/-?\d+(?:\.\d+)?/g) || [] + var nums = [] + for (var i = 0; i < parts.length && i < 4; i++) { + var n = Number(parts[i]) + nums.push(isFinite(n) && n > 0 ? n : 0) + } + + if (nums.length === 0) nums = [fb] + if (nums.length === 1) return { top: nums[0], right: nums[0], bottom: nums[0], left: nums[0] } + if (nums.length === 2) return { top: nums[0], right: nums[1], bottom: nums[0], left: nums[1] } + if (nums.length === 3) return { top: nums[0], right: nums[1], bottom: nums[2], left: nums[1] } + return { top: nums[0], right: nums[1], bottom: nums[2], left: nums[3] } +} + +function withSideOverrides(widths, top, right, bottom, left) { + var out = { + top: Number(widths && widths.top) || 0, + right: Number(widths && widths.right) || 0, + bottom: Number(widths && widths.bottom) || 0, + left: Number(widths && widths.left) || 0, + } + if (top !== undefined && top !== null && top !== "") out.top = Math.max(0, Number(top) || 0) + if (right !== undefined && right !== null && right !== "") out.right = Math.max(0, Number(right) || 0) + if (bottom !== undefined && bottom !== null && bottom !== "") out.bottom = Math.max(0, Number(bottom) || 0) + if (left !== undefined && left !== null && left !== "") out.left = Math.max(0, Number(left) || 0) + return out +} + +function parseGradientSpec(value, fallbackColor, alpha) { + var s = String(value || "").replace(/^\s+|\s+$/g, "") + var colors = [] + var angle = 0 + var parts = s.length > 0 ? s.split(/\s+/) : [] + + for (var i = 0; i < parts.length; i++) { + var part = parts[i] + var angleMatch = part.match(/^(-?\d+(?:\.\d+)?)deg$/) + if (angleMatch) angle = Number(angleMatch[1]) + else colors.push(canonicalColor(part, alpha)) + } + + if (colors.length === 0 && fallbackColor !== undefined && fallbackColor !== null) + colors.push(canonicalColor(fallbackColor, alpha)) + + return { + colors: colors, + angle: isFinite(angle) ? angle : 0, + enabled: colors.length > 1, + } +} + +function isUniform(widths) { + if (!widths) return true + return widths.top === widths.right && widths.top === widths.bottom && widths.top === widths.left +} + +function maxWidth(widths) { + if (!widths) return 0 + return Math.max(widths.top || 0, widths.right || 0, widths.bottom || 0, widths.left || 0) +} + +function needsOverlay(spec) { + if (!spec) return false + if (maxWidth(spec.widths) <= 0) return false + return !!(spec.gradient && spec.gradient.enabled) || !isUniform(spec.widths) +} + +function canUseNative(spec) { + return !!spec && maxWidth(spec.widths) > 0 && !needsOverlay(spec) +} + +function normalizeRadii(w, h, r) { + var tl = { rx: Math.max(0, Number(r.tlrx) || 0), ry: Math.max(0, Number(r.tlry) || 0) } + var tr = { rx: Math.max(0, Number(r.trrx) || 0), ry: Math.max(0, Number(r.trry) || 0) } + var br = { rx: Math.max(0, Number(r.brrx) || 0), ry: Math.max(0, Number(r.brry) || 0) } + var bl = { rx: Math.max(0, Number(r.blrx) || 0), ry: Math.max(0, Number(r.blry) || 0) } + var scale = 1 + + if (tl.rx + tr.rx > w && tl.rx + tr.rx > 0) scale = Math.min(scale, w / (tl.rx + tr.rx)) + if (bl.rx + br.rx > w && bl.rx + br.rx > 0) scale = Math.min(scale, w / (bl.rx + br.rx)) + if (tl.ry + bl.ry > h && tl.ry + bl.ry > 0) scale = Math.min(scale, h / (tl.ry + bl.ry)) + if (tr.ry + br.ry > h && tr.ry + br.ry > 0) scale = Math.min(scale, h / (tr.ry + br.ry)) + + if (scale < 1) { + tl.rx *= scale; tr.rx *= scale; br.rx *= scale; bl.rx *= scale + tl.ry *= scale; tr.ry *= scale; br.ry *= scale; bl.ry *= scale + } + + return { tl: tl, tr: tr, br: br, bl: bl } +} + +function roundedRectPath(x, y, w, h, radii) { + if (w <= 0 || h <= 0) return "" + var r = normalizeRadii(w, h, radii) + var right = x + w + var bottom = y + h + var p = [] + + p.push("M", x + r.tl.rx, y) + p.push("H", right - r.tr.rx) + if (r.tr.rx > 0 || r.tr.ry > 0) p.push("A", r.tr.rx, r.tr.ry, 0, 0, 1, right, y + r.tr.ry) + p.push("V", bottom - r.br.ry) + if (r.br.rx > 0 || r.br.ry > 0) p.push("A", r.br.rx, r.br.ry, 0, 0, 1, right - r.br.rx, bottom) + p.push("H", x + r.bl.rx) + if (r.bl.rx > 0 || r.bl.ry > 0) p.push("A", r.bl.rx, r.bl.ry, 0, 0, 1, x, bottom - r.bl.ry) + p.push("V", y + r.tl.ry) + if (r.tl.rx > 0 || r.tl.ry > 0) p.push("A", r.tl.rx, r.tl.ry, 0, 0, 1, x + r.tl.rx, y) + p.push("Z") + + return p.join(" ") +} + +function ringPath(w, h, radius, widths) { + w = Math.max(0, Number(w) || 0) + h = Math.max(0, Number(h) || 0) + radius = Math.max(0, Number(radius) || 0) + widths = widths || { top: 0, right: 0, bottom: 0, left: 0 } + + var outer = roundedRectPath(0, 0, w, h, { + tlrx: radius, tlry: radius, + trrx: radius, trry: radius, + brrx: radius, brry: radius, + blrx: radius, blry: radius, + }) + + var ix = widths.left || 0 + var iy = widths.top || 0 + var iw = w - ix - (widths.right || 0) + var ih = h - iy - (widths.bottom || 0) + if (iw <= 0 || ih <= 0) return outer + + var inner = roundedRectPath(ix, iy, iw, ih, { + tlrx: Math.max(0, radius - (widths.left || 0)), + tlry: Math.max(0, radius - (widths.top || 0)), + trrx: Math.max(0, radius - (widths.right || 0)), + trry: Math.max(0, radius - (widths.top || 0)), + brrx: Math.max(0, radius - (widths.right || 0)), + brry: Math.max(0, radius - (widths.bottom || 0)), + blrx: Math.max(0, radius - (widths.left || 0)), + blry: Math.max(0, radius - (widths.bottom || 0)), + }) + + return outer + " " + inner +} + +function gradientEndpoints(w, h, angle) { + w = Math.max(1, Number(w) || 1) + h = Math.max(1, Number(h) || 1) + var rad = (Number(angle) || 0) * Math.PI / 180 + var dx = Math.cos(rad) + var dy = Math.sin(rad) + var len = (Math.abs(w * dx) + Math.abs(h * dy)) / 2 + var cx = w / 2 + var cy = h / 2 + return { + x1: cx - dx * len, + y1: cy - dy * len, + x2: cx + dx * len, + y2: cy + dy * len, + } +} + +function stopColor(colors, index) { + if (!colors || colors.length === 0) return "transparent" + if (index < colors.length) return colors[index] + return colors[colors.length - 1] +} + +function stopPosition(colors, index) { + var count = colors ? colors.length : 0 + if (count <= 1) return index === 0 ? 0 : 1 + if (index >= count) return 1 + return index / (count - 1) +} diff --git a/shell/Commons/Color.qml b/shell/Commons/Color.qml index 8df6e5b0..08104180 100644 --- a/shell/Commons/Color.qml +++ b/shell/Commons/Color.qml @@ -2,11 +2,12 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io +import "BorderGeometry.js" as Geometry // Color surfaces for the shell. Foundational palette (foreground, background, // accent, urgent) comes from theme/colors.toml. Per-surface roles come from // theme/shell.toml — generated per theme from default/themed/shell.toml.tpl, -// or shipped directly by a theme to override individual keys. Surfaces that +// or shipped directly by a theme to replace the generated file. Surfaces that // don't appear in shell.toml fall back to the foundational palette. QtObject { id: root @@ -34,9 +35,32 @@ QtObject { return Util.clampAlpha(n) } - // Compose a color from a base-color key and its `-alpha` companion. + function firstColorToken(value) { + var parts = String(value || "").replace(/^\s+|\s+$/g, "").split(/\s+/) + for (var i = 0; i < parts.length; i++) { + if (!parts[i].match(/^-?\d+(?:\.\d+)?deg$/)) return parts[i] + } + return value + } + + function flatColor(value, fallback) { + var token = firstColorToken(value) + var role = String(token || "").replace(/^\s+|\s+$/g, "").toLowerCase() + if (role === "foreground" || role === "text") return root.foreground + if (role === "accent") return root.accent + if (role === "urgent") return root.urgent + if (role === "background") return root.background + if (role === "transparent") return Qt.rgba(0, 0, 0, 0) + + var color = Geometry.canonicalColor(token, 1) + if (typeof color === "string" && color === token && token.charAt(0) !== "#") return fallback + return color + } + + // Compose a color from a base-color key and its `-alpha` companion. If the + // base token is a gradient, color-only consumers use the first stop. function composed(colorKey, alphaKey, colorFallback, alphaFallback) { - return Util.alpha(pick(colorKey, colorFallback), pickAlpha(alphaKey, alphaFallback)) + return Util.alpha(flatColor(pick(colorKey, colorFallback), colorFallback), pickAlpha(alphaKey, alphaFallback)) } readonly property QtObject bar: QtObject { @@ -130,8 +154,9 @@ QtObject { // Single TOML walker for shell.toml. Both Color (surface roles) and Style // (typography, spacing, bar, control states) consume the resulting dict. - // Accepts quoted strings and bare numeric values; tolerates inline comments. - // Numbers are kept as strings here — readers coerce when they pull a value. + // Accepts quoted strings, bare numeric values, bare width lists, and bare + // role names; tolerates inline comments. Numbers are kept as strings here — + // readers coerce when they pull a value. function loadShell(raw) { var parsed = {} var text = String(raw || "") @@ -145,8 +170,9 @@ QtObject { if (sectionMatch) { section = sectionMatch[1]; continue } var stringKv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*["']([^"']+)["']\s*(#.*)?$/) var numKv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+(?:\.\d+)?)\s*(#.*)?$/) + var widthKv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+(?:\.\d+)?(?:\s+-?\d+(?:\.\d+)?){1,3})\s*(#.*)?$/) var bareKv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*([A-Za-z][A-Za-z0-9_-]*)\s*(#.*)?$/) - var kv = stringKv || numKv || bareKv + var kv = stringKv || numKv || widthKv || bareKv if (!kv || !section) continue parsed[section + "." + kv[1]] = kv[2] } diff --git a/shell/Commons/Style.qml b/shell/Commons/Style.qml index 789b070e..88aa6a90 100644 --- a/shell/Commons/Style.qml +++ b/shell/Commons/Style.qml @@ -414,8 +414,9 @@ QtObject { if (key === "scale") nextSpacingScale = fval else spacingOut[key] = fval } - } else if (section === "controls") { + } else if (section === "controls" || section === "style") { // Strings are passed through; styleRawNum/styleString coerce on read. + // [style] is the legacy name for [controls]. styleOut[key] = raw } } diff --git a/shell/Commons/qmldir b/shell/Commons/qmldir index e6145b96..4bfd0b78 100644 --- a/shell/Commons/qmldir +++ b/shell/Commons/qmldir @@ -1,4 +1,5 @@ module qs.Commons +singleton Border 1.0 Border.qml singleton Color 1.0 Color.qml singleton Style 1.0 Style.qml singleton Util 1.0 Util.qml diff --git a/shell/Ui/BorderOverlay.qml b/shell/Ui/BorderOverlay.qml new file mode 100644 index 00000000..4fd0fb6e --- /dev/null +++ b/shell/Ui/BorderOverlay.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons +import "../Commons/BorderGeometry.js" as Geometry + +// Visual-only border renderer. It draws a filled rounded ring so borders can +// have gradients and independent top/right/bottom/left widths. Flat uniform +// borders should stay on Rectangle.border; this component is the fallback for +// cases Rectangle cannot represent. +Item { + id: root + + property var borderSpec: Border.none() + property real radius: 0 + + readonly property var _widths: borderSpec && borderSpec.widths ? borderSpec.widths : Geometry.parseWidthSpec(0, 0) + readonly property bool hasBorder: Geometry.maxWidth(_widths) > 0 + readonly property var _gradient: borderSpec && borderSpec.gradient ? borderSpec.gradient : ({ colors: [], angle: 0, enabled: false }) + readonly property var _colors: _gradient.enabled ? _gradient.colors : [Border.color(borderSpec), Border.color(borderSpec)] + readonly property var _endpoints: Geometry.gradientEndpoints(width, height, _gradient.angle || 0) + readonly property string _path: Geometry.ringPath(width, height, radius, _widths) + + visible: hasBorder && width > 0 && height > 0 + anchors.fill: parent + z: 100000 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillRule: ShapePath.OddEvenFill + strokeWidth: 0 + fillGradient: LinearGradient { + x1: root._endpoints.x1 + y1: root._endpoints.y1 + x2: root._endpoints.x2 + y2: root._endpoints.y2 + + GradientStop { position: Geometry.stopPosition(root._colors, 0); color: Geometry.stopColor(root._colors, 0) } + GradientStop { position: Geometry.stopPosition(root._colors, 1); color: Geometry.stopColor(root._colors, 1) } + GradientStop { position: Geometry.stopPosition(root._colors, 2); color: Geometry.stopColor(root._colors, 2) } + GradientStop { position: Geometry.stopPosition(root._colors, 3); color: Geometry.stopColor(root._colors, 3) } + GradientStop { position: Geometry.stopPosition(root._colors, 4); color: Geometry.stopColor(root._colors, 4) } + GradientStop { position: Geometry.stopPosition(root._colors, 5); color: Geometry.stopColor(root._colors, 5) } + GradientStop { position: Geometry.stopPosition(root._colors, 6); color: Geometry.stopColor(root._colors, 6) } + GradientStop { position: Geometry.stopPosition(root._colors, 7); color: Geometry.stopColor(root._colors, 7) } + GradientStop { position: Geometry.stopPosition(root._colors, 8); color: Geometry.stopColor(root._colors, 8) } + GradientStop { position: Geometry.stopPosition(root._colors, 9); color: Geometry.stopColor(root._colors, 9) } + } + + PathSvg { path: root._path } + } + } +} diff --git a/shell/Ui/BorderSurface.qml b/shell/Ui/BorderSurface.qml new file mode 100644 index 00000000..50714e52 --- /dev/null +++ b/shell/Ui/BorderSurface.qml @@ -0,0 +1,40 @@ +import QtQuick +import qs.Commons + +// Rectangle-compatible surface with Omarchy border specs. Uses native +// Rectangle.border for cheap flat/uniform borders and BorderOverlay for +// gradients or per-side widths. +Rectangle { + id: root + + property var borderSpec: Border.none() + property real padding: 0 + property real topPadding: padding + property real rightPadding: padding + property real bottomPadding: padding + property real leftPadding: padding + + readonly property real borderTop: Border.top(borderSpec) + readonly property real borderRight: Border.right(borderSpec) + readonly property real borderBottom: Border.bottom(borderSpec) + readonly property real borderLeft: Border.left(borderSpec) + readonly property real contentTopInset: borderTop + topPadding + readonly property real contentRightInset: borderRight + rightPadding + readonly property real contentBottomInset: borderBottom + bottomPadding + readonly property real contentLeftInset: borderLeft + leftPadding + readonly property bool usesOverlayBorder: Border.needsOverlay(borderSpec) + + border.color: Border.canUseNative(borderSpec) ? Border.color(borderSpec) : "transparent" + border.width: Border.canUseNative(borderSpec) ? Border.uniformWidth(borderSpec) : 0 + + Loader { + anchors.fill: parent + active: root.usesOverlayBorder + + sourceComponent: BorderOverlay { + anchors.fill: parent + radius: root.radius + borderSpec: root.borderSpec + } + } +} diff --git a/shell/Ui/Button.qml b/shell/Ui/Button.qml index e870791b..c88521f2 100644 --- a/shell/Ui/Button.qml +++ b/shell/Ui/Button.qml @@ -17,7 +17,7 @@ import qs.Commons // // Emits `hovered(bool)` so panels with their own keyboard cursor model // can update state on mouse enter/leave. -Rectangle { +BorderSurface { id: root property string text: "" @@ -46,6 +46,11 @@ Rectangle { property real verticalPadding: Style.spacing.controlPaddingY property bool leftAlign: false + leftPadding: horizontalPadding + rightPadding: horizontalPadding + topPadding: verticalPadding + bottomPadding: verticalPadding + // Tooltip palette. Auto-rendered if tooltipText is set. Defaults pull // from [tooltip] in shell.toml; override per-instance only when a button // intentionally wants a tooltip that diverges from the theme. @@ -62,13 +67,21 @@ Rectangle { Keys.onEnterPressed: if (focusable) root.clicked() Keys.onSpacePressed: if (focusable) root.clicked() - implicitWidth: row.implicitWidth + horizontalPadding * 2 - implicitHeight: row.implicitHeight + verticalPadding * 2 + implicitWidth: row.implicitWidth + horizontalPadding * 2 + borderLeft + borderRight + implicitHeight: row.implicitHeight + verticalPadding * 2 + borderTop + borderBottom radius: Style.cornerRadius readonly property bool hot: mouseArea.containsMouse || hasCursor readonly property bool _showFocusRing: focusable && activeFocus readonly property color _selectedColor: Style.selectedStateColor(root.foreground, root.accent) + readonly property var _tooltipBorderSpec: Border.localOrSurfaceSpec("tooltip", "border", root.tooltipBorder, Color.tooltip.border, Math.max(1, Style.normalBorderWidth)) + readonly property var _selectedBorderSpec: Border.controlSpec("selected", root.foreground, root.accent) + readonly property var _normalBorderSpec: Border.controlSpec("normal", root.foreground, root.accent) + readonly property var _borderSpec: _showFocusRing ? Border.controlSpec("focus", root.foreground, root.accent) + : hot ? Border.controlSpec("hover-cursor", root.foreground, root.accent) + : selected ? (Border.controlHasWidth("selected") ? _selectedBorderSpec : (bordered ? _normalBorderSpec : Border.none())) + : bordered ? _normalBorderSpec + : Border.none() color: mouseArea.pressed ? Style.pressedFillFor(root.foreground, root.accent) : _showFocusRing ? Style.focusFillFor(root.foreground, root.accent) @@ -84,17 +97,7 @@ Rectangle { // default for plain buttons; explicitly bordered buttons keep their // normal border when selected unless selected-border-width opts in to a // dedicated selected border. - border.color: _showFocusRing ? Style.focusBorderFor(root.foreground, root.accent) - : hot ? Style.hoverBorderFor(root.foreground, root.accent) - : selected ? (Style.selectedBorderWidth > 0 ? Style.selectedBorderFor(root.foreground, root.accent) : Style.normalBorderFor(root.foreground, root.accent)) - : bordered ? Style.normalBorderFor(root.foreground, root.accent) - : "transparent" - - border.width: _showFocusRing ? Style.focusBorderWidth - : hot ? Style.hoverBorderWidth - : selected ? (Style.selectedBorderWidth > 0 ? Style.selectedBorderWidth : (bordered ? Style.normalBorderWidth : 0)) - : bordered ? Style.normalBorderWidth - : 0 + borderSpec: _borderSpec Behavior on color { ColorAnimation { duration: 120 } } @@ -103,10 +106,9 @@ Rectangle { text: root.tooltipText delay: 400 padding: 0 - background: Rectangle { + background: BorderSurface { color: root.tooltipBackground - border.color: root.tooltipBorder - border.width: Math.max(1, Style.normalBorderWidth) + borderSpec: root._tooltipBorderSpec radius: 0 } contentItem: Text { @@ -114,10 +116,10 @@ Rectangle { color: root.tooltipForeground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall - leftPadding: Style.spacing.controlPaddingX - rightPadding: Style.spacing.controlPaddingX - topPadding: Style.spacing.controlPaddingY - bottomPadding: Style.spacing.controlPaddingY + leftPadding: Border.left(root._tooltipBorderSpec) + Style.spacing.controlPaddingX + rightPadding: Border.right(root._tooltipBorderSpec) + Style.spacing.controlPaddingX + topPadding: Border.top(root._tooltipBorderSpec) + Style.spacing.controlPaddingY + bottomPadding: Border.bottom(root._tooltipBorderSpec) + Style.spacing.controlPaddingY } } @@ -125,7 +127,7 @@ Rectangle { id: row anchors.verticalCenter: parent.verticalCenter anchors.left: root.leftAlign ? parent.left : undefined - anchors.leftMargin: root.leftAlign ? root.horizontalPadding : 0 + anchors.leftMargin: root.leftAlign ? root.contentLeftInset : 0 anchors.horizontalCenter: root.leftAlign ? undefined : parent.horizontalCenter spacing: Style.spacing.controlGap diff --git a/shell/Ui/ConfirmDialog.qml b/shell/Ui/ConfirmDialog.qml index 21554331..45d8da18 100644 --- a/shell/Ui/ConfirmDialog.qml +++ b/shell/Ui/ConfirmDialog.qml @@ -46,20 +46,24 @@ Item { MouseArea { anchors.fill: parent; onClicked: root.canceled() } - Rectangle { + BorderSurface { + id: card width: Math.min(parent.width - Style.space(96), Style.space(370)) height: Style.space(132) anchors.centerIn: parent color: root.background - border.color: root.selectedText - border.width: Style.normalBorderWidth + borderSpec: Border.flat(root.selectedText, Style.normalBorderWidth) + padding: Style.space(18) radius: root.cornerRadius MouseArea { anchors.fill: parent; onClicked: {} } Item { anchors.fill: parent - anchors.margins: Style.space(18) + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset Text { anchors.left: parent.left @@ -80,7 +84,7 @@ Item { Repeater { model: [root.cancelText, root.confirmText] - Rectangle { + BorderSurface { required property int index required property string modelData @@ -92,10 +96,9 @@ Item { color: selected ? (destructive ? Util.alpha(Color.urgent, 0.22) : root.selectedBackground) : "transparent" - border.color: destructive + borderSpec: Border.flat(destructive ? (selected ? Color.urgent : Util.alpha(Color.urgent, 0.56)) - : (selected ? root.selectedText : Util.alpha(root.foreground, 0.38)) - border.width: Style.normalBorderWidth + : (selected ? root.selectedText : Util.alpha(root.foreground, 0.38)), Style.normalBorderWidth) radius: 0 Text { diff --git a/shell/Ui/CursorSurface.qml b/shell/Ui/CursorSurface.qml index 77b25426..58e46d06 100644 --- a/shell/Ui/CursorSurface.qml +++ b/shell/Ui/CursorSurface.qml @@ -11,7 +11,7 @@ import qs.Commons // hover-cursor border. `outline` remains as a compatibility flag for // callers that used to request border-only rows, but slider rows still // receive the same hover-cursor background as every other row. -Rectangle { +BorderSurface { id: root property bool hasCursor: false @@ -27,19 +27,13 @@ Rectangle { radius: Style.cornerRadius color: hasCursor ? fill : (current ? currentFill : "transparent") - - border.color: root.hasCursor - ? Style.hoverBorderFor(root.foreground, root.accent) + borderSpec: root.hasCursor + ? Border.controlSpec("hover-cursor", root.foreground, root.accent) : (root.current - ? Style.selectedBorderFor(root.foreground, root.accent) + ? Border.controlSpec("selected", root.foreground, root.accent) : (root.bordered - ? Style.normalBorderFor(root.foreground, root.accent) - : "transparent")) - border.width: root.hasCursor - ? Style.hoverBorderWidth - : (root.current - ? Style.selectedBorderWidth - : (root.bordered ? Style.normalBorderWidth : 0)) + ? Border.controlSpec("normal", root.foreground, root.accent) + : Border.none())) Behavior on color { ColorAnimation { duration: 60 } diff --git a/shell/Ui/Dropdown.qml b/shell/Ui/Dropdown.qml index 48bd942b..214a7fe4 100644 --- a/shell/Ui/Dropdown.qml +++ b/shell/Ui/Dropdown.qml @@ -26,6 +26,7 @@ Item { property color background: Color.popups.background property color popupBorder: Color.popups.border property color accent: Color.accent + readonly property var popupBorderSpec: Border.localOrSurfaceSpec("popups", "border", popupBorder, Color.popups.border, Style.normalBorderWidth) property string fontFamily: Style.font.family property int rowHeight: Style.spacing.controlHeight property int popupRowHeight: Style.spacing.popupRowHeight @@ -78,7 +79,7 @@ Item { font.bold: true } - Rectangle { + BorderSurface { id: trigger width: parent.width height: root.rowHeight @@ -86,10 +87,10 @@ Item { readonly property bool _focused: trigger.activeFocus readonly property bool _hot: triggerHover.hovered || root.hasCursor + readonly property var _borderSpec: Border.controlSpec(trigger._focused ? "focus" : (trigger._hot ? "hover-cursor" : "normal"), root.foreground, root.accent) color: Style.controlFill(trigger._focused, trigger._hot, root.foreground, root.accent) - border.color: Style.controlBorder(trigger._focused, trigger._hot, root.foreground, root.accent) - border.width: Style.controlBorderWidth(trigger._focused, trigger._hot) + borderSpec: _borderSpec activeFocusOnTab: true @@ -112,8 +113,8 @@ Item { anchors.left: parent.left anchors.right: chevron.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.spacing.controlPaddingX - anchors.rightMargin: Style.spacing.md + anchors.leftMargin: trigger.borderLeft + Style.spacing.controlPaddingX + anchors.rightMargin: trigger.borderRight + Style.spacing.md text: root.currentLabel() color: root.foreground font.family: root.fontFamily @@ -125,7 +126,7 @@ Item { id: chevron anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: Style.spacing.controlGap + anchors.rightMargin: trigger.borderRight + Style.spacing.controlGap text: "󰅀" color: Qt.darker(root.foreground, 1.2) font.family: root.fontFamily @@ -149,12 +150,15 @@ Item { implicitHeight: Math.min(root.options.length * root.popupRowHeight + Math.max(0, root.options.length - 1) * Style.spacing.labelGap + Style.spacing.xxs, root.popupRowHeight * 8 + 7 * Style.spacing.labelGap + Style.spacing.xxs) padding: Style.spacing.hairline + leftPadding: Border.left(root.popupBorderSpec) + Style.spacing.hairline + rightPadding: Border.right(root.popupBorderSpec) + Style.spacing.hairline + topPadding: Border.top(root.popupBorderSpec) + Style.spacing.hairline + bottomPadding: Border.bottom(root.popupBorderSpec) + Style.spacing.hairline focus: true - background: Rectangle { + background: BorderSurface { color: root.background - border.color: root.popupBorder - border.width: Style.normalBorderWidth + borderSpec: root.popupBorderSpec radius: Style.cornerRadius } diff --git a/shell/Ui/KeyboardPanel.qml b/shell/Ui/KeyboardPanel.qml index c23d41f3..6d09093b 100644 --- a/shell/Ui/KeyboardPanel.qml +++ b/shell/Ui/KeyboardPanel.qml @@ -285,15 +285,15 @@ PanelWindow { // --- card ---------------------------------------------------------------- - Rectangle { + BorderSurface { id: card x: root.cardOrigin.x y: root.cardOrigin.y width: root.contentWidth height: root.contentHeight color: Color.popups.background - border.color: Color.popups.border - border.width: Math.max(1, Style.space(2)) + borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2))) + padding: root.padding radius: Style.cornerRadius opacity: root.open || root.popoutSwitching ? 1.0 : 0 @@ -309,7 +309,10 @@ PanelWindow { Item { id: contentHolder anchors.fill: parent - anchors.margins: root.padding + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset opacity: root.popoutSwitching ? (root.open ? 1.0 : 0) : 1.0 Behavior on opacity { diff --git a/shell/Ui/MultiSelect.qml b/shell/Ui/MultiSelect.qml index 93c4bfe7..f759b66d 100644 --- a/shell/Ui/MultiSelect.qml +++ b/shell/Ui/MultiSelect.qml @@ -37,6 +37,7 @@ Item { property color background: Color.popups.background property color popupBorder: Color.popups.border property color accent: Color.accent + readonly property var popupBorderSpec: Border.localOrSurfaceSpec("popups", "border", popupBorder, Color.popups.border, Style.normalBorderWidth) property string fontFamily: Style.font.family property int rowHeight: Style.spacing.controlHeight property int popupRowHeight: Style.spacing.popupRowHeight @@ -266,7 +267,7 @@ Item { font.bold: true } - Rectangle { + BorderSurface { id: trigger width: parent.width height: root.rowHeight @@ -274,10 +275,10 @@ Item { readonly property bool _focused: trigger.activeFocus readonly property bool _hot: triggerHover.hovered || root.hasCursor + readonly property var _borderSpec: Border.controlSpec(trigger._focused ? "focus" : (trigger._hot ? "hover-cursor" : "normal"), root.foreground, root.accent) color: Style.controlFill(trigger._focused, trigger._hot, root.foreground, root.accent) - border.color: Style.controlBorder(trigger._focused, trigger._hot, root.foreground, root.accent) - border.width: Style.controlBorderWidth(trigger._focused, trigger._hot) + borderSpec: _borderSpec activeFocusOnTab: true @@ -300,8 +301,8 @@ Item { anchors.left: parent.left anchors.right: chevron.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.spacing.controlPaddingX - anchors.rightMargin: Style.spacing.md + anchors.leftMargin: trigger.borderLeft + Style.spacing.controlPaddingX + anchors.rightMargin: trigger.borderRight + Style.spacing.md text: root.selectionLabel() || root.triggerLabel || root.noSelectionText color: root.selectionLabel() ? root.foreground : Qt.darker(root.foreground, 1.5) font.family: root.fontFamily @@ -313,7 +314,7 @@ Item { id: chevron anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: Style.spacing.controlGap + anchors.rightMargin: trigger.borderRight + Style.spacing.controlGap text: "󰅀" color: Qt.darker(root.foreground, 1.2) font.family: root.fontFamily @@ -359,6 +360,10 @@ Item { // when there isn't room, otherwise the popup overflows the window. implicitHeight: Math.min(_availableBelow, _idealContent, _maxRowsHeight) padding: Style.spacing.hairline + leftPadding: Border.left(root.popupBorderSpec) + Style.spacing.hairline + rightPadding: Border.right(root.popupBorderSpec) + Style.spacing.hairline + topPadding: Border.top(root.popupBorderSpec) + Style.spacing.hairline + bottomPadding: Border.bottom(root.popupBorderSpec) + Style.spacing.hairline focus: true Connections { @@ -369,10 +374,9 @@ Item { function onHeightChanged() { popup.reposition() } } - background: Rectangle { + background: BorderSurface { color: root.background - border.color: root.popupBorder - border.width: Style.normalBorderWidth + borderSpec: root.popupBorderSpec radius: Style.cornerRadius } @@ -432,7 +436,7 @@ Item { } } - Rectangle { + BorderSurface { id: refreshButton visible: root.arrayFrom(root.optionsCommand).length > 0 enabled: !root.loadingOptions @@ -442,10 +446,9 @@ Item { color: refreshHover.hovered ? Style.hoverFillFor(root.foreground, root.accent) : Style.normalFillFor(root.foreground, root.accent) - border.color: refreshHover.hovered - ? Style.hoverBorderFor(root.foreground, root.accent) - : Style.normalBorderFor(root.foreground, root.accent) - border.width: refreshHover.hovered ? Style.hoverBorderWidth : Style.normalBorderWidth + borderSpec: refreshHover.hovered + ? Border.controlSpec("hover-cursor", root.foreground, root.accent) + : Border.controlSpec("normal", root.foreground, root.accent) Text { anchors.centerIn: parent @@ -550,17 +553,16 @@ Item { anchors.rightMargin: Style.spacing.controlPaddingX spacing: Style.spacing.rowGap - Rectangle { + BorderSurface { id: checkbox width: Style.space(16) height: Style.space(16) radius: Math.max(2, Style.cornerRadius / 2) anchors.verticalCenter: parent.verticalCenter color: selected ? Style.selectedFillFor(root.foreground, root.accent) : "transparent" - border.color: selected - ? Style.selectedBorderFor(root.foreground, root.accent) - : Style.normalBorderFor(root.foreground, root.accent) - border.width: selected ? Style.selectedBorderWidth : Style.normalBorderWidth + borderSpec: selected + ? Border.controlSpec("selected", root.foreground, root.accent) + : Border.controlSpec("normal", root.foreground, root.accent) Text { anchors.centerIn: parent diff --git a/shell/Ui/NumberField.qml b/shell/Ui/NumberField.qml index 8c860d70..24b70f5b 100644 --- a/shell/Ui/NumberField.qml +++ b/shell/Ui/NumberField.qml @@ -44,15 +44,20 @@ Column { font.family: root.fontFamily font.pixelSize: root.fontSize + readonly property bool _focused: spin.activeFocus + readonly property bool _hot: root._hovered || root.hasCursor + readonly property var _borderSpec: Border.controlSpec(_focused ? "focus" : (_hot ? "hover-cursor" : "normal"), root.foreground, root.accent) + + leftPadding: Border.left(_borderSpec) + Style.spacing.controlPaddingX + rightPadding: Border.right(_borderSpec) + Style.spacing.controlPaddingX + topPadding: Border.top(_borderSpec) + bottomPadding: Border.bottom(_borderSpec) + onValueModified: root.modified(value) - background: Rectangle { - readonly property bool _focused: spin.activeFocus - readonly property bool _hot: root._hovered || root.hasCursor - - color: Style.controlFill(_focused, _hot, root.foreground, root.accent) - border.color: Style.controlBorder(_focused, _hot, root.foreground, root.accent) - border.width: Style.controlBorderWidth(_focused, _hot) + background: BorderSurface { + color: Style.controlFill(spin._focused, spin._hot, root.foreground, root.accent) + borderSpec: spin._borderSpec radius: Style.cornerRadius HoverHandler { diff --git a/shell/Ui/PanelActionButton.qml b/shell/Ui/PanelActionButton.qml index cf437607..8a1b10bc 100644 --- a/shell/Ui/PanelActionButton.qml +++ b/shell/Ui/PanelActionButton.qml @@ -24,7 +24,7 @@ import qs.Commons // Use this when a PanelActionButton is itself the cursor target (rather // than living inside a CursorSurface row). Emits `hovered(bool)` on // pointer enter/leave so the panel can update its cursor state to match. -Rectangle { +BorderSurface { id: root property string iconText: "" @@ -53,22 +53,18 @@ Rectangle { readonly property bool _showFocusRing: focusable && activeFocus readonly property bool _hot: (mouse.containsMouse || root.hasCursor) && root.enabled + readonly property var _borderSpec: _showFocusRing + ? Border.controlSpec("focus", hoverColor, hoverColor) + : (_hot && bordered + ? Border.controlSpec("hover-cursor", hoverColor, hoverColor) + : (bordered ? Border.controlSpec("normal", foreground, Color.accent) : Border.none())) color: _showFocusRing ? Style.focusFillFor(hoverColor, hoverColor) : (_hot ? Style.hoverFillFor(hoverColor, hoverColor) : "transparent") - border.width: _showFocusRing - ? Style.focusBorderWidth - : (_hot && bordered - ? Style.hoverBorderWidth - : (bordered ? Style.normalBorderWidth : 0)) - border.color: _showFocusRing - ? Style.focusBorderFor(hoverColor, hoverColor) - : (_hot && bordered - ? Style.hoverBorderFor(hoverColor, hoverColor) - : Style.normalBorderFor(foreground, Color.accent)) + borderSpec: _borderSpec Behavior on color { ColorAnimation { duration: 60 } } diff --git a/shell/Ui/PanelHero.qml b/shell/Ui/PanelHero.qml index 2f44f162..8ef8c617 100644 --- a/shell/Ui/PanelHero.qml +++ b/shell/Ui/PanelHero.qml @@ -56,15 +56,14 @@ Item { height: 1 } - Rectangle { + BorderSurface { id: detailPill visible: root.detail !== "" implicitWidth: detailText.implicitWidth + Style.space(10) implicitHeight: detailText.implicitHeight + Style.space(4) anchors.verticalCenter: parent.verticalCenter color: "transparent" - border.color: Style.normalBorderFor(root.foreground) - border.width: Style.normalBorderWidth + borderSpec: Border.controlSpec("normal", root.foreground, Color.accent) radius: Style.cornerRadius Text { diff --git a/shell/Ui/PanelSlider.qml b/shell/Ui/PanelSlider.qml index b7ec9495..f8359bb5 100644 --- a/shell/Ui/PanelSlider.qml +++ b/shell/Ui/PanelSlider.qml @@ -55,14 +55,13 @@ Item { } } - Rectangle { + BorderSurface { id: knob width: root.knobSize height: root.knobSize radius: root.knobSize / 2 color: root.knobColor - border.color: root.bar ? root.bar.background : "#101315" - border.width: Math.max(1, Style.space(2)) + borderSpec: Border.flat(root.bar ? root.bar.background : "#101315", Math.max(1, Style.space(2))) anchors.verticalCenter: track.verticalCenter x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2)) scale: root._hot ? 1.15 : 1.0 diff --git a/shell/Ui/PanelToolTip.qml b/shell/Ui/PanelToolTip.qml index 97c7da08..139b6cf0 100644 --- a/shell/Ui/PanelToolTip.qml +++ b/shell/Ui/PanelToolTip.qml @@ -24,13 +24,14 @@ ToolTip { property string fontFamily: Style.font.family property real fontSize: Style.font.bodySmall + readonly property var panelBorderSpec: Border.localOrSurfaceSpec("tooltip", "border", panelBorder, Color.tooltip.border, Style.normalBorderWidth) + delay: 400 padding: 0 - background: Rectangle { + background: BorderSurface { color: root.panelBackground - border.color: root.panelBorder - border.width: Style.normalBorderWidth + borderSpec: root.panelBorderSpec radius: Style.cornerRadius } @@ -39,9 +40,9 @@ ToolTip { color: root.panelForeground font.family: root.fontFamily font.pixelSize: root.fontSize - leftPadding: Style.spacing.controlPaddingX - rightPadding: Style.spacing.controlPaddingX - topPadding: Style.spacing.controlPaddingY - bottomPadding: Style.spacing.controlPaddingY + leftPadding: Border.left(root.panelBorderSpec) + Style.spacing.controlPaddingX + rightPadding: Border.right(root.panelBorderSpec) + Style.spacing.controlPaddingX + topPadding: Border.top(root.panelBorderSpec) + Style.spacing.controlPaddingY + bottomPadding: Border.bottom(root.panelBorderSpec) + Style.spacing.controlPaddingY } } diff --git a/shell/Ui/PopupCard.qml b/shell/Ui/PopupCard.qml index 475d7df4..ef72fe16 100644 --- a/shell/Ui/PopupCard.qml +++ b/shell/Ui/PopupCard.qml @@ -14,6 +14,7 @@ PopupWindow { property int contentWidth: Style.space(280) property int contentHeight: Style.space(200) property color borderColor: Color.popups.border + property var borderSpec: Border.localOrSurfaceSpec("popups", "border", borderColor, Color.popups.border, Math.max(1, Style.space(2))) property bool open: false property bool centerOnBar: false // "click" — uses HyprlandFocusGrab so clicking outside dismisses the popup. @@ -145,12 +146,12 @@ PopupWindow { } } - Rectangle { + BorderSurface { id: card anchors.fill: parent color: Color.popups.background - border.color: root.borderColor - border.width: Math.max(1, Style.space(2)) + borderSpec: root.borderSpec + padding: root.padding radius: Style.cornerRadius opacity: root.open ? 1.0 : 0 @@ -161,7 +162,10 @@ PopupWindow { Item { id: contentHolder anchors.fill: parent - anchors.margins: root.padding + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset } HoverHandler { diff --git a/shell/Ui/SearchableDropdown.qml b/shell/Ui/SearchableDropdown.qml index 59c1f9dc..9cf0aa49 100644 --- a/shell/Ui/SearchableDropdown.qml +++ b/shell/Ui/SearchableDropdown.qml @@ -29,6 +29,7 @@ Item { property color background: Color.popups.background property color popupBorder: Color.popups.border property color accent: Color.accent + readonly property var popupBorderSpec: Border.localOrSurfaceSpec("popups", "border", popupBorder, Color.popups.border, Style.normalBorderWidth) property string fontFamily: Style.font.family property int rowHeight: Style.spacing.controlHeight property int popupRowHeight: Style.spacing.popupRowHeight @@ -100,7 +101,7 @@ Item { font.bold: true } - Rectangle { + BorderSurface { id: trigger width: parent.width height: root.rowHeight @@ -108,10 +109,10 @@ Item { readonly property bool _focused: trigger.activeFocus readonly property bool _hot: triggerHover.hovered || root.hasCursor + readonly property var _borderSpec: Border.controlSpec(trigger._focused ? "focus" : (trigger._hot ? "hover-cursor" : "normal"), root.foreground, root.accent) color: Style.controlFill(trigger._focused, trigger._hot, root.foreground, root.accent) - border.color: Style.controlBorder(trigger._focused, trigger._hot, root.foreground, root.accent) - border.width: Style.controlBorderWidth(trigger._focused, trigger._hot) + borderSpec: _borderSpec activeFocusOnTab: true @@ -134,8 +135,8 @@ Item { anchors.left: parent.left anchors.right: chevron.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.spacing.controlPaddingX - anchors.rightMargin: Style.spacing.md + anchors.leftMargin: trigger.borderLeft + Style.spacing.controlPaddingX + anchors.rightMargin: trigger.borderRight + Style.spacing.md text: root.currentLabel() || root.triggerLabel || root.placeholderText color: (root.currentLabel() || root.triggerLabel) ? root.foreground : Qt.darker(root.foreground, 1.5) font.family: root.fontFamily @@ -147,7 +148,7 @@ Item { id: chevron anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: Style.spacing.controlGap + anchors.rightMargin: trigger.borderRight + Style.spacing.controlGap text: "󰅀" color: Qt.darker(root.foreground, 1.2) font.family: root.fontFamily @@ -172,12 +173,15 @@ Item { Math.min(resultList.contentHeight + Style.space(50), root.popupRowHeight * 6 + 5 * Style.spacing.labelGap + Style.space(50))) padding: Style.spacing.hairline + leftPadding: Border.left(root.popupBorderSpec) + Style.spacing.hairline + rightPadding: Border.right(root.popupBorderSpec) + Style.spacing.hairline + topPadding: Border.top(root.popupBorderSpec) + Style.spacing.hairline + bottomPadding: Border.bottom(root.popupBorderSpec) + Style.spacing.hairline focus: true - background: Rectangle { + background: BorderSurface { color: root.background - border.color: root.popupBorder - border.width: Style.normalBorderWidth + borderSpec: root.popupBorderSpec radius: Style.cornerRadius } diff --git a/shell/Ui/TextField.qml b/shell/Ui/TextField.qml index 53c16d91..8d4183f6 100644 --- a/shell/Ui/TextField.qml +++ b/shell/Ui/TextField.qml @@ -34,6 +34,7 @@ TextField { readonly property bool _focused: activeFocus readonly property bool _hot: hovered || hasCursor + readonly property var _borderSpec: Border.controlSpec(_focused ? "focus" : (_hot ? "hover-cursor" : "normal"), root.foreground, root.accent) echoMode: password ? TextInput.Password : TextInput.Normal font.family: Style.font.family @@ -43,15 +44,14 @@ TextField { selectedTextColor: foreground placeholderTextColor: Qt.darker(foreground, 1.6) - leftPadding: horizontalPadding - rightPadding: horizontalPadding - topPadding: verticalPadding - bottomPadding: verticalPadding + leftPadding: horizontalPadding + Border.left(_borderSpec) + rightPadding: horizontalPadding + Border.right(_borderSpec) + topPadding: verticalPadding + Border.top(_borderSpec) + bottomPadding: verticalPadding + Border.bottom(_borderSpec) - background: Rectangle { + background: BorderSurface { color: Style.controlFill(root._focused, root._hot, root.foreground, root.accent) - border.color: Style.controlBorder(root._focused, root._hot, root.foreground, root.accent) - border.width: Style.controlBorderWidth(root._focused, root._hot) + borderSpec: root._borderSpec radius: Style.cornerRadius } } diff --git a/shell/Ui/Toggle.qml b/shell/Ui/Toggle.qml index 10324f87..0933508f 100644 --- a/shell/Ui/Toggle.qml +++ b/shell/Ui/Toggle.qml @@ -12,7 +12,7 @@ import qs.Commons // `rounded` auto-detects from Style.cornerRadius so the switch follows // the theme: pill shape when Hyprland corners are rounded, square on sharp. // Callers can override per-instance. -Rectangle { +BorderSurface { id: root property string label: "" @@ -52,10 +52,10 @@ Rectangle { radius: Style.cornerRadius readonly property bool _hot: hasCursor || mouse.containsMouse + readonly property var _borderSpec: Border.controlSpec(activeFocus ? "focus" : (_hot ? "hover-cursor" : "normal"), foreground, accent) color: Style.controlFill(activeFocus, _hot, foreground, accent) - border.color: Style.controlBorder(activeFocus, _hot, foreground, accent) - border.width: Style.controlBorderWidth(activeFocus, _hot) + borderSpec: _borderSpec Behavior on color { ColorAnimation { duration: 100 } } @@ -64,8 +64,8 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.spacing.rowPaddingX - anchors.rightMargin: Style.spacing.rowPaddingX + anchors.leftMargin: root.borderLeft + Style.spacing.rowPaddingX + anchors.rightMargin: root.borderRight + Style.spacing.rowPaddingX spacing: Style.spacing.rowPaddingX Column { @@ -94,7 +94,7 @@ Rectangle { } } - Rectangle { + BorderSurface { id: track width: root.trackWidth height: root.trackHeight @@ -102,10 +102,9 @@ Rectangle { color: root.checked ? Style.selectedFillFor(root.foreground, root.accent) : Style.normalFillFor(root.foreground, root.accent) - border.color: root.checked - ? Style.selectedBorderFor(root.foreground, root.accent) - : Style.normalBorderFor(root.foreground, root.accent) - border.width: root.checked ? Style.selectedBorderWidth : Style.normalBorderWidth + borderSpec: root.checked + ? Border.controlSpec("selected", root.foreground, root.accent) + : Border.controlSpec("normal", root.foreground, root.accent) anchors.verticalCenter: parent.verticalCenter Behavior on color { ColorAnimation { duration: 120 } } diff --git a/shell/Ui/qmldir b/shell/Ui/qmldir index e3d9dabe..2e25f4e5 100644 --- a/shell/Ui/qmldir +++ b/shell/Ui/qmldir @@ -2,6 +2,8 @@ module qs.Ui BarIndicator 1.0 BarIndicator.qml BarWidget 1.0 BarWidget.qml +BorderOverlay 1.0 BorderOverlay.qml +BorderSurface 1.0 BorderSurface.qml Button 1.0 Button.qml ButtonGroup 1.0 ButtonGroup.qml ConfirmDialog 1.0 ConfirmDialog.qml diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 9902aec3..33c937ef 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -798,13 +798,12 @@ Item { } } - Rectangle { + BorderSurface { id: tooltipBubble implicitWidth: tooltipLabel.implicitWidth + 20 implicitHeight: tooltipLabel.implicitHeight + 14 color: Color.tooltip.background - border.color: Color.tooltip.border - border.width: 1 + borderSpec: Border.surfaceSpec("tooltip", "border", Color.tooltip.border, 1) radius: Style.cornerRadius Text { @@ -904,11 +903,10 @@ Item { width: ghostWindow.ghostWidth + ghostWindow.ghostPadding * 2 height: ghostWindow.ghostHeight + ghostWindow.ghostPadding * 2 - Rectangle { + BorderSurface { anchors.fill: parent color: root.transparent ? "transparent" : root.background - border.color: root.barForeground - border.width: 1 + borderSpec: Border.flat(root.barForeground, 1) radius: Math.min(Style.cornerRadius, height / 2) opacity: root.transparent ? 0.45 : 0.94 } @@ -1234,13 +1232,12 @@ Item { HoverHandler { id: moduleHover } - Rectangle { + BorderSurface { visible: slot.dragSource anchors.fill: parent anchors.margins: Style.space(1) color: root.transparent ? "transparent" : root.background - border.color: root.barForeground - border.width: 1 + borderSpec: Border.flat(root.barForeground, 1) radius: Math.min(Style.cornerRadius, height / 2) opacity: root.transparent ? 0.22 : 0.32 } diff --git a/shell/plugins/clipboard/Clipboard.qml b/shell/plugins/clipboard/Clipboard.qml index b35a246c..1db30c01 100644 --- a/shell/plugins/clipboard/Clipboard.qml +++ b/shell/plugins/clipboard/Clipboard.qml @@ -26,6 +26,7 @@ Item { property color background: Color.menu.background property color foreground: Color.menu.text property color border: Color.menu.border + property var borderSpec: Border.surfaceSpec("menu", "border", border, Math.max(1, Style.space(2))) property color scrim: Color.menu.scrim property color selectedBackground: Color.menu.selectedBackground property color selectedText: Color.menu.selectedText @@ -275,15 +276,15 @@ Item { onClicked: root.close() } - Rectangle { + BorderSurface { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent color: root.background - border.color: root.border - border.width: Math.max(1, Style.space(2)) + borderSpec: root.borderSpec + padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: {} } @@ -357,7 +358,10 @@ Item { Column { anchors.fill: parent - anchors.margins: root.contentMargin + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset spacing: root.contentSpacing Rectangle { @@ -365,7 +369,6 @@ Item { height: root.headerHeight radius: root.cornerRadius color: "transparent" - border.width: 0 Text { anchors.left: parent.left @@ -415,7 +418,6 @@ Item { height: root.rowHeight radius: root.cornerRadius color: hasCursor ? root.selectedBackground : "transparent" - border.width: 0 Row { anchors.fill: parent diff --git a/shell/plugins/dev-gallery/GalleryPanel.qml b/shell/plugins/dev-gallery/GalleryPanel.qml index e679b50a..aec74b36 100644 --- a/shell/plugins/dev-gallery/GalleryPanel.qml +++ b/shell/plugins/dev-gallery/GalleryPanel.qml @@ -376,13 +376,12 @@ Item { font.bold: true } - Rectangle { + BorderSurface { width: parent.width implicitHeight: conventionsCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: conventionsCol @@ -458,13 +457,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: typeCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: typeCol @@ -676,13 +674,12 @@ Item { font.pixelSize: Style.font.caption } - Rectangle { + BorderSurface { width: parent.width implicitHeight: shCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: shCol @@ -732,13 +729,12 @@ Item { font.pixelSize: Style.font.caption } - Rectangle { + BorderSurface { width: parent.width implicitHeight: sepCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: sepCol @@ -777,13 +773,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: csCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: csCol @@ -862,13 +857,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: buttonRow.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Row { id: buttonRow @@ -1019,13 +1013,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: choiceRow.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) ButtonGroup { id: choiceRow @@ -1070,13 +1063,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: pabCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Row { id: pabCol @@ -1162,7 +1154,7 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { id: tipSwatch width: Style.space(140) height: Style.space(36) @@ -1170,10 +1162,9 @@ Item { color: tipMouse.containsMouse || focused ? Style.hoverFillFor(root.foreground, root.accent) : Style.normalFillFor(root.foreground, root.accent) - border.color: focused - ? Style.hoverBorderFor(root.foreground, root.accent) - : Style.normalBorderFor(root.foreground, root.accent) - border.width: focused ? Style.hoverBorderWidth : Style.normalBorderWidth + borderSpec: focused + ? Border.controlSpec("hover-cursor", root.foreground, root.accent) + : Border.controlSpec("normal", root.foreground, root.accent) radius: Style.cornerRadius onFocusedChanged: if (focused) root.ensureCursorVisible(this) @@ -1306,13 +1297,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: tfCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: tfCol @@ -1390,12 +1380,11 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: numberDemo.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12), 1) radius: Style.cornerRadius NumberField { @@ -1502,13 +1491,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: ddCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: ddCol @@ -1558,13 +1546,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: sddCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: sddCol @@ -1630,13 +1617,12 @@ Item { wrapMode: Text.WordWrap } - Rectangle { + BorderSurface { width: parent.width implicitHeight: composedCol.implicitHeight + Style.spacing.rowPaddingX * 2 color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04) radius: Style.cornerRadius - border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10), 1) Column { id: composedCol diff --git a/shell/plugins/emojis/Emojis.qml b/shell/plugins/emojis/Emojis.qml index 9c44d6df..b2b7bf4b 100644 --- a/shell/plugins/emojis/Emojis.qml +++ b/shell/plugins/emojis/Emojis.qml @@ -3,6 +3,7 @@ import Quickshell.Io import Quickshell.Wayland import QtQuick import qs.Commons +import qs.Ui import "EmojiSearch.js" as EmojiSearch Item { @@ -25,6 +26,7 @@ Item { property color background: Color.menu.background property color foreground: Color.menu.text property color border: Color.menu.border + property var borderSpec: Border.surfaceSpec("menu", "border", border, Math.max(1, Style.space(2))) property color scrim: Color.menu.scrim property color selectedBackground: Color.menu.selectedBackground property color selectedText: Color.menu.selectedText @@ -175,15 +177,15 @@ Item { onClicked: root.dismiss() } - Rectangle { + BorderSurface { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent color: root.background - border.color: root.border - border.width: Math.max(1, Style.space(2)) + borderSpec: root.borderSpec + padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: {} } @@ -232,7 +234,10 @@ Item { Column { anchors.fill: parent - anchors.margins: root.contentMargin + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset spacing: root.contentSpacing Rectangle { @@ -240,7 +245,6 @@ Item { height: root.headerHeight radius: root.cornerRadius color: "transparent" - border.width: 0 Text { anchors.left: parent.left @@ -278,7 +282,6 @@ Item { height: root.cellHeight radius: root.cornerRadius color: hasCursor ? root.selectedBackground : "transparent" - border.width: 0 Text { text: parent.emoji diff --git a/shell/plugins/launcher/Launcher.qml b/shell/plugins/launcher/Launcher.qml index bde9b097..91cc0806 100644 --- a/shell/plugins/launcher/Launcher.qml +++ b/shell/plugins/launcher/Launcher.qml @@ -37,9 +37,12 @@ Item { property color background: Color.launcher.background property color foreground: Color.launcher.text property color border: Color.launcher.border + property var borderSpec: Border.surfaceSpec("launcher", "border", border, 2) property color scrim: Color.launcher.scrim property color selectedBackground: Color.launcher.selectedBackground property color selectedText: Color.launcher.selectedText + property color selectedBorder: Color.launcher.selectedBorder + property var selectedBorderSpec: Border.surfaceSpec("launcher", "selected-border", selectedBorder, 0) property string fontFamily: Style.font.menuFamily property int cardWidth: 644 @@ -342,15 +345,15 @@ Item { onClicked: root.dismiss() } - Rectangle { + BorderSurface { id: card width: Math.min(root.cardWidth, panel.width - Style.gapsOut * 2) height: Math.min(root.cardHeight, panel.height - Style.gapsOut * 2) radius: Style.cornerRadius anchors.centerIn: parent color: root.background - border.color: root.border - border.width: 2 + borderSpec: root.borderSpec + padding: root.contentMargin clip: true MouseArea { anchors.fill: parent; onClicked: {} } @@ -435,7 +438,10 @@ Item { Column { anchors.fill: parent - anchors.margins: root.contentMargin + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset spacing: root.contentSpacing Rectangle { @@ -471,7 +477,7 @@ Item { spacing: 0 boundsBehavior: Flickable.StopAtBounds - delegate: Rectangle { + delegate: BorderSurface { id: row required property int index required property string name @@ -484,12 +490,12 @@ Item { height: root.rowHeight radius: 0 color: row.hasCursor ? root.selectedBackground : "transparent" - border.width: 0 + borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none() Item { id: iconSlot anchors.left: parent.left - anchors.leftMargin: 14 + anchors.leftMargin: row.borderLeft + 14 anchors.verticalCenter: parent.verticalCenter width: root.iconSlotWidth height: parent.height @@ -519,7 +525,7 @@ Item { anchors.left: iconSlot.right anchors.leftMargin: 14 anchors.right: parent.right - anchors.rightMargin: 14 + anchors.rightMargin: row.borderRight + 14 anchors.verticalCenter: parent.verticalCenter text: row.name color: row.hasCursor ? root.selectedText : root.foreground diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index 224f33b7..c9afda1d 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Effects import qs.Commons +import qs.Ui Item { id: root @@ -26,6 +27,9 @@ Item { readonly property int fieldFontSize: Style.font.heading readonly property int passwordDotFontSize: Math.round(fieldFontSize * 1.3) readonly property bool showPasswordCursor: inputEnabled && !authenticatingPassword && failureMessage.length === 0 + readonly property string borderToken: failureMessage.length > 0 ? "border-error" : (authenticatingPassword ? "border-active" : "border") + readonly property color borderFallback: failureMessage.length > 0 ? Color.lock.borderError : (authenticatingPassword ? Color.lock.borderActive : Color.lock.border) + readonly property var inputBorderSpec: Border.surfaceSpec("lock", borderToken, borderFallback, root.outlineThickness, "border-alpha") signal submitPassword(string password) signal passwordTextEdited(string password) @@ -97,22 +101,23 @@ Item { onPositionChanged: root.wakeRequested() } - Rectangle { + BorderSurface { id: inputField width: root.fieldWidth height: root.fieldHeight anchors.centerIn: parent color: Color.lock.background - border.color: root.failureMessage.length > 0 ? Color.lock.borderError : (root.authenticatingPassword ? Color.lock.borderActive : Color.lock.border) - border.width: root.outlineThickness + borderSpec: root.inputBorderSpec radius: Style.cornerRadius clip: true TextInput { id: passwordInput anchors.fill: parent - anchors.leftMargin: root.outlineThickness + 18 - anchors.rightMargin: root.outlineThickness + 18 + anchors.topMargin: inputField.borderTop + anchors.rightMargin: inputField.borderRight + 18 + anchors.bottomMargin: inputField.borderBottom + anchors.leftMargin: inputField.borderLeft + 18 verticalAlignment: TextInput.AlignVCenter horizontalAlignment: TextInput.AlignHCenter activeFocusOnPress: true diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 49b0910e..54cbdf60 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -3,6 +3,7 @@ import Quickshell.Io import Quickshell.Wayland import QtQuick import qs.Commons +import qs.Ui import "MenuModel.js" as MenuModel Item { @@ -77,9 +78,12 @@ Item { property color background: Color.menu.background property color foreground: Color.menu.text property color border: Color.menu.border + property var borderSpec: Border.surfaceSpec("menu", "border", border, Math.max(1, Style.space(2))) property color scrim: Color.menu.scrim property color selectedBackground: Color.menu.selectedBackground property color selectedText: Color.menu.selectedText + property color selectedBorder: Color.menu.selectedBorder + property var selectedBorderSpec: Border.surfaceSpec("menu", "selected-border", selectedBorder, 0) readonly property int cornerRadius: Style.cornerRadius property int contentMargin: Style.spacing.panelPadding property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2) @@ -787,15 +791,15 @@ Item { onClicked: root.cancel() } - Rectangle { + BorderSurface { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent color: root.background - border.color: root.border - border.width: Math.max(1, Style.space(2)) + borderSpec: root.borderSpec + padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: {} } @@ -842,7 +846,10 @@ Item { Column { anchors.fill: parent - anchors.margins: root.contentMargin + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset spacing: root.contentSpacing Rectangle { @@ -850,7 +857,6 @@ Item { height: root.headerHeight radius: root.cornerRadius color: "transparent" - border.width: 0 Text { anchors.left: parent.left @@ -898,7 +904,7 @@ Item { } } - delegate: Rectangle { + delegate: BorderSurface { id: row required property int index required property string itemId @@ -918,7 +924,7 @@ Item { height: root.rowHeightForDetail(row.detail) radius: root.cornerRadius color: row.hasCursor ? root.selectedBackground : "transparent" - border.width: 0 + borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none() Rectangle { visible: false @@ -927,7 +933,7 @@ Item { radius: Math.min(root.cornerRadius, Style.space(4)) color: root.selectedBackground anchors.left: parent.left - anchors.leftMargin: Style.space(8) + anchors.leftMargin: row.borderLeft + Style.space(8) anchors.verticalCenter: parent.verticalCenter } @@ -942,14 +948,14 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter anchors.left: parent.left - anchors.leftMargin: Style.space(8) + anchors.leftMargin: row.borderLeft + Style.space(8) y: contentColumn.y + labelText.y + (labelText.height - height) / 2 } Column { id: contentColumn anchors.left: row.hasIcon ? iconText.right : parent.left - anchors.leftMargin: row.hasIcon ? Style.space(6) : Style.space(18) + anchors.leftMargin: row.hasIcon ? Style.space(6) : row.borderLeft + Style.space(18) anchors.right: trail.left anchors.rightMargin: Style.space(6) anchors.verticalCenter: parent.verticalCenter @@ -982,7 +988,7 @@ Item { id: trail width: Style.space(14) anchors.right: parent.right - anchors.rightMargin: Style.space(8) + anchors.rightMargin: row.borderRight + Style.space(8) y: contentColumn.y + labelText.y + (labelText.height - height) / 2 spacing: 0 diff --git a/shell/plugins/model-usage/Widget.qml b/shell/plugins/model-usage/Widget.qml index 89d63ae8..8dbb7366 100644 --- a/shell/plugins/model-usage/Widget.qml +++ b/shell/plugins/model-usage/Widget.qml @@ -1055,7 +1055,7 @@ BarWidget { } } - component SectionCard: Rectangle { + component SectionCard: BorderSurface { id: section property string title: "" property string subtitle: "" @@ -1064,17 +1064,20 @@ BarWidget { Layout.fillWidth: true color: card - border.color: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.05) - border.width: 1 + borderSpec: Border.flat(Qt.rgba(foreground.r, foreground.g, foreground.b, 0.05), 1) + padding: 12 radius: Style.cornerRadius - implicitHeight: body.implicitHeight + 22 + implicitHeight: body.implicitHeight + contentTopInset + contentBottomInset ColumnLayout { id: body anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: 12 + anchors.topMargin: section.contentTopInset + anchors.rightMargin: section.contentRightInset + anchors.bottomMargin: section.contentBottomInset + anchors.leftMargin: section.contentLeftInset spacing: 8 PanelSectionHeader { diff --git a/shell/plugins/notifications/BarWidget.qml b/shell/plugins/notifications/BarWidget.qml index a1217044..282ce215 100644 --- a/shell/plugins/notifications/BarWidget.qml +++ b/shell/plugins/notifications/BarWidget.qml @@ -130,14 +130,13 @@ BarWidget { Item { Layout.fillWidth: true } - Rectangle { + BorderSurface { id: dndPill Layout.preferredHeight: Math.max(Style.space(24), Style.font.bodySmall + Style.spacing.controlPaddingY * 2) Layout.preferredWidth: dndLabel.implicitWidth + dndGlyph.implicitWidth + Style.space(18) radius: Math.min(Style.space(12), root.cardRadius + Style.space(6)) color: dndOn ? root.colAccent : root.colSurface - border.color: dndOn ? root.colAccent : root.colBorder - border.width: Style.normalBorderWidth + borderSpec: Border.flat(dndOn ? root.colAccent : root.colBorder, Style.normalBorderWidth) readonly property bool dndOn: !!root.notificationService && root.notificationService.doNotDisturb @@ -228,13 +227,12 @@ BarWidget { Item { Layout.fillWidth: true } - Rectangle { + BorderSurface { Layout.preferredWidth: actionLabel.implicitWidth + Style.space(16) Layout.preferredHeight: Math.max(Style.space(22), Style.font.bodySmall + Style.spacing.controlPaddingY * 2) radius: Math.min(Style.space(6), root.cardRadius) color: actionArea.containsMouse ? root.colBorder : "transparent" - border.color: root.colBorder - border.width: Style.normalBorderWidth + borderSpec: Border.flat(root.colBorder, Style.normalBorderWidth) Text { id: actionLabel @@ -272,7 +270,7 @@ BarWidget { : (onPending ? root.notificationService.pendingModel : root.notificationService.pastModel) visible: count > 0 - delegate: Rectangle { + delegate: BorderSurface { id: rowCard required property int index required property string app @@ -293,8 +291,7 @@ BarWidget { implicitHeight: rowContent.implicitHeight + Style.spacing.panelGap radius: root.cardRadius color: "transparent" - border.color: root.colBorder - border.width: Style.normalBorderWidth + borderSpec: Border.flat(root.colBorder, Style.normalBorderWidth) MouseArea { anchors.fill: parent @@ -307,8 +304,8 @@ BarWidget { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.space(12) - anchors.rightMargin: Style.space(12) + anchors.leftMargin: rowCard.borderLeft + Style.space(12) + anchors.rightMargin: rowCard.borderRight + Style.space(12) spacing: Style.space(10) Item { diff --git a/shell/plugins/notifications/components/NotificationCard.qml b/shell/plugins/notifications/components/NotificationCard.qml index f560d581..def0b036 100644 --- a/shell/plugins/notifications/components/NotificationCard.qml +++ b/shell/plugins/notifications/components/NotificationCard.qml @@ -6,9 +6,10 @@ import QtQuick import QtQuick.Layouts import Quickshell import qs.Commons +import qs.Ui import "../NotificationLogic.js" as NotificationLogic -Rectangle { +BorderSurface { id: root property string app: "" @@ -48,6 +49,7 @@ Rectangle { readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4) readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15) readonly property color accentColor: urgency === 2 ? Color.urgent : (urgency === 0 ? dimColor : Color.notifications.countdown) + readonly property var cardBorderSpec: Border.surfaceSpec("notifications", "border", Color.notifications.border, Math.max(1, Style.space(2))) function sanitizeBody(s) { return NotificationLogic.sanitizeBody(s, app, appIcon) @@ -62,13 +64,12 @@ Rectangle { } implicitWidth: Style.space(380) - // Add 2 * border.width so mainColumn (inset by border.width on top/left/right) + // Add vertical border insets so mainColumn (inset by border on top/left/right) // doesn't push content under the bottom edge. - implicitHeight: mainColumn.implicitHeight + border.width * 2 + implicitHeight: mainColumn.implicitHeight + borderTop + borderBottom radius: cornerRadius color: Color.notifications.background - border.color: Color.notifications.border - border.width: Math.max(1, Style.space(2)) + borderSpec: cardBorderSpec clip: true HoverHandler { id: hoverTracker } @@ -86,9 +87,9 @@ Rectangle { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: root.border.width - anchors.leftMargin: root.border.width - anchors.rightMargin: root.border.width + anchors.topMargin: root.borderTop + anchors.leftMargin: root.borderLeft + anchors.rightMargin: root.borderRight spacing: 0 // Text content. diff --git a/shell/plugins/osd/Osd.qml b/shell/plugins/osd/Osd.qml index 8b592163..3a89ada6 100644 --- a/shell/plugins/osd/Osd.qml +++ b/shell/plugins/osd/Osd.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import qs.Commons +import qs.Ui import "OsdModel.js" as OsdModel Item { @@ -79,7 +80,7 @@ Item { // never blocks clicks to the desktop below it. mask: Region {} - Rectangle { + BorderSurface { id: card width: root.mediaOsd ? root.mediaCardWidth : root.cardWidth height: Math.max(Style.space(68), Style.font.displayLarge + Style.spacing.panelGap) @@ -87,15 +88,16 @@ Item { anchors.bottom: parent.bottom anchors.bottomMargin: Style.space(67) color: Util.alpha(Color.background, 0.97) - border.color: Color.popups.border - border.width: Math.max(1, Style.space(2)) + borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2))) radius: Style.cornerRadius opacity: root.opened ? 1 : 0 Row { anchors.fill: parent - anchors.leftMargin: Style.space(16) - anchors.rightMargin: Style.space(16) + anchors.topMargin: card.borderTop + anchors.rightMargin: card.borderRight + Style.space(16) + anchors.bottomMargin: card.borderBottom + anchors.leftMargin: card.borderLeft + Style.space(16) spacing: Style.space(16) Text { width: Style.space(28) diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index eb01318e..4def7c1c 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -741,15 +741,14 @@ Panel { Item { width: Math.max(0, parent.width - parent.children[0].width - heroDetailPill.implicitWidth); height: 1 } - Rectangle { + BorderSurface { id: heroDetailPill visible: heroSsid.detail !== "" implicitWidth: heroDetail.implicitWidth + Style.space(10) implicitHeight: heroDetail.implicitHeight + Style.space(4) anchors.verticalCenter: parent.verticalCenter color: "transparent" - border.color: Style.normalBorderFor(root.bar.foreground) - border.width: Style.normalBorderWidth + borderSpec: Border.controlSpec("normal", root.bar.foreground, Color.accent) radius: Style.cornerRadius Text { @@ -1139,12 +1138,11 @@ Panel { font.pixelSize: Style.font.subtitle } - Rectangle { + BorderSurface { anchors.fill: parent visible: row.forgetFocused color: Style.hoverFillFor(root.bar.urgent, root.bar.urgent) - border.color: Style.hoverBorderFor(root.bar.urgent, root.bar.urgent) - border.width: Style.hoverBorderWidth + borderSpec: Border.controlSpec("hover-cursor", root.bar.urgent, root.bar.urgent) radius: Style.cornerRadius z: -1 } @@ -1256,7 +1254,7 @@ Panel { Component.onCompleted: if (visible) Qt.callLater(forceActiveFocus) } - Rectangle { + BorderSurface { id: statusMsgWrapper visible: row.isBusy || row.isFailed anchors.left: parent.left @@ -1264,8 +1262,7 @@ Panel { anchors.verticalCenter: parent.verticalCenter height: Style.spacing.controlHeight color: Style.normalFillFor(root.bar.foreground) - border.color: Style.normalBorderFor(root.bar.foreground) - border.width: Style.normalBorderWidth + borderSpec: Border.controlSpec("normal", root.bar.foreground, Color.accent) radius: Style.cornerRadius Text { diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 2cbdf9b4..69e8d5fb 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -1022,10 +1022,9 @@ Panel { event.accepted = true } } - background: Rectangle { + background: BorderSurface { color: Color.background - border.color: root.dim - border.width: 1 + borderSpec: Border.flat(root.dim, 1) radius: Style.cornerRadius } diff --git a/shell/plugins/panels/tailscale/TailscaleIcon.qml b/shell/plugins/panels/tailscale/TailscaleIcon.qml index e0f3a1b5..c8b25560 100644 --- a/shell/plugins/panels/tailscale/TailscaleIcon.qml +++ b/shell/plugins/panels/tailscale/TailscaleIcon.qml @@ -1,5 +1,6 @@ import QtQuick import qs.Commons +import qs.Ui Item { id: root @@ -42,7 +43,7 @@ Item { rotation: -45 } - Rectangle { + BorderSurface { visible: root.warning width: Math.max(7, parent.width * 0.42) height: width @@ -50,8 +51,7 @@ Item { color: root.badgeColor anchors.right: parent.right anchors.bottom: parent.bottom - border.color: Color.popups.background - border.width: 1 + borderSpec: Border.flat(Color.popups.background, 1) Text { anchors.centerIn: parent diff --git a/shell/plugins/polkit/PolkitAgent.qml b/shell/plugins/polkit/PolkitAgent.qml index dc42d235..4cc34f2e 100644 --- a/shell/plugins/polkit/PolkitAgent.qml +++ b/shell/plugins/polkit/PolkitAgent.qml @@ -4,6 +4,7 @@ import Quickshell.Io import Quickshell.Services.Polkit import Quickshell.Wayland import qs.Commons +import qs.Ui import "PolkitModel.js" as PolkitModel Item { @@ -16,6 +17,7 @@ Item { property color foreground: Color.polkit.text property color border: Color.polkit.border property color borderError: Color.polkit.borderError + property var borderSpec: Border.surfaceSpec("polkit", errorFlash ? "border-error" : "border", errorFlash ? borderError : border, Math.max(1, Style.space(2)), "border-alpha") property color scrim: Color.polkit.scrim readonly property int cornerRadius: Style.cornerRadius property int contentMargin: Style.spacing.panelPadding @@ -212,7 +214,7 @@ Item { onClicked: root.refocus() } - Rectangle { + BorderSurface { id: card width: root.cardWidth height: root.cardHeight @@ -220,8 +222,8 @@ Item { anchors.centerIn: parent anchors.horizontalCenterOffset: root.shakeOffset color: root.background - border.color: root.errorFlash ? root.borderError : root.border - border.width: Math.max(1, Style.space(2)) + borderSpec: root.borderSpec + padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: root.refocus() } @@ -245,7 +247,10 @@ Item { Row { id: cardRow anchors.fill: parent - anchors.margins: root.contentMargin + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset spacing: Style.space(14) Text { diff --git a/shell/plugins/reminders/ReminderFlow.qml b/shell/plugins/reminders/ReminderFlow.qml index 779d535f..f8d009ad 100644 --- a/shell/plugins/reminders/ReminderFlow.qml +++ b/shell/plugins/reminders/ReminderFlow.qml @@ -2,6 +2,7 @@ import Quickshell import Quickshell.Wayland import QtQuick import qs.Commons +import qs.Ui import "ReminderFlowModel.js" as ReminderFlowModel Item { @@ -20,6 +21,7 @@ Item { property color background: Color.menu.background property color foreground: Color.menu.text property color border: Color.menu.border + property var borderSpec: Border.surfaceSpec("menu", "border", border, Math.max(1, Style.space(2))) property color scrim: Color.menu.scrim readonly property int cornerRadius: Style.cornerRadius property int contentMargin: Style.spacing.panelPadding @@ -110,15 +112,15 @@ Item { onClicked: root.dismiss() } - Rectangle { + BorderSurface { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent color: root.background - border.color: root.border - border.width: Math.max(1, Style.space(2)) + borderSpec: root.borderSpec + padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: {} } @@ -148,7 +150,10 @@ Item { Item { anchors.fill: parent - anchors.margins: root.contentMargin + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset Text { anchors.left: parent.left diff --git a/shell/plugins/services/media/BarWidget.qml b/shell/plugins/services/media/BarWidget.qml index 3766c0e4..942c8b8a 100644 --- a/shell/plugins/services/media/BarWidget.qml +++ b/shell/plugins/services/media/BarWidget.qml @@ -117,13 +117,12 @@ BarWidget { spacing: Style.space(10) width: parent.width - Rectangle { + BorderSurface { width: Style.space(64) height: Style.space(64) radius: Style.spacing.labelGap color: Style.normalFillFor(root.bar.foreground, Color.accent) - border.color: Style.normalBorderFor(root.bar.foreground, Color.accent) - border.width: Style.normalBorderWidth + borderSpec: Border.controlSpec("normal", root.bar.foreground, Color.accent) Image { anchors.fill: parent @@ -230,7 +229,7 @@ BarWidget { Repeater { model: root.sourcePlayers - Rectangle { + BorderSurface { id: sourceRow required property var modelData @@ -244,16 +243,15 @@ BarWidget { height: sourceInner.implicitHeight + Style.space(10) radius: Style.spacing.labelGap color: selected ? Style.selectedFillFor(root.bar.foreground, Color.accent) : "transparent" - border.color: selected ? Style.selectedBorderFor(root.bar.foreground, Color.accent) : "transparent" - border.width: selected ? Style.normalBorderWidth : 0 + borderSpec: selected ? Border.controlSpec("normal", root.bar.foreground, Color.accent) : Border.none() Row { id: sourceInner anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.space(8) - anchors.rightMargin: Style.space(8) + anchors.leftMargin: sourceRow.borderLeft + Style.space(8) + anchors.rightMargin: sourceRow.borderRight + Style.space(8) spacing: Style.space(8) Text { diff --git a/test/cli b/test/cli index 10755405..82493e28 100755 --- a/test/cli +++ b/test/cli @@ -249,8 +249,10 @@ EOF cat >"$USER_THEMED/gradient-test.txt.tpl" <<'EOF' hypr={{ hypr_gradient hyprland_active_border accent }} shell={{ gradient_start hyprland_active_border accent }} +shell-gradient={{ shell_gradient hyprland_active_border accent }} fallback-hypr={{ hypr_gradient missing accent }} fallback-shell={{ gradient_start missing accent }} +fallback-shell-gradient={{ shell_gradient missing accent }} EOF OMARCHY_PATH="$ROOT" HOME="$PI_TMPDIR" "$ROOT/bin/omarchy-theme-set-templates" @@ -261,8 +263,10 @@ pass "theme template color mix helpers work" grep -qx 'hypr={ colors = { "rgba(010203ee)", "rgba(040506ee)" }, angle = 45 }' "$NEXT_THEME/gradient-test.txt" || fail "theme template hypr_gradient helper works" grep -qx 'shell=#010203' "$NEXT_THEME/gradient-test.txt" || fail "theme template gradient_start helper works" +grep -qx 'shell-gradient=rgba(010203ee) rgba(040506ee) 45deg' "$NEXT_THEME/gradient-test.txt" || fail "theme template shell_gradient helper works" grep -qx 'fallback-hypr="#336699"' "$NEXT_THEME/gradient-test.txt" || fail "theme template hypr_gradient fallback works" grep -qx 'fallback-shell=#336699' "$NEXT_THEME/gradient-test.txt" || fail "theme template gradient_start fallback works" +grep -qx 'fallback-shell-gradient=#336699' "$NEXT_THEME/gradient-test.txt" || fail "theme template shell_gradient fallback works" pass "theme template gradient helpers work" jq -e '.name == "omarchy-system" and .vars.panel == "#0f0f0f" and .vars.border == "#4d4d4d" and .colors.accent == "accent"' "$NEXT_THEME/pi.json" >/dev/null diff --git a/test/shell.d/border-geometry-test.sh b/test/shell.d/border-geometry-test.sh new file mode 100755 index 00000000..65718dd8 --- /dev/null +++ b/test/shell.d/border-geometry-test.sh @@ -0,0 +1,44 @@ +#!/bin/bash +source "$(dirname "$0")/base-test.sh" + +run_node_test <<'JS' +const fs = require('fs') +const vm = require('vm') + +const source = fs.readFileSync(path.join(root, 'shell/Commons/BorderGeometry.js'), 'utf8').replace(/^\.pragma library\n/, '') +const geometry = {} +vm.createContext(geometry) +vm.runInContext(source, geometry) + +assertDeepEqual( + geometry.parseWidthSpec('2 4 6 8', 1), + { top: 2, right: 4, bottom: 6, left: 8 }, + 'border geometry parses four-sided widths' +) + +assertDeepEqual( + geometry.parseWidthSpec('2 4', 1), + { top: 2, right: 4, bottom: 2, left: 4 }, + 'border geometry parses CSS two-value widths' +) + +const gradient = geometry.parseGradientSpec('rgba(010203ee) rgba(040506ee) 45deg', '#336699', 1) +assertEqual(gradient.colors[0], '#010203ee', 'border geometry parses first rgba gradient stop') +assertEqual(gradient.colors[1], '#040506ee', 'border geometry parses second rgba gradient stop') +assertEqual(gradient.angle, 45, 'border geometry parses gradient angle') +assert(gradient.enabled, 'border geometry marks multi-stop gradients enabled') + +assertEqual( + geometry.canonicalColor('0xee33ccff', 1), + '#33ccffee', + 'border geometry converts legacy ARGB color to QML RGBA hex' +) + +const pathData = geometry.ringPath(100, 50, 10, { top: 4, right: 2, bottom: 8, left: 6 }) +assert(pathData.includes('M 10 0'), 'border geometry emits outer rounded path') +assert(pathData.includes('M 10 4'), 'border geometry emits inset inner rounded path') + +const endpoints = geometry.gradientEndpoints(100, 50, 0) +assertEqual(Math.round(endpoints.x1), 0, 'border geometry 0deg starts at left edge') +assertEqual(Math.round(endpoints.x2), 100, 'border geometry 0deg ends at right edge') +JS diff --git a/themes/last-horizon/shell.toml b/themes/last-horizon/shell.toml deleted file mode 100644 index d8adae2a..00000000 --- a/themes/last-horizon/shell.toml +++ /dev/null @@ -1,5 +0,0 @@ -# last-horizon preserves the historical behavior of matching notification -# borders to the theme's Hyprland active-window border (hyprland_active_border -# in colors.toml), rather than the accent. -[notifications] -border = "#8a8588" diff --git a/themes/solitude/shell.toml b/themes/solitude/shell.toml deleted file mode 100644 index ed5d4afc..00000000 --- a/themes/solitude/shell.toml +++ /dev/null @@ -1,5 +0,0 @@ -# solitude preserves the historical behavior of matching notification -# borders to the theme's Hyprland active-window border (hyprland_active_border -# in colors.toml), rather than the accent. -[notifications] -border = "#798186"