From e551e04e8f3f92115751d8e06a5f43ec765eca2e Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 26 Oct 2026 11:18:41 +0800 Subject: [PATCH] [ui] refine desktop shadows and control contrast --- apps/desktop/src/gui/ui/about_panel.slint | 21 +- apps/desktop/src/gui/ui/common.slint | 149 ++++++---- .../src/gui/ui/device_group_dialog.slint | 28 +- .../gui/ui/file_transfer_notification.slint | 9 +- .../src/gui/ui/gallery_menu_button.slint | 27 +- .../src/gui/ui/gallery_network_dialog.slint | 7 +- apps/desktop/src/gui/ui/gallery_view.slint | 93 +++--- apps/desktop/src/gui/ui/group_filter.slint | 49 ++-- apps/desktop/src/gui/ui/main_window.slint | 272 +++++++++--------- apps/desktop/src/gui/ui/message_panel.slint | 24 +- .../src/gui/ui/rounded_line_edit.slint | 27 +- apps/desktop/src/gui/ui/server_window.slint | 27 +- .../src/gui/ui/settings_controls.slint | 32 ++- apps/desktop/src/gui/ui/stream_window.slint | 91 +++--- apps/desktop/src/gui/ui/ui_shadow.slint | 63 ++++ .../src/gui/ui/workbench_controls.slint | 50 ++-- 16 files changed, 557 insertions(+), 412 deletions(-) create mode 100644 apps/desktop/src/gui/ui/ui_shadow.slint diff --git a/apps/desktop/src/gui/ui/about_panel.slint b/apps/desktop/src/gui/ui/about_panel.slint index 32890ed7..9f87328e 100644 --- a/apps/desktop/src/gui/ui/about_panel.slint +++ b/apps/desktop/src/gui/ui/about_panel.slint @@ -1,9 +1,10 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { ScrollView, TextEdit } from "std-widgets.slint"; import { SettingsDialog, SettingsButton } from "settings_controls.slint"; import { WorkbenchStyle } from "workbench_controls.slint"; import { UiRadius } from "ui_radius.slint"; import { GalleryStrings } from "device_strings.slint"; -import { FontAwesomeIcons } from "common.slint"; +import { FontAwesomeIcons, ControlStyle } from "common.slint"; import "../assets/fonts/message_spacing.ttf"; export enum AboutPage { overview, components, component, document, source } @@ -26,11 +27,11 @@ export global AboutStrings { out property back: GalleryStrings.tr("返回", "Back", "Назад"); } -export component AboutNavigationGroup inherits Rectangle { +export component AboutNavigationGroup inherits ShadowSurface { height: rows.preferred-height + 2px; background: WorkbenchStyle.surface; border-radius: UiRadius.control; - border-width: 1px; border-color: WorkbenchStyle.separator; + shadow-color: UiShadow.edge; clip: true; rows := VerticalLayout { x: 1px; y: 1px; width: parent.width - 2px; @@ -41,13 +42,13 @@ export component AboutNavigationGroup inherits Rectangle { export component AboutRowSeparator inherits Rectangle { height: 1px; - Rectangle { + ShadowSeparator { x: 14px; width: parent.width - 30px; height: 1px; - background: WorkbenchStyle.separator; + shadow-color: UiShadow.separator; } } -export component AboutNavigationRow inherits Rectangle { +export component AboutNavigationRow inherits ShadowSurface { in property title; in property subtitle; in property icon; @@ -60,7 +61,7 @@ export component AboutNavigationRow inherits Rectangle { background: touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface; border-radius: root.grouped ? 0px : UiRadius.control; - border-width: root.grouped ? 0px : 1px; border-color: WorkbenchStyle.separator; + shadow-enabled: !root.grouped; shadow-color: UiShadow.edge; accessible-role: button; accessible-label: root.title + (root.subtitle != "" ? ", " + root.subtitle : "") + (root.detail != "" ? ", " + root.detail : ""); @@ -69,10 +70,10 @@ export component AboutNavigationRow inherits Rectangle { x: 12px; y: (parent.height - self.height) / 2; width: 28px; height: 28px; border-radius: UiRadius.control; - background: WorkbenchStyle.field-surface; + background: ControlStyle.surface; Text { text: root.icon; font-family: "Font Awesome 6 Free"; font-weight: 900; - font-size: 13px; color: WorkbenchStyle.muted; + font-size: 13px; color: ControlStyle.icon; horizontal-alignment: center; vertical-alignment: center; accessible-role: none; } @@ -226,7 +227,7 @@ export component AboutPanel inherits SettingsDialog { footer := VerticalLayout { x: 24px; y: parent.height - self.height - 24px; width: parent.width - 48px; height: self.preferred-height; spacing: 12px; - Rectangle { height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } HorizontalLayout { alignment: end; SettingsButton { text: AboutStrings.back; clicked => { root.back(); } } diff --git a/apps/desktop/src/gui/ui/common.slint b/apps/desktop/src/gui/ui/common.slint index b32bdd22..115f468b 100644 --- a/apps/desktop/src/gui/ui/common.slint +++ b/apps/desktop/src/gui/ui/common.slint @@ -1,17 +1,28 @@ +import { ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; -// The legacy client uses ImGui::StyleColorsLight(). Keep the structural line -// colors in one place so the Slint renderer produces the same visual weight. -export global ImGuiLineStyle { - out property border: #0000004d; - out property separator: #6363639e; - out property divider: #0000007a; - out property control-separator: #b2b2b2; -} - export global FloatingBarStyle { out property corner-radius: UiRadius.menu; } +// Filled controls remain distinct from white cards without restoring outlines. +export global ControlStyle { + out property surface: #dce6f3; + out property hover: #cedef1; + out property pressed: #bfd2ec; + out property selected: #bcd4f7; + out property selected-hover: #adcaf1; + out property selected-pressed: #99bce8; + out property selected-text: #1d4e9e; + out property disabled: #f0f2f5; + out property icon: #52657f; + out property input-surface: #dee7f2; + out property input-focused: #e5ecf5; + out property input-placeholder: #566a84; + // These tints over white match the field fills for standard LineEdit. + out property input-tint: #235eaa26; + out property input-focus-tint: #235eaa1e; +} + // ImGui loads a 32px font atlas and applies per-window font scales. These // logical sizes reproduce those visual tiers on Slint's HiDPI renderer. export global ImGuiFontStyle { @@ -69,10 +80,10 @@ export component IconButton inherits Rectangle { in property enabled: true; in property danger: false; in property icon-size: 16px; - in property icon-color: #24272d; + in property icon-color: ControlStyle.icon; in property normal-background: transparent; - in property hover-background: #edf0f5; - in property pressed-background: #d9dde5; + in property hover-background: ControlStyle.hover; + in property pressed-background: ControlStyle.pressed; in property slashed: false; callback clicked; @@ -138,8 +149,8 @@ export component ImageButton inherits Rectangle { width: 30px; height: 30px; - background: touch.pressed ? (danger ? #d92d20 : #d9dde5) - : touch.has-hover ? (danger ? #f04438 : #edf0f5) + background: touch.pressed ? (danger ? #d92d20 : ControlStyle.pressed) + : touch.has-hover ? (danger ? #f04438 : ControlStyle.hover) : transparent; border-radius: UiRadius.control; @@ -174,14 +185,15 @@ export component ImageButton inherits Rectangle { } } -export component CompactComboBox inherits Rectangle { +export component CompactComboBox inherits ShadowSurface { + shadow-compact: true; in property <[string]> model; in-out property current-index: 0; in property enabled: true; in property subtle: false; in property show-arrow: true; - in property normal-border-color: root.subtle ? #dfe6ef : #bfc2c7; - in property hover-border-color: root.normal-border-color; + in property normal-shadow-color: UiShadow.edge; + in property hover-shadow-color: UiShadow.hover; property popup-animation-enabled: false; property popup-progress: 0.0; callback selected(int); @@ -205,9 +217,10 @@ export component CompactComboBox inherits Rectangle { width: 73px; height: 24px; - background: root.enabled ? (root.subtle ? #f8fafc : white) : root.subtle ? #f1f3f6 : #f1f1f1; - border-width: 1px; - border-color: root.enabled && combo-touch.has-hover ? root.hover-border-color : root.normal-border-color; + background: !root.enabled ? ControlStyle.disabled + : combo-touch.pressed || combo-popup.is-open ? ControlStyle.pressed + : combo-touch.has-hover ? ControlStyle.hover : ControlStyle.surface; + shadow-color: root.enabled && combo-touch.has-hover ? root.hover-shadow-color : root.normal-shadow-color; border-radius: UiRadius.control; Text { @@ -224,12 +237,12 @@ export component CompactComboBox inherits Rectangle { width: 20px; height: parent.height - 2px; y: 1px; - background: root.subtle ? transparent : root.enabled ? #9bc9f8 : #d7d9dc; + background: root.subtle ? transparent : root.enabled ? ControlStyle.selected : #d7d9dc; border-top-right-radius: UiRadius.control - 1px; border-bottom-right-radius: UiRadius.control - 1px; Text { text: FontAwesomeIcons.angle-down; - color: root.subtle ? #79879a : #202124; + color: root.subtle ? ControlStyle.icon : #202124; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px; @@ -274,7 +287,7 @@ export component CompactComboBox inherits Rectangle { height: root.model.length * 23px; close-policy: close-on-click-outside; - Rectangle { + ShadowSurface { // Keep the reveal anchored to the popup's top edge while the // content settles downward into its final position. y: (1.0 - root.popup-progress) * -8px; @@ -282,16 +295,22 @@ export component CompactComboBox inherits Rectangle { height: parent.height * root.popup-progress; opacity: root.popup-progress; background: white; - border-width: 1px; - border-color: root.normal-border-color; + shadow-color: root.normal-shadow-color; border-radius: UiRadius.menu; clip: true; for choice[index] in root.model: Rectangle { y: index * 23px; width: parent.width; height: 23px; - background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white; - Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } + background: index == root.current-index + ? (option-touch.pressed ? ControlStyle.selected-pressed : option-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) + : option-touch.pressed ? ControlStyle.pressed : option-touch.has-hover ? ControlStyle.hover : white; + Text { + x: 5px; width: parent.width - 10px; text: choice; + color: index == root.current-index ? ControlStyle.selected-text : #202124; + font-weight: index == root.current-index ? 600 : 400; + font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; + } option-touch := TouchArea { enabled: root.enabled; clicked => { @@ -306,12 +325,12 @@ export component CompactComboBox inherits Rectangle { } } } - Rectangle { + ShadowSurface { width: parent.width; height: parent.height; background: transparent; - border-width: 1px; - border-color: root.normal-border-color; + clip: true; + shadow-color: root.normal-shadow-color; border-radius: UiRadius.menu; z: 2; } @@ -319,19 +338,21 @@ export component CompactComboBox inherits Rectangle { } } -export component ImGuiCheckBox inherits Rectangle { +export component ImGuiCheckBox inherits ShadowSurface { + shadow-compact: true; in-out property checked; in property enabled: true; width: 21px; height: 21px; - background: root.enabled ? white : #f1f1f1; - border-width: 1px; - border-color: check-touch.has-hover && root.enabled ? #8cbef2 : #b9bdc2; + background: !root.enabled ? ControlStyle.disabled + : root.checked ? (check-touch.pressed ? #254cc0 : check-touch.has-hover ? #4574f0 : #3564e3) + : check-touch.pressed ? ControlStyle.pressed : check-touch.has-hover ? ControlStyle.hover : ControlStyle.surface; + shadow-color: check-touch.has-hover && root.enabled ? UiShadow.focus : UiShadow.edge; border-radius: 4px; if root.checked: Text { text: FontAwesomeIcons.check; - color: root.enabled ? #2e86de : #8b8f95; + color: root.enabled ? white : #8b8f95; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 16px; @@ -368,7 +389,8 @@ export component MiniToggleSwitch inherits Rectangle { } } -export component CompactButton inherits Rectangle { +export component CompactButton inherits ShadowSurface { + shadow-compact: true; in property text; in property text-size: ImGuiFontStyle.body; in property primary: false; @@ -378,12 +400,11 @@ export component CompactButton inherits Rectangle { width: 34px; height: 24px; - background: !root.enabled ? (root.quiet ? transparent : #eceef0) - : button-touch.pressed ? (root.primary ? #82b8f2 : #d8dadd) - : button-touch.has-hover ? (root.primary ? #b8d9fb : #f0f1f2) - : root.primary ? #a8d0fa : root.quiet ? transparent : #e2e4e7; - border-width: 1px; - border-color: root.primary ? #9bc3ed : root.quiet ? transparent : #c8cbd0; + background: !root.enabled ? (root.quiet ? transparent : ControlStyle.disabled) + : button-touch.pressed ? (root.primary ? #82b8f2 : ControlStyle.pressed) + : button-touch.has-hover ? (root.primary ? #b8d9fb : ControlStyle.hover) + : root.primary ? #a8d0fa : root.quiet ? transparent : ControlStyle.surface; + shadow-color: root.primary ? UiShadow.focus : root.quiet ? transparent : UiShadow.edge; border-radius: UiRadius.control; Text { text: root.text; @@ -396,24 +417,29 @@ export component CompactButton inherits Rectangle { button-touch := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } } } -export component PrimaryButton inherits Rectangle { +export component PrimaryButton inherits ShadowSurface { in property text; - in property text-size: ImGuiFontStyle.body; + in property text-size: 12px; in property enabled: true; callback clicked; min-width: 72px; - height: 30px; + height: 32px; background: !root.enabled ? #b8c1d1 - : touch.pressed ? #1849a9 - : touch.has-hover ? #2970d6 - : #2463c7; + : touch.pressed ? #254cc0 + : touch.has-hover ? #4574f0 + : #3564e3; + shadow-compact: true; + shadow-enabled: root.enabled && !touch.pressed; + shadow-color: UiShadow.focus; border-radius: UiRadius.control; + animate background { duration: 100ms; } Text { text: root.text; color: white; font-size: root.text-size; + font-weight: 600; horizontal-alignment: center; vertical-alignment: center; } @@ -424,26 +450,28 @@ export component PrimaryButton inherits Rectangle { } } -export component SecondaryButton inherits Rectangle { +export component SecondaryButton inherits ShadowSurface { in property text; - in property text-size: ImGuiFontStyle.body; + in property text-size: 12px; in property enabled: true; callback clicked; min-width: 72px; - height: 30px; - background: !root.enabled ? #f1f2f4 - : touch.pressed ? #dfe3e9 - : touch.has-hover ? #f3f4f6 : white; - border-width: 1px; - border-color: #c9ced7; + height: 32px; + background: !root.enabled ? ControlStyle.disabled + : touch.pressed ? ControlStyle.pressed + : touch.has-hover ? ControlStyle.hover : ControlStyle.surface; + shadow-color: UiShadow.edge; + shadow-compact: true; + shadow-enabled: root.enabled && !touch.pressed; border-radius: UiRadius.control; - clip: true; + animate background { duration: 100ms; } Text { text: root.text; - color: root.enabled ? #24272d : #949aa5; + color: root.enabled ? #303f55 : #949aa5; font-size: root.text-size; + font-weight: 500; horizontal-alignment: center; vertical-alignment: center; } @@ -495,9 +523,8 @@ export component SectionTitle inherits Text { vertical-alignment: center; } -export component DialogSurface inherits Rectangle { +export component DialogSurface inherits ShadowSurface { background: white; - border-width: 1px; - border-color: ImGuiLineStyle.border; + shadow-color: UiShadow.edge; border-radius: UiRadius.surface; } diff --git a/apps/desktop/src/gui/ui/device_group_dialog.slint b/apps/desktop/src/gui/ui/device_group_dialog.slint index 8eb54586..7d2d461a 100644 --- a/apps/desktop/src/gui/ui/device_group_dialog.slint +++ b/apps/desktop/src/gui/ui/device_group_dialog.slint @@ -1,7 +1,8 @@ +import { ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { ScrollView } from "std-widgets.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint"; -import { CompactButton, ImGuiCheckBox } from "common.slint"; +import { CompactButton, ImGuiCheckBox, ControlStyle } from "common.slint"; import { GalleryStrings } from "device_strings.slint"; export struct DeviceGroupOption { @@ -20,13 +21,13 @@ export component DeviceGroupDialog inherits Rectangle { callback cancel(); background: #00000040; TouchArea {} - dialog := Rectangle { + dialog := ShadowSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(360px, parent.width - 40px); height: 208px + group-list.height + (root.error == "" ? 0px : 32px); background: white; border-radius: UiRadius.surface; - border-width: 1px; border-color: #b2b2b2; + shadow-color: UiShadow.edge; TouchArea {} Text { x: 18px; y: 16px; height: 24px; @@ -41,10 +42,10 @@ export component DeviceGroupDialog inherits Rectangle { x: 18px; y: 67px; width: parent.width - 36px; height: 16px; text: "ID " + root.device-id; color: #858c97; font-size: 11px; } - group-list := Rectangle { + group-list := ShadowSurface { x: 18px; y: 95px; width: parent.width - 36px; height: min(5, max(1, root.options.length)) * 32px + 2px; - background: #fbfcfd; border-width: 1px; border-color: #e1e5eb; border-radius: UiRadius.surface; + background: #fbfcfd; shadow-color: UiShadow.edge; border-radius: UiRadius.surface; clip: true; ScrollView { x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px; @@ -52,16 +53,21 @@ export component DeviceGroupDialog inherits Rectangle { VerticalLayout { spacing: 0px; for option[index] in root.options: Rectangle { + private property selected: option.selected && (option.name != "" || root.new-group == ""); height: 32px; - background: row-touch.has-hover ? #eef4fc : transparent; + background: self.selected + ? (row-touch.pressed ? ControlStyle.selected-pressed : row-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) + : row-touch.pressed ? ControlStyle.pressed : row-touch.has-hover ? ControlStyle.hover : transparent; ImGuiCheckBox { x: 9px; y: 6px; width: 20px; height: 20px; - checked: option.selected && (option.name != "" || root.new-group == ""); + checked: parent.selected; } Text { x: 39px; width: parent.width - 48px; height: parent.height; text: option.name == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : option.name; - color: #30343b; font-size: 12px; overflow: elide; vertical-alignment: center; + color: parent.selected ? ControlStyle.selected-text : #30343b; + font-weight: parent.selected ? 600 : 400; + font-size: 12px; overflow: elide; vertical-alignment: center; } row-touch := TouchArea { clicked => { root.toggle-group(option.name); } } } @@ -75,10 +81,10 @@ export component DeviceGroupDialog inherits Rectangle { border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface; border-color: white; } - Rectangle { + ShadowSurface { width: parent.width; height: parent.height; - border-width: 1px; border-radius: UiRadius.surface; - border-color: #e1e5eb; + border-radius: UiRadius.surface; + shadow-color: UiShadow.edge; } } RoundedLineEdit { diff --git a/apps/desktop/src/gui/ui/file_transfer_notification.slint b/apps/desktop/src/gui/ui/file_transfer_notification.slint index 1f47f702..ab505047 100644 --- a/apps/desktop/src/gui/ui/file_transfer_notification.slint +++ b/apps/desktop/src/gui/ui/file_transfer_notification.slint @@ -1,3 +1,4 @@ +import { ShadowSeparator, UiShadow } from "ui_shadow.slint"; import { FileTransferEntry } from "stream_window.slint"; import { DialogSurface, FontAwesomeIcons, IconButton, ImGuiFontStyle } from "common.slint"; import { WorkbenchStyle, WorkbenchButton } from "workbench_controls.slint"; @@ -26,7 +27,7 @@ export component FileTransferNotification inherits Window { DialogSurface { width: parent.width; height: parent.height; background: WorkbenchStyle.surface; - border-color: WorkbenchStyle.separator; + shadow-color: UiShadow.edge; Text { x: 16px; y: 8px; width: parent.width - 64px - (root.remote-id == "" ? 0px : 108px); @@ -50,9 +51,9 @@ export component FileTransferNotification inherits Window { pressed-background: WorkbenchStyle.pressed-surface; clicked => { root.dismissed(); } } - Rectangle { + ShadowSeparator { x: 16px; y: 40px; width: parent.width - 32px; height: 1px; - background: WorkbenchStyle.separator; + shadow-color: UiShadow.separator; } Text { x: 16px; y: 47px; width: parent.width - 32px; height: 18px; @@ -96,7 +97,7 @@ export component FileTransferNotification inherits Window { text: root.cancel-text; text-size: ImGuiFontStyle.base; text-color: WorkbenchStyle.muted; - normal-border-color: WorkbenchStyle.border; + normal-shadow-color: UiShadow.edge; clicked => { root.cancel-requested(); } } } diff --git a/apps/desktop/src/gui/ui/gallery_menu_button.slint b/apps/desktop/src/gui/ui/gallery_menu_button.slint index 30673f8e..40d1f304 100644 --- a/apps/desktop/src/gui/ui/gallery_menu_button.slint +++ b/apps/desktop/src/gui/ui/gallery_menu_button.slint @@ -1,5 +1,6 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; -import { CompactButton, FontAwesomeIcons } from "common.slint"; +import { CompactButton, FontAwesomeIcons, ControlStyle } from "common.slint"; export struct GalleryMenuItem { label: string, enabled: bool } @@ -88,12 +89,11 @@ export component GalleryMenuButton inherits Rectangle { color: root.enabled ? #62666d : #8e9298; horizontal-alignment: center; vertical-alignment: center; } - if root.compact-trigger: Rectangle { - background: more-touch.pressed || popup.is-open ? #e5edfc : more-touch.has-hover ? white : #fffffff0; + if root.compact-trigger: ShadowSurface { + background: more-touch.pressed || popup.is-open ? ControlStyle.selected : more-touch.has-hover ? ControlStyle.hover : white; border-radius: UiRadius.menu; - border-width: 1px; - border-color: trigger-focus.has-focus ? #3564e3 : #ffffff80; - animate background, border-color { duration: 120ms; } + shadow-color: trigger-focus.has-focus ? UiShadow.focus : UiShadow.edge; + animate background, shadow-color { duration: 120ms; } for dot in 3: Rectangle { x: parent.width / 2 - 6px + dot * 5px; y: (parent.height - self.height) / 2; @@ -117,11 +117,12 @@ export component GalleryMenuButton inherits Rectangle { forward-focus: popup-focus; popup-focus := FocusScope { key-pressed(event) => { return root.handle-key(event); } - Rectangle { - background: white; border-width: 1px; border-color: #c9cdd2; border-radius: UiRadius.menu; - if root.destructive-index > 0: Rectangle { + ShadowSurface { + background: white; shadow-color: UiShadow.edge; border-radius: UiRadius.menu; + clip: true; + if root.destructive-index > 0: ShadowSeparator { x: 10px; y: root.menu-padding + root.destructive-index * root.row-height + (root.divider-spacing - 1px) / 2; - width: parent.width - 20px; height: 1px; background: #e8edf4; + width: parent.width - 20px; height: 1px; shadow-color: UiShadow.separator; } for item[index] in root.items: Rectangle { private property destructive: index == root.destructive-index; @@ -132,7 +133,9 @@ export component GalleryMenuButton inherits Rectangle { + (root.destructive-index > 0 && index >= root.destructive-index ? root.divider-spacing : 0px); width: parent.width - root.menu-padding * 2; height: root.row-height; border-radius: UiRadius.control; - background: item.enabled && index == root.current-index ? (self.destructive ? #fff0f0 : #edf4fc) : transparent; + background: item.enabled && index == root.current-index + ? (self.destructive ? (item-touch.pressed ? #f6babb : #fbe0e0) + : item-touch.pressed ? ControlStyle.pressed : ControlStyle.hover) : transparent; accessible-role: button; accessible-label: item.label; accessible-enabled: item.enabled; accessible-action-default => { root.choose(index); } if self.has-icon: Text { @@ -148,7 +151,7 @@ export component GalleryMenuButton inherits Rectangle { text: item.label; color: parent.foreground; font-size: root.compact-menu ? 11px : 12px; vertical-alignment: center; } - TouchArea { + item-touch := TouchArea { enabled: item.enabled; mouse-cursor: pointer; changed has-hover => { if self.has-hover { root.current-index = index; } } diff --git a/apps/desktop/src/gui/ui/gallery_network_dialog.slint b/apps/desktop/src/gui/ui/gallery_network_dialog.slint index 16bd52f1..0392af45 100644 --- a/apps/desktop/src/gui/ui/gallery_network_dialog.slint +++ b/apps/desktop/src/gui/ui/gallery_network_dialog.slint @@ -1,3 +1,4 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { GalleryStrings } from "device_strings.slint"; import { CompactButton } from "common.slint"; @@ -190,13 +191,13 @@ export component GalleryNetworkDialog inherits Rectangle { callback close(); background: #00000042; TouchArea { clicked => { root.close(); } } - Rectangle { + ShadowSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(760px, parent.width - 32px); height: min(120px + root.list-height, parent.height - 32px); background: white; border-radius: UiRadius.surface; - border-width: 1px; border-color: #c6cdd5; + shadow-color: UiShadow.edge; TouchArea {} FocusScope { init => { self.focus(); } @@ -237,7 +238,7 @@ export component GalleryNetworkDialog inherits Rectangle { for device[index] in root.devices: NetworkRow { y: index * root.row-height; width: list.viewport-width; height: root.row-height; device: device; background: mod(index, 2) == 0 ? white : #f8fafc; - Rectangle { y: parent.height - 1px; height: 1px; background: #edf0f4; } + ShadowSeparator { y: parent.height - 1px; height: 1px; shadow-color: UiShadow.separator; } } } if root.devices.length == 0: NetworkEmptyState { diff --git a/apps/desktop/src/gui/ui/gallery_view.slint b/apps/desktop/src/gui/ui/gallery_view.slint index e9355c4d..a5374321 100644 --- a/apps/desktop/src/gui/ui/gallery_view.slint +++ b/apps/desktop/src/gui/ui/gallery_view.slint @@ -1,3 +1,4 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { GalleryStrings } from "device_strings.slint"; import { RemoteCursor } from "remote_cursor.slint"; @@ -6,7 +7,7 @@ import { GalleryMenuButton } from "gallery_menu_button.slint"; import { GroupFilter } from "group_filter.slint"; import { ScrollView } from "std-widgets.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint"; -import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint"; +import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox, ControlStyle } from "common.slint"; component GalleryButton inherits CompactButton { border-radius: UiRadius.control; @@ -473,7 +474,7 @@ export component GalleryView inherits Rectangle { height: root.card-height; opacity: root.dragging && root.drag-source == tile.id ? 0.45 : 1; background: !tile.online ? DeviceCardStyle.offline-background - : tile.selected ? #eaf3ff : root.batch && tile-touch.has-hover ? #f3f8fe : white; + : tile.selected ? ControlStyle.selected : root.batch && tile-touch.has-hover ? ControlStyle.hover : white; Rectangle { x: root.card-inset; y: self.x; width: parent.width - self.x * 2; @@ -491,12 +492,13 @@ export component GalleryView inherits Rectangle { width: parent.width - self.x - (tile-touch.has-hover && !root.batch ? DeviceCardStyle.toolbar-width + 12px : root.batch ? 35px : 9px); height: 24px; text: tile.name; color: tile.online ? #24272d : DeviceCardStyle.offline-text; font-size: 12px; overflow: elide; vertical-alignment: center; } - if root.batch && !root.simplified: Rectangle { + if root.batch && !root.simplified: ShadowSurface { x: parent.width - 27px; y: parent.height - 27px; - width: 18px; height: 18px; border-radius: 4px; border-width: 1px; - background: tile.selected ? (tile-touch.pressed ? #5c9fdf : tile-touch.has-hover ? #659fe0 : #6dabea) : tile-touch.has-hover ? #f3f8fe : white; - border-color: tile.selected ? #6dabea : tile-touch.has-hover ? #76aeea : #b7bec8; - animate background, border-color { duration: 120ms; } + width: 18px; height: 18px; border-radius: 4px; + background: tile.selected ? (tile-touch.pressed ? #254cc0 : tile-touch.has-hover ? #4574f0 : #3564e3) + : tile-touch.pressed ? ControlStyle.pressed : tile-touch.has-hover ? ControlStyle.hover : ControlStyle.surface; + shadow-color: tile.selected ? UiShadow.focus : tile-touch.has-hover ? UiShadow.hover : UiShadow.edge; + animate background, shadow-color { duration: 120ms; } if tile.selected: Text { text: FontAwesomeIcons.check; color: white; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 11px; @@ -531,27 +533,27 @@ export component GalleryView inherits Rectangle { } } // Draw each table line once, continuously across occupied and empty cells. - for column in root.columns + 1: Rectangle { + for column in root.columns + 1: ShadowSeparator { visible: root.count > 0; x: min(column * root.card-width, grid.table-width - 1px); y: 0px; width: 1px; height: grid.table-height; - background: root.resizing || (grid.has-hover && grid.edge-x != 0 && grid.column-line == column) - ? #76aeea : #c9cdd2; + vertical: true; shadow-color: root.resizing || (grid.has-hover && grid.edge-x != 0 && grid.column-line == column) + ? UiShadow.focus : UiShadow.separator; } - for row in root.rows + 1: Rectangle { + for row in root.rows + 1: ShadowSeparator { visible: root.count > 0; x: 0px; y: min(row * root.card-height, grid.table-height - 1px); width: grid.table-width; height: 1px; - background: root.resizing || (grid.has-hover && grid.edge-y != 0 && grid.row-line == row) - ? #76aeea : #c9cdd2; + shadow-color: root.resizing || (grid.has-hover && grid.edge-y != 0 && grid.row-line == row) + ? UiShadow.focus : UiShadow.separator; } - if root.drag-target >= 0: Rectangle { + if root.drag-target >= 0: ShadowSurface { x: mod(root.drag-target, root.columns) * root.card-width + 1px; y: floor(root.drag-target / root.columns) * root.card-height + 1px; width: root.card-width - 2px; height: root.card-height - 2px; - background: #76aeea26; border-width: 2px; border-color: #4a94de; + background: #76aeea26; shadow-color: UiShadow.focus; } if root.count == 0: Text { text: root.simplified @@ -597,11 +599,11 @@ export component GalleryView inherits Rectangle { } GalleryButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.tr("释放操控", "Release", "Отпустить") : GalleryStrings.tr("接管操控", "Control", "Управлять")) : GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } } } - Rectangle { + ShadowSurface { x: 0px; y: rail.visible ? focus-divider.y + focus-divider.height : 36px; width: parent.width; height: parent.height - self.y; background: root.focused.online ? #eff1f4 : DeviceCardStyle.offline-background; border-radius: UiRadius.surface; - border-width: root.controlling ? 2px : 1px; border-color: root.controlling ? #76aeea : root.focused.online ? #d5dbe3 : DeviceCardStyle.offline-border; + shadow-color: root.controlling ? UiShadow.focus : UiShadow.edge; video := Rectangle { x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px; background: root.focused.online ? #24272d : DeviceCardStyle.offline-preview; @@ -668,12 +670,12 @@ export component GalleryView inherits Rectangle { // Collapse the focused tile and its gap without changing the page model. width: tile.id == root.focused.id ? 0px : root.rail-slot-width; height: root.rail-card-height; visible: tile.id != root.focused.id; - rail-card := Rectangle { + rail-card := ShadowSurface { private property presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети") : tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно"); private property details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence; width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: UiRadius.surface; - background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; border-width: 1px; border-color: tile.online ? #d5dbe3 : DeviceCardStyle.offline-border; + background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; shadow-color: UiShadow.edge; accessible-role: button; accessible-label: self.details; accessible-action-default => { root.action("focus", tile.id); } @@ -714,9 +716,9 @@ export component GalleryView inherits Rectangle { visible: root.focus-rail-visible; x: 0px; y: rail.y + rail.height; width: parent.width; height: 12px; private property highlighted: divider-touch.has-hover || root.focus-resizing || divider-focus.has-focus; - Rectangle { + ShadowSeparator { x: 0px; y: 5px; width: parent.width; height: 1px; - background: focus-divider.highlighted ? #a6c9ed : #e6e9ed; + shadow-color: focus-divider.highlighted ? UiShadow.focus : UiShadow.separator; } Rectangle { x: (parent.width - self.width) / 2; y: 4px; width: 36px; height: 3px; border-radius: 1.5px; @@ -775,7 +777,7 @@ export component GalleryView inherits Rectangle { } Rectangle { y: parent.height - 44px; height: 44px; background: #fafbfc; - Rectangle { y: 0px; width: parent.width; height: 1px; background: #e1e4e8; } + ShadowSeparator { y: 0px; width: parent.width; height: 1px; shadow-color: UiShadow.separator; } } // Connection status and pagination remain available in every selection state. footer := Rectangle { @@ -793,7 +795,7 @@ export component GalleryView inherits Rectangle { network-button := SecondaryButton { x: connection-count.x + connection-count.width + 12px; min-width: 64px; width: 64px; height: parent.height; - border-color: #c8cbd0; border-radius: UiRadius.control; + shadow-color: UiShadow.edge; border-radius: UiRadius.control; text: GalleryStrings.tr("网络状态", "Network", "Сеть"); accessible-role: button; accessible-label: self.text; @@ -823,6 +825,7 @@ export component GalleryView inherits Rectangle { IconButton { width: 24px; height: 24px; border-radius: UiRadius.control; + normal-background: ControlStyle.surface; icon: root.fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; tooltip: root.fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран"); clicked => { root.toggle-fullscreen(); } @@ -840,11 +843,11 @@ export component GalleryView inherits Rectangle { horizontal-alignment: center; vertical-alignment: center; } } - if root.dragging: Rectangle { + if root.dragging: ShadowSurface { x: clamp(grid.x + root.drag-x + 14px, 8px, parent.width - self.width - 8px); y: clamp(grid.y + root.drag-y + 14px, 8px, footer.y - self.height - 12px); width: 224px; height: 54px; z: 10; - background: #fffffff2; border-width: 1px; border-color: #76aeea; border-radius: UiRadius.surface; + background: #fffffff2; shadow-color: UiShadow.focus; border-radius: UiRadius.surface; Rectangle { x: 8px; y: 9px; width: 64px; height: 36px; background: root.dragged-tile.online ? #24272d : DeviceCardStyle.offline-preview; @@ -862,20 +865,20 @@ export component GalleryView inherits Rectangle { font-size: 10px; color: #527ca8; overflow: elide; } } - if root.swap-save-failed: Rectangle { + if root.swap-save-failed: ShadowSurface { x: (parent.width - self.width) / 2; y: footer.y - self.height - 14px; width: min(420px, parent.width - 32px); height: 36px; z: 10; - background: #fff2f0; border-width: 1px; border-color: #e9b7b0; border-radius: UiRadius.surface; + background: #fff2f0; shadow-color: UiShadow.edge; border-radius: UiRadius.surface; Text { x: 12px; y: 0px; width: parent.width - 24px; height: parent.height; font-size: 12px; color: #9d4339; vertical-alignment: center; text: GalleryStrings.tr("换位未保存,请重试", "Couldn't save positions. Please try again.", "Не удалось сохранить порядок. Повторите попытку."); overflow: elide; } } // Device details are rendered by MainWindow; resize hints stay local. - if root.device-tooltip-visible && (root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus"): Rectangle { + if root.device-tooltip-visible && (root.device-tooltip-id == "resize-grid" || root.device-tooltip-id == "resize-focus"): ShadowSurface { x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px); y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px); width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px; - background: #22252bf2; border-width: 1px; border-color: #ffffff38; border-radius: UiRadius.control; + background: #22252bf2; shadow-color: UiShadow.edge; border-radius: UiRadius.control; device-tooltip-label := Text { x: 8px; y: 8px; width: parent.width - 16px; text: root.device-tooltip-text; color: white; font-size: 11px; wrap: word-wrap; @@ -884,9 +887,9 @@ export component GalleryView inherits Rectangle { if root.adding: Rectangle { background: #00000042; TouchArea {} - add-dialog := Rectangle { + add-dialog := ShadowSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 400px; height: 306px; - background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2; + background: white; border-radius: UiRadius.surface; shadow-color: UiShadow.edge; function submit() { if root.add-device(device-id.text, password.text, group.text, remember.checked) { password.text = ""; @@ -931,25 +934,27 @@ export component GalleryView inherits Rectangle { if root.importing: Rectangle { background: #00000042; TouchArea {} - Rectangle { + ShadowSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px; - background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2; + background: white; border-radius: UiRadius.surface; shadow-color: UiShadow.edge; Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("从最近连接添加", "Add from recent connections", "Добавить из недавних"); font-size: 16px; } Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.tr("加入桌面墙后,可通过批量选择连接", "Add devices, then use Select to connect them.", "После добавления подключайтесь через выбор."); color: #62666d; font-size: 11px; } ImGuiCheckBox { x: 18px; y: 74px; enabled: root.recent-available > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-available; } Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.tr("全选", "Select all", "Выбрать все"); color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; } TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-available > 0; clicked => { root.action("recent-all", ""); } } Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.tr("待添加 ", "To add ", "К добавлению ") + root.recent-selected + " / " + root.recent-available; horizontal-alignment: right; vertical-alignment: center; font-size: 12px; color: #62666d; } - Rectangle { + ShadowSurface { x: 18px; y: 102px; width: 404px; height: 160px; - border-width: 1px; border-color: #d8dadd; border-radius: UiRadius.surface; clip: true; + shadow-color: UiShadow.edge; border-radius: UiRadius.surface; clip: true; recent-list := ScrollView { x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px; viewport-width: self.width - 12px; viewport-height: max(self.height, root.recent-devices.length * 44px); for device[index] in root.recent-devices: Rectangle { y: index * 44px; width: recent-list.viewport-width; height: 44px; - background: device.added ? #f1f2f4 : device.selected ? #edf5fe : row-touch.has-hover ? #f4f5f6 : white; + background: device.added ? ControlStyle.disabled + : device.selected ? (row-touch.pressed ? ControlStyle.selected-pressed : row-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) + : row-touch.pressed ? ControlStyle.pressed : row-touch.has-hover ? ControlStyle.hover : white; ImGuiCheckBox { x: 8px; y: 11px; enabled: !device.added; checked: device.added || device.selected; } Text { x: 36px; y: 3px; width: parent.width - 126px; height: 20px; text: device.name; color: device.added ? #92969e : #24272d; font-size: 12px; overflow: elide; vertical-alignment: center; } Text { x: 36px; y: 23px; width: parent.width - 126px; height: 16px; text: device.id; font-size: 11px; color: device.added ? #92969e : #808080; } @@ -970,10 +975,10 @@ export component GalleryView inherits Rectangle { border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface; border-color: white; } - Rectangle { + ShadowSurface { width: parent.width; height: parent.height; - border-width: 1px; border-radius: UiRadius.surface; - border-color: #d8dadd; + border-radius: UiRadius.surface; + shadow-color: UiShadow.edge; } } Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); font-size: 12px; vertical-alignment: center; } @@ -985,10 +990,10 @@ export component GalleryView inherits Rectangle { } if root.confirming: Rectangle { background: #00000042; TouchArea {} - Rectangle { + ShadowSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 360px; height: confirmation-list.height + 118px; - background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2; + background: white; border-radius: UiRadius.surface; shadow-color: UiShadow.edge; Text { x: 20px; y: 18px; width: parent.width - 132px; height: 24px; text: root.action-code == "disconnect" ? GalleryStrings.tr("断开连接", "Disconnect devices", "Отключение") : root.action-name; @@ -999,10 +1004,10 @@ export component GalleryView inherits Rectangle { background: #eff3f8; border-radius: UiRadius.surface; Text { text: root.selected-count + GalleryStrings.tr(" 台设备", " devices", " устр."); color: #626e7d; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } } - confirmation-list := Rectangle { + confirmation-list := ShadowSurface { x: 20px; y: 58px; width: parent.width - 40px; height: clamp(root.selected-devices.length * 52px + 2px, 54px, 210px); - background: #fcfdfe; border-width: 1px; border-color: #e2e6eb; border-radius: UiRadius.surface; clip: true; + background: #fcfdfe; shadow-color: UiShadow.edge; border-radius: UiRadius.surface; clip: true; confirmation-scroll := ScrollView { x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px; viewport-width: self.width - 12px; @@ -1018,7 +1023,7 @@ export component GalleryView inherits Rectangle { Rectangle { x: 9px; y: 7px; width: 6px; height: 6px; border-radius: 3px; background: device.connected ? #27a762 : #a0a6b0; } Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.tr("已连接", "Connected", "Подключено") : GalleryStrings.tr("未连接", "Not connected", "Нет связи"); color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; } } - if index + 1 < root.selected-devices.length: Rectangle { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #edf0f3; } + if index + 1 < root.selected-devices.length: ShadowSeparator { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; shadow-color: UiShadow.separator; } } } } diff --git a/apps/desktop/src/gui/ui/group_filter.slint b/apps/desktop/src/gui/ui/group_filter.slint index 3bcd1ec4..e025c02e 100644 --- a/apps/desktop/src/gui/ui/group_filter.slint +++ b/apps/desktop/src/gui/ui/group_filter.slint @@ -1,22 +1,24 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { ScrollView } from "std-widgets.slint"; -import { FontAwesomeIcons } from "common.slint"; +import { FontAwesomeIcons, ControlStyle } from "common.slint"; // Shared by history and DeskHub; system choices precede the named groups. -export component GroupFilter inherits Rectangle { +export component GroupFilter inherits ShadowSurface { in property <[string]> model; in property system-count: 2; in property enabled: true; - // An embedded picker shares its enclosing field's border and focus ring. + // An embedded picker shares its enclosing field's shadow and focus glow. in property embedded: false; in property show-indicator: true; in property popup-min-width: 0px; in property text-size: 12px; in property text-weight: 400; in property text-color: #30343b; - in property normal-border-color: #c9cdd2; - in property hover-border-color: root.normal-border-color; - in property focus-border-color: #8db5df; + in property normal-background: ControlStyle.surface; + in property normal-shadow-color: UiShadow.edge; + in property hover-shadow-color: UiShadow.hover; + in property focus-shadow-color: UiShadow.focus; in-out property current-index: 0; out property has-focus: trigger-focus.has-focus || popup.is-open; out property has-hover: root.enabled && trigger-touch.has-hover && !popup.is-open; @@ -34,13 +36,16 @@ export component GroupFilter inherits Rectangle { } width: 155px; height: 28px; - background: root.embedded ? (root.enabled && trigger-touch.has-hover ? #eef3fb : transparent) - : !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white; - border-width: root.embedded ? 0px : 1px; - border-color: popup.is-open || trigger-focus.has-focus ? root.focus-border-color - : trigger-touch.has-hover ? root.hover-border-color : root.normal-border-color; + background: root.embedded ? (root.enabled && trigger-touch.has-hover ? ControlStyle.hover : transparent) + : !root.enabled ? ControlStyle.disabled + : trigger-touch.pressed || popup.is-open ? ControlStyle.pressed + : trigger-touch.has-hover ? ControlStyle.hover : root.normal-background; + shadow-enabled: !root.embedded; + shadow-focused: popup.is-open || trigger-focus.has-focus; + shadow-color: popup.is-open || trigger-focus.has-focus ? root.focus-shadow-color + : trigger-touch.has-hover ? root.hover-shadow-color : root.normal-shadow-color; border-radius: UiRadius.control; - animate background, border-color { duration: 100ms; easing: ease-out; } + animate background, shadow-color { duration: 100ms; easing: ease-out; } forward-focus: trigger-focus; accessible-role: combobox; accessible-enabled: root.enabled; @@ -135,12 +140,13 @@ export component GroupFilter inherits Rectangle { forward-focus: popup-focus; popup-focus := FocusScope { key-pressed(event) => { return root.handle-key(event); } - Rectangle { + ShadowSurface { y: (1 - root.popup-progress) * -4px; width: parent.width; height: parent.height; opacity: root.popup-progress; - background: white; border-width: 1px; - border-color: root.normal-border-color; border-radius: UiRadius.menu; + background: white; + clip: true; + shadow-color: root.normal-shadow-color; border-radius: UiRadius.menu; ScrollView { x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px; viewport-width: self.width; @@ -148,27 +154,30 @@ export component GroupFilter inherits Rectangle { viewport-y <=> root.scroll-y; Rectangle { width: parent.width; height: root.list-height; - if root.has-divider: Rectangle { + if root.has-divider: ShadowSeparator { x: 6px; y: root.system-count * 26px + 3px; - width: parent.width - 12px; height: 1px; background: #e9ebee; + width: parent.width - 12px; height: 1px; shadow-color: UiShadow.separator; } for choice[index] in root.model: Rectangle { y: index * 26px + (root.has-divider && index >= root.system-count ? 6px : 0px); width: parent.width; height: 26px; border-radius: UiRadius.control; - background: index == root.current-index ? #edf4fc : option-touch.has-hover ? #f1f3f5 : transparent; + background: index == root.current-index + ? (option-touch.pressed ? ControlStyle.selected-pressed : option-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) + : option-touch.pressed ? ControlStyle.pressed : option-touch.has-hover ? ControlStyle.hover : transparent; animate background { duration: 100ms; easing: ease-out; } accessible-role: list-item; accessible-label: choice; Text { x: 8px; width: parent.width - 34px; height: parent.height; - text: choice; color: #30343b; font-size: 12px; + text: choice; color: index == root.current-index ? ControlStyle.selected-text : #30343b; font-size: 12px; + font-weight: index == root.current-index ? 600 : 400; overflow: elide; vertical-alignment: center; accessible-role: none; } if index == root.current-index: Text { x: parent.width - 24px; width: 16px; height: parent.height; text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free"; - font-weight: 900; font-size: 10px; color: #4e87c0; + font-weight: 900; font-size: 10px; color: ControlStyle.selected-text; horizontal-alignment: center; vertical-alignment: center; accessible-role: none; } diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 2fb5a3a6..40500d6f 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -1,3 +1,4 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { LanStrings } from "lan_strings.slint"; import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint"; import { UiRadius } from "ui_radius.slint"; @@ -14,7 +15,7 @@ import { GroupFilter } from "group_filter.slint"; import { ScrollView } from "std-widgets.slint"; import { RoundedLineEdit } from "rounded_line_edit.slint"; import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint"; -import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint"; +import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiFontStyle, ControlStyle } from "common.slint"; import "../assets/fonts/message_spacing.ttf"; export struct RecentConnection { @@ -206,26 +207,29 @@ component DialogPromptText inherits Rectangle { } } -// Use the same compact, plain-text field for connection and password changes. -component PasswordInput inherits Rectangle { +// Use the same plain-text field for connection and password changes. +component PasswordInput inherits ShadowSurface { in-out property text; in property max-length: 0; in property text-size: ImGuiFontStyle.base; + in property error: false; private property previous-text; callback accepted; width: 70px; height: 23px; - background: white; - border-width: 1px; - border-color: ImGuiLineStyle.border; + background: editor.has-focus ? ControlStyle.input-focused : ControlStyle.input-surface; + shadow-color: root.error ? UiShadow.error : editor.has-focus ? UiShadow.focus : UiShadow.edge; + shadow-focused: editor.has-focus || root.error; + shadow-compact: true; border-radius: UiRadius.control; + clip: true; forward-focus: editor; init => { root.previous-text = root.text; } editor := TextInput { - x: 5px; y: 1px; width: parent.width - 10px; height: parent.height - 2px; + x: 12px; y: 1px; width: parent.width - 24px; height: parent.height - 2px; text <=> root.text; input-type: InputType.text; single-line: true; @@ -937,10 +941,10 @@ export component MainWindow inherits Window { clicked => { root.close-main-window(); } } } - Rectangle { + ShadowSeparator { y: parent.height - 1px; height: 1px; - background: #d5d3d0; + shadow-color: UiShadow.separator; } } @@ -1024,10 +1028,10 @@ export component MainWindow inherits Window { } close-touch := TouchArea { clicked => { root.close-main-window(); } } } - Rectangle { + ShadowSeparator { y: parent.height - 1px; height: 1px; - background: #d4d4d4; + shadow-color: UiShadow.separator; } } @@ -1090,12 +1094,12 @@ export component MainWindow inherits Window { // Slint owns the client area below the native macOS title bar. Draw // the boundary here because AppKit's titlebar separator is not shown // reliably when winit creates a window without an NSToolbar. - Rectangle { + ShadowSeparator { x: 0px; y: 0px; width: parent.width; height: 1px; - background: ImGuiLineStyle.control-separator; + shadow-color: UiShadow.separator; z: 7; } @@ -1146,6 +1150,7 @@ export component MainWindow inherits Window { x: root.ip-direct-enabled ? connection-mode-switch.x + connection-mode-switch.width + 8px : 20px; y: connection-mode-switch.y; width: 80px; height: 28px; + normal-background: white; text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub"); z: 8; clicked => { root.open-gallery(); } @@ -1154,20 +1159,20 @@ export component MainWindow inherits Window { visible: !root.gallery-mode; x: parent.width - 56px; y: open-gallery-button.y; width: 36px; height: 28px; + normal-background: white; icon: FontAwesomeIcons.bars; tooltip: GalleryStrings.tr("更多", "More", "Ещё"); z: 8; clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; } } // Unread messages take precedence so the menu has only one indicator. - if !root.gallery-mode && root.update-available && root.announcement-unread <= 0: Rectangle { + if !root.gallery-mode && root.update-available && root.announcement-unread <= 0: ShadowSurface { x: menu-button.x + menu-button.width - self.width + 4px; y: menu-button.y - 1px; width: 16px; height: 16px; border-radius: 8px; - border-width: 1px; - border-color: white; + shadow-color: UiShadow.edge; background: WorkbenchStyle.accent; opacity: root.menu-open || mod(animation-tick(), 2s) < 1s ? 1 : 0; z: 9; @@ -1201,14 +1206,13 @@ export component MainWindow inherits Window { clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; } } } - if !root.gallery-mode && root.announcement-unread > 0: Rectangle { + if !root.gallery-mode && root.announcement-unread > 0: ShadowSurface { x: menu-button.x + menu-button.width - self.width + 4px; y: menu-button.y - 1px; width: max(16px, unread-badge-label.preferred-width + 8px); height: 16px; border-radius: 8px; - border-width: 1px; - border-color: white; + shadow-color: UiShadow.edge; background: #d92d20; z: 9; accessible-role: button; @@ -1280,7 +1284,7 @@ export component MainWindow inherits Window { lan-port-editor.select-all(); } } - local-address-field := Rectangle { + local-address-field := ShadowSurface { x: 16px; y: 90px; width: parent.width - 32px; height: 40px; // Size the endpoint as one line; reserve five port digits so // editing never shifts the IP or changes the font size. @@ -1298,15 +1302,15 @@ export component MainWindow inherits Window { // Raise the whole field: a child's z cannot escape its // parent's stacking order above the following password row. z: 1; - background: WorkbenchStyle.field-surface; border-radius: UiRadius.control; - border-width: 1px; - border-color: !root.lan-mode ? WorkbenchStyle.border - : root.lan-endpoint-error != "" ? #d92d20 - : lan-ip-picker.has-focus || root.lan-port-editing ? WorkbenchStyle.accent - : WorkbenchStyle.border; - animate border-color { duration: 120ms; } + background: ControlStyle.input-surface; border-radius: UiRadius.control; + shadow-compact: true; + shadow-color: !root.lan-mode ? UiShadow.edge + : root.lan-endpoint-error != "" ? UiShadow.error + : lan-ip-picker.has-focus || root.lan-port-editing ? UiShadow.focus + : UiShadow.edge; + animate shadow-color { duration: 120ms; } if !root.lan-mode: Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; overflow: elide; color: WorkbenchStyle.text; vertical-alignment: center; } - Rectangle { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; background: WorkbenchStyle.border; } + ShadowSeparator { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; vertical: true; shadow-color: UiShadow.separator; } lan-ip-picker := GroupFilter { visible: root.lan-mode; x: 4px; y: 2px; @@ -1319,9 +1323,9 @@ export component MainWindow inherits Window { model: root.lan-interfaces; system-count: 0; current-index <=> root.lan-interface-draft-index; enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0; - normal-border-color: WorkbenchStyle.control-border; - hover-border-color: WorkbenchStyle.hover-border; - focus-border-color: WorkbenchStyle.accent; + normal-shadow-color: UiShadow.edge; + hover-shadow-color: UiShadow.hover; + focus-shadow-color: WorkbenchStyle.accent; accessible-label: GalleryStrings.tr("切换本机 IP", "Switch local IP", "Сменить локальный IP"); selected(index) => { root.copy-feedback = CopyFeedback.idle; @@ -1346,17 +1350,16 @@ export component MainWindow inherits Window { font-weight: 600; color: WorkbenchStyle.text; horizontal-alignment: center; vertical-alignment: center; } - lan-port-field := Rectangle { + lan-port-field := ShadowSurface { visible: root.lan-mode; x: lan-ip-picker.x + lan-ip-picker.width + 2px; y: 2px; width: lan-port-metrics.preferred-width * local-address-field.lan-font-scale + 4px; height: parent.height - 4px; - background: lan-port-editor.has-focus ? WorkbenchStyle.surface - : lan-port-hover.has-hover ? WorkbenchStyle.hover-surface : transparent; + background: lan-port-editor.has-focus ? ControlStyle.input-focused + : lan-port-hover.has-hover ? ControlStyle.hover : transparent; border-radius: UiRadius.control; - border-width: 1px; - border-color: lan-port-editor.has-focus ? WorkbenchStyle.accent : transparent; - animate background, border-color { duration: 120ms; } + shadow-color: lan-port-editor.has-focus ? UiShadow.focus : transparent; + animate background, shadow-color { duration: 120ms; } lan-port-hover := TouchArea { enabled: root.lan-mode && !root.lan-password-pending; mouse-cursor: text; @@ -1439,12 +1442,13 @@ export component MainWindow inherits Window { } } Text { x: 16px; y: 138px; height: 18px; text: UiStrings.password; font-size: 12px; color: WorkbenchStyle.muted; } - Rectangle { + ShadowSurface { x: 16px; y: 158px; width: parent.width - 32px; height: 36px; - background: WorkbenchStyle.field-surface; border-radius: UiRadius.control; - border-width: 1px; border-color: WorkbenchStyle.border; + background: ControlStyle.input-surface; border-radius: UiRadius.control; + shadow-color: UiShadow.edge; + shadow-compact: true; Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.active-password == "" ? "—" : root.active-password-visible ? root.active-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; } - Rectangle { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; background: WorkbenchStyle.border; } + ShadowSeparator { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; vertical: true; shadow-color: UiShadow.separator; } WorkbenchButton { x: parent.width - 112px; y: 0px; width: 36px; quiet: true; icon: root.active-password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash; @@ -1471,15 +1475,18 @@ export component MainWindow inherits Window { Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.remote-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; } Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("输入 IP:端口,通过局域网连接", "Enter IP:port to connect over LAN", "Введите IP:порт для подключения по LAN") : GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; } Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; } - Rectangle { + ShadowSurface { x: 16px; y: 90px; width: parent.width - 32px; height: 40px; - background: white; border-radius: UiRadius.control; border-width: 1px; - border-color: root.connection-input-error != "" ? #d65252 : remote-id-editor.has-focus ? WorkbenchStyle.accent : WorkbenchStyle.control-border; - animate border-color { duration: 120ms; } + background: remote-id-editor.has-focus ? ControlStyle.input-focused : ControlStyle.input-surface; + shadow-compact: true; + border-radius: UiRadius.control; + shadow-color: root.connection-input-error != "" ? UiShadow.error : remote-id-editor.has-focus ? UiShadow.focus : UiShadow.edge; + shadow-focused: remote-id-editor.has-focus || root.connection-input-error != ""; + animate shadow-color { duration: 120ms; } if root.remote-id-input == "": Text { x: 12px; width: parent.width - 24px; height: parent.height; text: root.lan-mode ? "192.168.1.20:52888" : GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр"); - font-size: 13px; color: #8290a4; vertical-alignment: center; + font-size: 13px; color: ControlStyle.input-placeholder; vertical-alignment: center; } remote-id-editor := TextInput { x: 12px; width: parent.width - 24px; height: parent.height; @@ -1528,15 +1535,16 @@ export component MainWindow inherits Window { visible: !root.lan-mode; x: parent.width - self.width - 20px; y: 0px; width: 155px; height: 32px; - normal-border-color: WorkbenchStyle.control-border; - hover-border-color: WorkbenchStyle.hover-border; - focus-border-color: WorkbenchStyle.accent; + normal-background: white; + normal-shadow-color: UiShadow.edge; + hover-shadow-color: UiShadow.hover; + focus-shadow-color: WorkbenchStyle.accent; model: root.recent-groups; current-index <=> root.recent-group-index; selected(index) => { root.recent-group-selected(index); } } - recent-list := Rectangle { + recent-list := ShadowSurface { x: 20px; y: 36px; width: parent.width - 40px; @@ -1544,8 +1552,8 @@ export component MainWindow inherits Window { // A continuous inset surface groups the cards into one // horizontal scrolling lane, including its scrollbar. background: root.recent-connections.length > 0 ? #e6edf6 : transparent; - border-width: root.recent-connections.length > 0 ? 1px : 0px; - border-color: #c9d4e3; + shadow-enabled: root.recent-connections.length > 0; + shadow-color: UiShadow.edge; border-radius: UiRadius.surface; clip: true; @@ -1668,7 +1676,7 @@ export component MainWindow inherits Window { width: parent.width; height: 44px; background: white; - Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { x: 0px; y: 0px; width: parent.width; height: 1px; shadow-color: UiShadow.separator; } Text { x: 12px; y: 5px; height: 19px; width: parent.width - 56px; @@ -1802,11 +1810,11 @@ export component MainWindow inherits Window { } } } - Rectangle { + ShadowSurface { width: parent.width; height: 124px; - border-width: 1px; border-radius: UiRadius.surface; - border-color: card-touch.has-hover || card-menu.menu-open ? WorkbenchStyle.hover-border : WorkbenchStyle.border; - animate border-color { duration: 120ms; } + border-radius: UiRadius.surface; + shadow-color: card-touch.has-hover || card-menu.menu-open ? UiShadow.hover : UiShadow.edge; + animate shadow-color { duration: 120ms; } } } } @@ -1863,10 +1871,10 @@ export component MainWindow inherits Window { y: parent.height - 28px; height: 28px; background: white; - Rectangle { + ShadowSeparator { y: 0px; height: 1px; - background: WorkbenchStyle.border; + shadow-color: UiShadow.separator; } Rectangle { x: 20px; y: 11px; width: 6px; height: 6px; border-radius: 3px; @@ -1917,7 +1925,7 @@ export component MainWindow inherits Window { // Shared device details sit outside the clipped recent list and wall rail, // below the modal layer (z >= 39). if (root.gallery-mode ? gallery.hovered-device.id != "" : root.recent-tooltip-visible) - && !root.connection-dialog-open && !root.lan-open: device-tip := Rectangle { + && !root.connection-dialog-open && !root.lan-open: device-tip := ShadowSurface { private property device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name; private property host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host; private property device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id; @@ -1944,8 +1952,7 @@ export component MainWindow inherits Window { self.label-width + max(tooltip-id.preferred-width, tooltip-groups.preferred-width) + 32px)); height: tooltip-groups.y + tooltip-groups.height + 10px; background: WorkbenchStyle.surface; - border-width: 1px; - border-color: #d5dfea; + shadow-color: UiShadow.edge; border-radius: UiRadius.menu; drop-shadow-color: #14294424; drop-shadow-blur: 14px; @@ -1988,11 +1995,11 @@ export component MainWindow inherits Window { color: WorkbenchStyle.muted; font-size: 10px; wrap: word-wrap; } - tooltip-divider := Rectangle { + tooltip-divider := ShadowSeparator { x: 12px; y: tooltip-name.y + tooltip-name.height + (device-tip.has-host-name ? tooltip-host.height + 3px : 0px) + 8px; width: parent.width - 24px; height: 1px; - background: WorkbenchStyle.separator; + shadow-color: UiShadow.separator; } tooltip-id-label := Text { x: 12px; y: tooltip-divider.y + 7px; @@ -2039,7 +2046,7 @@ export component MainWindow inherits Window { } // Expand downward from the application toolbar. - if !root.gallery-mode && root.menu-open: Rectangle { + if !root.gallery-mode && root.menu-open: ShadowSurface { x: menu-button.x + menu-button.width - self.width; y: menu-button.y + menu-button.height + 6px; width: max(128px, max(menu-settings-label.preferred-width, @@ -2047,8 +2054,7 @@ export component MainWindow inherits Window { menu-about-label.preferred-width + (root.update-available ? menu-update-badge.width + 8px : 0px))) + 28px); height: application-menu-content.preferred-height; background: WorkbenchStyle.surface; - border-width: 1px; - border-color: WorkbenchStyle.control-border; + shadow-color: UiShadow.edge; border-radius: UiRadius.menu; z: 10; TouchArea { } @@ -2081,9 +2087,9 @@ export component MainWindow inherits Window { } Rectangle { height: 7px; - Rectangle { + ShadowSeparator { x: 8px; y: 3px; width: parent.width - 16px; height: 1px; - background: WorkbenchStyle.border; + shadow-color: UiShadow.separator; } } menu-messages := Rectangle { @@ -2130,9 +2136,9 @@ export component MainWindow inherits Window { } Rectangle { height: 7px; - Rectangle { + ShadowSeparator { x: 8px; y: 3px; width: parent.width - 16px; height: 1px; - background: WorkbenchStyle.border; + shadow-color: UiShadow.separator; } } menu-about := Rectangle { @@ -2232,24 +2238,29 @@ export component MainWindow inherits Window { DialogSurface { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; - width: root.compact-language ? 230px : 320px; - height: 170px; - VerticalLayout { - padding: 20px; - spacing: 12px; - DialogPromptText { prompt: UiStrings.new-password; } - HorizontalLayout { - alignment: center; - PasswordInput { - text <=> root.new-password-input; - max-length: 6; - } + width: root.compact-language ? 260px : 320px; + height: reset-content.preferred-height; + reset-content := VerticalLayout { + padding: 24px; + spacing: 16px; + Text { + text: UiStrings.new-password; color: WorkbenchStyle.text; + font-size: 13px; font-weight: 500; wrap: word-wrap; + min-height: 22px; vertical-alignment: center; + } + PasswordInput { + width: parent.width - 48px; height: 38px; text-size: 16px; + text <=> root.new-password-input; + max-length: 6; + error: root.reset-password-invalid; + } + if root.reset-password-invalid: Text { + text: UiStrings.invalid-password; color: #b42318; font-size: 12px; wrap: word-wrap; } - Text { height: root.reset-password-invalid ? 18px : 0px; visible: root.reset-password-invalid; text: UiStrings.invalid-password; color: #d92d20; font-size: ImGuiFontStyle.small; horizontal-alignment: center; } HorizontalLayout { - alignment: center; spacing: 8px; + spacing: 12px; height: 34px; PrimaryButton { - text: UiStrings.ok; + text: UiStrings.ok; height: 34px; horizontal-stretch: 1; enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending; clicked => { if (root.reset-password(root.new-password-input)) { @@ -2259,7 +2270,7 @@ export component MainWindow inherits Window { } else { root.reset-password-invalid = true; } } } - SecondaryButton { text: UiStrings.cancel; clicked => { root.new-password-input = ""; root.reset-password-invalid = false; root.reset-password-open = false; } } + SecondaryButton { text: UiStrings.cancel; height: 34px; horizontal-stretch: 1; clicked => { root.new-password-input = ""; root.reset-password-invalid = false; root.reset-password-open = false; } } } } } @@ -2298,7 +2309,7 @@ export component MainWindow inherits Window { DialogPromptText { prompt: UiStrings.edit-alias; } RoundedLineEdit { height: 28px; - normal-border-color: ImGuiLineStyle.border; + normal-shadow-color: UiShadow.edge; text <=> root.alias-input; accepted => { root.recent-edit-alias(root.alias-remote-id, root.alias-input); @@ -2373,7 +2384,7 @@ export component MainWindow inherits Window { y: (parent.height - self.height) / 2; width: min(420px, parent.width - 40px); height: disconnect-content.preferred-height; - border-color: WorkbenchStyle.border; + shadow-color: UiShadow.edge; TouchArea { scroll-event(event) => { accept } } disconnect-content := VerticalLayout { padding: 24px; @@ -2463,14 +2474,14 @@ export component MainWindow inherits Window { if !root.lan-open && root.connection-password-required && !root.connection-validating: PasswordInput { x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px; width: 164px; height: 36px; text-size: 16px; - border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border; + border-radius: UiRadius.control; text <=> root.connection-password; accepted => { root.submit-connection-password(); } } if root.lan-open && !root.lan-connecting: PasswordInput { x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px; width: 164px; height: 36px; text-size: 16px; max-length: 6; - border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border; + border-radius: UiRadius.control; text <=> root.lan-access-password; accepted => { root.submit-connection-password(); } } @@ -2522,7 +2533,7 @@ export component MainWindow inherits Window { y: (parent.height - self.height) / 2; width: min(400px, parent.width - 40px); height: permission-content.preferred-height; - background: #f8fafc; border-color: WorkbenchStyle.separator; + background: #f8fafc; shadow-color: UiShadow.edge; drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px; TouchArea { clicked => { focus-sink.focus(); } } permission-content := VerticalLayout { @@ -2594,7 +2605,7 @@ export component MainWindow inherits Window { } VerticalLayout { spacing: 12px; - Rectangle { height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } Text { text: GalleryStrings.tr("请在系统设置中允许 CrossDesk 访问。如系统提示重新打开应用,请按提示操作。", "Allow CrossDesk access in System Settings. Reopen the app if macOS asks you to.", @@ -2619,7 +2630,7 @@ export component MainWindow inherits Window { y: (parent.height - self.height) / 2; width: min(400px, parent.width - 40px); height: min(446px, parent.height - 16px); - background: #f8fafc; border-radius: UiRadius.surface; border-color: #e1e7ef; + background: #f8fafc; border-radius: UiRadius.surface; shadow-color: UiShadow.edge; drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px; TouchArea { clicked => { focus-sink.focus(); } } Text { @@ -2627,16 +2638,16 @@ export component MainWindow inherits Window { width: parent.width - 2 * settings-dialog.content-inset; height: 27px; text: UiStrings.settings; font-size: 20px; font-weight: 600; color: #22324a; } - Rectangle { + ShadowSurface { x: settings-dialog.content-inset; y: 56px; width: parent.width - 2 * settings-dialog.content-inset; height: 40px; - background: WorkbenchStyle.canvas; border-radius: UiRadius.menu; - border-width: 1px; border-color: WorkbenchStyle.border; - Rectangle { + background: #dfe7f2; border-radius: UiRadius.menu; + shadow-color: UiShadow.edge; + ShadowSurface { x: 4px + root.settings-tab * (parent.width - 4px) / 2; y: 4px; width: (parent.width - 12px) / 2; height: parent.height - 8px; background: WorkbenchStyle.surface; border-radius: UiRadius.control; - border-width: 1px; border-color: WorkbenchStyle.control-border; + shadow-color: UiShadow.edge; animate x { duration: 140ms; easing: ease-out; } } for label[index] in [GalleryStrings.tr("常规", "General", "Общие"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle { @@ -2770,9 +2781,9 @@ export component MainWindow inherits Window { width: parent.width - 2 * settings-dialog.content-inset; height: 24px; text: root.settings-save-error; font-size: 11px; color: #c53e3e; wrap: word-wrap; } - Rectangle { + ShadowSeparator { x: settings-dialog.content-inset; y: parent.height - 64px; - width: parent.width - 2 * settings-dialog.content-inset; height: 1px; background: #e6ebf2; + width: parent.width - 2 * settings-dialog.content-inset; height: 1px; shadow-color: UiShadow.separator; } HorizontalLayout { x: settings-dialog.content-inset; y: parent.height - 50px; @@ -2798,7 +2809,7 @@ export component MainWindow inherits Window { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: min(460px, parent.width - 32px); height: min(380px, parent.height - 24px); - background: #f8fafc; border-color: #e1e7ef; + background: #f8fafc; shadow-color: UiShadow.edge; drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px; TouchArea { clicked => { focus-sink.focus(); } } Text { @@ -2852,7 +2863,7 @@ export component MainWindow inherits Window { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 360px; height: root.self-host-settings-error == "" ? 266px : 322px; - border-radius: UiRadius.surface; border-color: #e1e7ef; background: #f8fafc; + border-radius: UiRadius.surface; shadow-color: UiShadow.edge; background: #f8fafc; drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px; TouchArea { clicked => { focus-sink.focus(); } } Text { @@ -2863,21 +2874,21 @@ export component MainWindow inherits Window { RoundedLineEdit { x: 18px; y: 80px; width: parent.width - 36px; height: 30px; surrounding-background: #f8fafc; - normal-border-color: WorkbenchStyle.control-border; + normal-shadow-color: UiShadow.edge; text <=> root.server-host; } Text { x: 18px; y: 120px; width: parent.width - 36px; height: 20px; text: UiStrings.server-port; color: #34445a; font-size: 12px; overflow: elide; vertical-alignment: center; } RoundedLineEdit { x: 18px; y: 144px; width: parent.width - 36px; height: 30px; surrounding-background: #f8fafc; - normal-border-color: WorkbenchStyle.control-border; + normal-shadow-color: UiShadow.edge; text <=> root.server-port; input-type: InputType.number; } if root.self-host-settings-error != "": Text { x: 18px; y: 186px; width: parent.width - 36px; height: 48px; text: root.self-host-settings-error; color: #c53e3e; font-size: 11px; wrap: word-wrap; } - Rectangle { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; background: #e6ebf2; } + ShadowSeparator { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; shadow-color: UiShadow.separator; } SettingsButton { x: parent.width - 114px; y: parent.height - 50px; width: 92px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } } SettingsButton { x: parent.width - 204px; y: parent.height - 50px; width: 80px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } } } @@ -2915,7 +2926,7 @@ export component MainWindow inherits Window { color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; font-family: "CrossDesk Message Spacing"; } - Rectangle { height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } HorizontalLayout { alignment: end; SettingsButton { @@ -2983,11 +2994,11 @@ export component MainWindow inherits Window { width: about-scroll.width; height: self.preferred-height; // Keep the expanding scrollbar outside the content. padding: 0px; padding-right: 22px; spacing: 16px; - Rectangle { + ShadowSurface { height: max(80px, product-info.preferred-height + 32px); background: WorkbenchStyle.surface; border-radius: UiRadius.surface; - border-width: 1px; border-color: WorkbenchStyle.border; + shadow-color: UiShadow.edge; Image { x: 16px; y: (parent.height - self.height) / 2; width: 56px; height: 56px; @@ -3004,12 +3015,11 @@ export component MainWindow inherits Window { } } } - if root.update-available: Rectangle { + if root.update-available: ShadowSurface { height: update-summary.preferred-height + 24px; background: WorkbenchStyle.field-surface; border-radius: UiRadius.control; - border-width: 1px; - border-color: WorkbenchStyle.separator; + shadow-color: UiShadow.edge; update-summary := VerticalLayout { x: 12px; y: 12px; width: parent.width - 24px; height: self.preferred-height; @@ -3079,7 +3089,7 @@ export component MainWindow inherits Window { x: 24px; y: parent.height - self.height - 24px; width: parent.width - 48px; height: self.preferred-height; spacing: 16px; - Rectangle { height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } if root.update-status-text != "": Text { text: root.update-status-text; color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e @@ -3123,7 +3133,7 @@ export component MainWindow inherits Window { // Use the same content area for sizing and centering the dialog. height: min(488px, parent.height - 32px); border-radius: UiRadius.surface; - border-color: WorkbenchStyle.border; + shadow-color: UiShadow.edge; drop-shadow-color: #07142b24; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px; @@ -3179,10 +3189,9 @@ export component MainWindow inherits Window { font-weight: 600; vertical-alignment: center; } - Rectangle { + ShadowSurface { background: WorkbenchStyle.field-surface; - border-width: 1px; - border-color: WorkbenchStyle.separator; + shadow-color: UiShadow.edge; border-radius: UiRadius.control; // Inset the viewport so text and the scrollbar stay clear // of the rounded frame, including at either scroll limit. @@ -3229,7 +3238,7 @@ export component MainWindow inherits Window { VerticalLayout { vertical-stretch: 0; spacing: 16px; - Rectangle { height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } if root.update-install-status != "": Text { text: root.update-install-status; font-size: 12px; @@ -3302,7 +3311,7 @@ export component MainWindow inherits Window { width: min(440px, parent.width - 40px); height: update-confirm-content.preferred-height; border-radius: UiRadius.surface; - border-color: WorkbenchStyle.border; + shadow-color: UiShadow.edge; TouchArea {} update-confirm-content := VerticalLayout { padding: 24px; @@ -3356,7 +3365,7 @@ export component MainWindow inherits Window { width: 512px; height: wayland-notice-content.preferred-height; border-radius: UiRadius.surface; - border-color: #dce3ee; + shadow-color: UiShadow.edge; drop-shadow-color: #07142b30; drop-shadow-blur: 28px; drop-shadow-offset-y: 10px; @@ -3380,10 +3389,10 @@ export component MainWindow inherits Window { } } - Rectangle { + ShadowSurface { background: #fff8eb; - border-color: #f0dfba; - border-width: 1px; + shadow-color: UiShadow.edge; + border-radius: UiRadius.surface; VerticalLayout { padding: 14px; spacing: 8px; @@ -3403,10 +3412,10 @@ export component MainWindow inherits Window { } } - Rectangle { + ShadowSurface { background: #f3f7fd; - border-color: #dfe8f5; - border-width: 1px; + shadow-color: UiShadow.edge; + border-radius: UiRadius.surface; VerticalLayout { padding: 14px; spacing: 8px; @@ -3426,7 +3435,7 @@ export component MainWindow inherits Window { } } - Rectangle { height: 1px; background: #e9edf3; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } HorizontalLayout { height: 34px; alignment: end; PrimaryButton { @@ -3459,8 +3468,8 @@ export component MainWindow inherits Window { text-size: ImGuiFontStyle.prominent; text-weight: 600; } - Rectangle { - background: #f7f8fa; border-color: #d9dde4; border-width: 1px; border-radius: UiRadius.surface; + ShadowSurface { + background: #f7f8fa; shadow-color: UiShadow.edge; border-radius: UiRadius.surface; Text { x: 14px; y: 22px; width: parent.width - 28px; height: parent.height - 24px; text: UiStrings.service-setup-message; color: #3d434d; wrap: word-wrap; vertical-alignment: center; } } if root.portable-service-status != "": Text { @@ -3531,14 +3540,13 @@ export component MainWindow inherits Window { z: 250; } - if root.custom-titlebar: Rectangle { + if root.custom-titlebar: ShadowSurface { x: 0px; y: 0px; width: root.width; height: root.height; background: transparent; - border-width: 1px; - border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b; + shadow-color: UiShadow.edge; border-radius: root.wayland-titlebar ? root.wayland-corner-radius : 0px; z: 200; diff --git a/apps/desktop/src/gui/ui/message_panel.slint b/apps/desktop/src/gui/ui/message_panel.slint index 59c44685..2b5d8fc3 100644 --- a/apps/desktop/src/gui/ui/message_panel.slint +++ b/apps/desktop/src/gui/ui/message_panel.slint @@ -1,6 +1,7 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { ScrollView } from "std-widgets.slint"; -import { IconButton, FontAwesomeIcons } from "common.slint"; +import { IconButton, FontAwesomeIcons, ControlStyle } from "common.slint"; import { SettingsDialog, SettingsButton } from "settings_controls.slint"; import { WorkbenchStyle } from "workbench_controls.slint"; import "../assets/fonts/message_spacing.ttf"; @@ -90,11 +91,11 @@ export component MessagePanel inherits SettingsDialog { text: root.notice-text; font-size: 12px; wrap: word-wrap; color: #b3261e; } } - message-content := Rectangle { + message-content := ShadowSurface { x: 24px; y: root.content-top + (root.notice-text == "" ? 0px : notice.height + 12px); width: parent.width - 48px; height: parent.height - self.y - 24px; background: WorkbenchStyle.surface; - border-radius: UiRadius.surface; border-width: 1px; border-color: WorkbenchStyle.border; + border-radius: UiRadius.surface; shadow-color: UiShadow.edge; if root.items.length > 0 || root.selected-id != "": Rectangle { x: 1px; y: 1px; width: content-divider.x - 1px; height: parent.height - 2px; background: WorkbenchStyle.field-surface; @@ -112,19 +113,19 @@ export component MessagePanel inherits SettingsDialog { announcement-entries := VerticalLayout { width: announcements-scroll.viewport-width; spacing: 10px; padding: 0px; alignment: start; - for item in root.items: Rectangle { + for item in root.items: ShadowSurface { height: 64px; - background: root.selected-id == item.id ? #e8efff + background: root.selected-id == item.id + ? (entry-touch.pressed ? ControlStyle.selected-pressed : entry-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) : entry-touch.pressed ? WorkbenchStyle.pressed-surface : entry-touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface; border-radius: UiRadius.control; - border-width: 1px; - border-color: root.selected-id == item.id ? #b5c9f5 : WorkbenchStyle.border; + shadow-color: root.selected-id == item.id ? UiShadow.focus : UiShadow.edge; Text { x: 10px; y: 9px; width: parent.width - 30px; height: 20px; text: item.title; overflow: elide; font-size: 13px; font-weight: item.read ? 400 : 600; - color: root.selected-id == item.id ? WorkbenchStyle.accent : WorkbenchStyle.text; + color: root.selected-id == item.id ? ControlStyle.selected-text : WorkbenchStyle.text; vertical-alignment: center; } if !item.read: Rectangle { @@ -154,10 +155,10 @@ export component MessagePanel inherits SettingsDialog { color: WorkbenchStyle.muted; font-size: 13px; horizontal-alignment: center; vertical-alignment: center; } - content-divider := Rectangle { + content-divider := ShadowSeparator { x: announcement-list.x + announcement-list.width + 8px; y: 1px; width: 1px; height: parent.height - 2px; - background: WorkbenchStyle.separator; + vertical: true; shadow-color: UiShadow.separator; visible: root.items.length > 0 || root.selected-id != ""; accessible-role: none; } @@ -199,13 +200,14 @@ export component MessagePanel inherits SettingsDialog { x: parent.width - self.width; y: 0px; width: 28px; height: 28px; icon: FontAwesomeIcons.trash-can; icon-size: 12px; icon-color: WorkbenchStyle.muted; danger: true; + normal-background: ControlStyle.surface; accessible-role: button; accessible-label: MessageStrings.delete-message + " " + root.selected-title; accessible-action-default => { root.request-delete(root.selected-id, root.selected-revision); } clicked => { root.request-delete(root.selected-id, root.selected-revision); } } } - Rectangle { height: 1px; background: WorkbenchStyle.separator; } + ShadowSeparator { height: 1px; shadow-color: UiShadow.separator; } StyledText { vertical-stretch: 0; text: root.selected-body; diff --git a/apps/desktop/src/gui/ui/rounded_line_edit.slint b/apps/desktop/src/gui/ui/rounded_line_edit.slint index 58affe67..3ac9cc72 100644 --- a/apps/desktop/src/gui/ui/rounded_line_edit.slint +++ b/apps/desktop/src/gui/ui/rounded_line_edit.slint @@ -1,15 +1,17 @@ +import { ShadowSurface, UiShadow } from "ui_shadow.slint"; import { LineEdit } from "std-widgets.slint"; import { UiRadius } from "ui_radius.slint"; +import { ControlStyle } from "common.slint"; // Retain the standard editor's selection, clipboard, clear button and input -// method support. Only its outer frame is replaced by the shared shape. +// method support. Its frame receives the shared field tint and rounded shape. export component RoundedLineEdit inherits Rectangle { in-out property text; in property placeholder-text; in property input-type: InputType.text; in property enabled: true; in property font-size: 12px; - in property normal-border-color: #dfe6ef; + in property normal-shadow-color: UiShadow.edge; in property surrounding-background: white; callback accepted(); callback edited(); @@ -20,8 +22,8 @@ export component RoundedLineEdit inherits Rectangle { forward-focus: editor; editor := LineEdit { - x: -1px; y: -1px; - width: parent.width + 2px; height: parent.height + 2px; + x: -2px; y: -2px; + width: parent.width + 4px; height: parent.height + 4px; text <=> root.text; placeholder-text: root.placeholder-text; input-type: root.input-type; @@ -30,8 +32,15 @@ export component RoundedLineEdit inherits Rectangle { accepted => { root.accepted(); } edited => { root.edited(); } } + // Fluent's LineEdit paints its own background and exposes no fill override. + // A visual-only tint preserves the standard editor and its built-in actions. + Rectangle { + width: parent.width; height: parent.height; + background: !root.enabled ? transparent + : editor.has-focus ? ControlStyle.input-focus-tint : ControlStyle.input-tint; + } // The software renderer clips children to rectangular bounds. Mask the - // standard editor's corners before drawing our rounded border on top. + // standard editor's corners before drawing our soft focus edge on top. Rectangle { x: -root.border-radius; y: -root.border-radius; width: parent.width + 2 * root.border-radius; @@ -40,11 +49,13 @@ export component RoundedLineEdit inherits Rectangle { border-radius: 2 * root.border-radius; border-color: root.surrounding-background; } - Rectangle { + ShadowSurface { width: parent.width; height: parent.height; border-radius: root.border-radius; - border-width: editor.has-focus ? 2px : 1px; - border-color: editor.has-focus ? #3564e3 : root.normal-border-color; + clip: true; + shadow-color: editor.has-focus ? UiShadow.focus : root.normal-shadow-color; + shadow-focused: editor.has-focus; + shadow-compact: true; background: transparent; } } diff --git a/apps/desktop/src/gui/ui/server_window.slint b/apps/desktop/src/gui/ui/server_window.slint index 90e2a613..fedb19bd 100644 --- a/apps/desktop/src/gui/ui/server_window.slint +++ b/apps/desktop/src/gui/ui/server_window.slint @@ -1,6 +1,7 @@ +import { ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { ScrollView } from "std-widgets.slint"; -import { FontAwesomeIcons, ImGuiLineStyle, FloatingBarStyle } from "common.slint"; +import { FontAwesomeIcons, FloatingBarStyle, ControlStyle } from "common.slint"; export struct ControllerEntry { remote-id: string, @@ -19,7 +20,7 @@ export global ServerStrings { in property cancel: "取消"; } -component SessionButton inherits Rectangle { +component SessionButton inherits ShadowSurface { in property text; in property danger: false; in property enabled: true; @@ -29,10 +30,10 @@ component SessionButton inherits Rectangle { width: label.preferred-width + 20px; height: 28px; border-radius: UiRadius.control; - border-width: 1px; - border-color: root.danger ? #d92d20 : #c9ced7; + shadow-color: root.danger ? UiShadow.error : UiShadow.edge; background: root.danger ? (root.pointer-feedback && touch.pressed ? #b42318 : root.pointer-feedback && touch.has-hover ? #f04438 : #d92d20) - : (root.pointer-feedback && touch.pressed ? #dfe3e9 : root.pointer-feedback && touch.has-hover ? #f3f4f6 : white); + : (root.pointer-feedback && touch.pressed ? ControlStyle.pressed + : root.pointer-feedback && touch.has-hover ? ControlStyle.hover : ControlStyle.surface); opacity: root.enabled ? 1 : 0.5; accessible-role: button; accessible-label: root.text; @@ -72,9 +73,9 @@ component SessionButton inherits Rectangle { // Restore hover feedback only after a fresh pointer move or press. clicked => { root.pointer-feedback = false; root.keyboard-focus = false; keyboard.focus(); root.clicked(); } } - if root.enabled && keyboard.has-focus && root.keyboard-focus: Rectangle { + if root.enabled && keyboard.has-focus && root.keyboard-focus: ShadowSurface { x: 2px; y: 2px; width: parent.width - 4px; height: parent.height - 4px; - border-radius: UiRadius.control - 2px; border-width: 1px; border-color: root.danger ? white : #2463c7; + border-radius: UiRadius.control - 2px; shadow-color: root.danger ? white : UiShadow.focus; } } @@ -107,10 +108,10 @@ export component ServerStatusBar inherits Window { return reject; } } - Rectangle { + ShadowSurface { x: 0px; y: 0px; width: parent.width; height: parent.height; background: touch.pressed ? #dfe3e9 : touch.has-hover ? #f3f4f6 : white; - border-width: 1px; border-color: ImGuiLineStyle.border; + shadow-color: UiShadow.edge; border-radius: FloatingBarStyle.corner-radius; border-top-right-radius: 0px; border-bottom-right-radius: 0px; @@ -170,9 +171,9 @@ export component ServerWindow inherits Window { if event.text == Key.Escape { root.dismiss-panel(); return accept; } return reject; } - Rectangle { + ShadowSurface { width: root.width; height: root.height; - background: white; border-width: 1px; border-color: ImGuiLineStyle.border; + background: white; shadow-color: UiShadow.edge; border-radius: UiRadius.surface; Text { x: 10px; y: 10px; width: parent.width - 20px; height: 24px; @@ -192,9 +193,9 @@ export component ServerWindow inherits Window { width: parent.width - (root.controllers.length > 1 ? disconnect.width + 4px : status.preferred-width + 8px); height: parent.height; border-radius: UiRadius.control; background: root.controllers.length > 1 && index == root.selected-controller ? #eef5fd : select-touch.has-hover ? #f3f4f6 : transparent; - if root.controllers.length > 1: Rectangle { + if root.controllers.length > 1: ShadowSurface { x: 4px; y: (parent.height - 13px) / 2; width: 13px; height: 13px; - border-radius: 7px; border-width: 1px; border-color: index == root.selected-controller ? #2463c7 : #8c939e; + border-radius: 7px; shadow-color: index == root.selected-controller ? UiShadow.focus : UiShadow.edge; if index == root.selected-controller: Rectangle { x: 3px; y: 3px; width: 7px; height: 7px; border-radius: 4px; background: #2463c7; } } name := Text { diff --git a/apps/desktop/src/gui/ui/settings_controls.slint b/apps/desktop/src/gui/ui/settings_controls.slint index 74224a55..903ee618 100644 --- a/apps/desktop/src/gui/ui/settings_controls.slint +++ b/apps/desktop/src/gui/ui/settings_controls.slint @@ -1,9 +1,11 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; -import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons } from "common.slint"; +import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons, ControlStyle } from "common.slint"; import { WorkbenchStyle } from "workbench_controls.slint"; // Shared visual treatment for the global settings pages. -export component SettingsButton inherits Rectangle { +export component SettingsButton inherits ShadowSurface { + shadow-compact: true; in property text; in property icon; in property primary: false; @@ -14,12 +16,13 @@ export component SettingsButton inherits Rectangle { private property content-padding: root.has-icon ? 8px : 12px; callback clicked(); width: 88px; height: 32px; border-radius: UiRadius.control; - background: !root.enabled ? #f0f3f7 + background: !root.enabled ? ControlStyle.disabled : root.primary ? (touch.pressed ? #1e4fa3 : touch.has-hover ? #2866c9 : #3275dc) - : touch.pressed ? #e9eef5 : touch.has-hover ? #f0f4fa : root.quiet ? transparent : white; - border-width: root.quiet || root.primary ? 0px : 1px; - border-color: !root.enabled ? WorkbenchStyle.border - : touch.has-hover ? WorkbenchStyle.hover-border : WorkbenchStyle.control-border; + : touch.pressed ? ControlStyle.pressed : touch.has-hover ? ControlStyle.hover + : root.quiet ? transparent : ControlStyle.surface; + shadow-enabled: !root.quiet && !root.primary; + shadow-color: !root.enabled ? UiShadow.edge + : touch.has-hover ? UiShadow.hover : UiShadow.edge; accessible-role: button; accessible-label: root.text; accessible-action-default => { if root.enabled { root.clicked(); } } Rectangle { @@ -59,7 +62,7 @@ export component SettingsDialog inherits DialogSurface { callback action(); callback close(); background: #f8fafc; - border-color: WorkbenchStyle.separator; + shadow-color: UiShadow.edge; drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px; @@ -97,6 +100,7 @@ export component SettingsDialog inherits DialogSurface { width: 28px; height: 28px; icon: FontAwesomeIcons.xmark; icon-size: 14px; icon-color: WorkbenchStyle.muted; + normal-background: ControlStyle.surface; hover-background: WorkbenchStyle.hover-surface; pressed-background: WorkbenchStyle.pressed-surface; accessible-role: button; accessible-label: root.close-label; @@ -110,8 +114,8 @@ export component SettingsDialog inherits DialogSurface { export component SettingsSelect inherits CompactComboBox { subtle: true; show-arrow: false; - normal-border-color: WorkbenchStyle.control-border; - hover-border-color: WorkbenchStyle.hover-border; + normal-shadow-color: UiShadow.edge; + hover-shadow-color: UiShadow.hover; height: 28px; } @@ -123,9 +127,9 @@ export component SettingsGroup inherits Rectangle { x: 2px; y: 0px; width: parent.width - 4px; height: 18px; text: root.title; font-size: 11px; font-weight: 600; color: #718096; vertical-alignment: center; } - Rectangle { + ShadowSurface { y: 24px; width: parent.width; height: parent.height - self.y; - background: white; border-radius: UiRadius.surface; border-color: WorkbenchStyle.border; border-width: 1px; + background: white; border-radius: UiRadius.surface; shadow-color: UiShadow.edge; VerticalLayout { padding: 0px; spacing: 0px; @children } } } @@ -141,8 +145,8 @@ export component SettingsRow inherits Rectangle { text: root.label; color: root.enabled ? #34445a : #9ba5b3; font-size: 12px; overflow: elide; vertical-alignment: center; } - if root.divider: Rectangle { - x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #d9e1ec; + if root.divider: ShadowSeparator { + x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; shadow-color: UiShadow.separator; } @children } diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 4b6b5fa2..463e1bdf 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -1,7 +1,8 @@ +import { ShadowSeparator, ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; import { ComboBox, ScrollView } from "std-widgets.slint"; import { RemoteCursor } from "remote_cursor.slint"; -import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint"; +import { FontAwesomeIcons, IconButton, PrimaryButton, SecondaryButton, DialogSurface, ImGuiFontStyle, ControlStyle } from "common.slint"; export struct StreamTab { remote-id: string, @@ -80,10 +81,9 @@ struct ControlBarTooltip { } // Shared surface treatment for the capsule and its detached detail cards. -component StreamControlSurface inherits Rectangle { +component StreamControlSurface inherits ShadowSurface { background: white; - border-width: 1px; - border-color: #dbe5f1; + shadow-color: UiShadow.edge; border-radius: 16px; drop-shadow-color: #1d35571a; drop-shadow-blur: 10px; @@ -121,14 +121,14 @@ component ControlBarButton inherits Rectangle { !root.show-logo && root.action-enabled && !root.muted && touch.pressed; // Share the fill with the slash cutout so every icon stays legible while // transitioning between idle, hover and pressed states. - private property button-fill: root.muted ? #eef0f3 - : root.button-pressed ? (root.danger ? #7a271a : #65a5f2) + private property button-fill: root.muted ? ControlStyle.disabled + : root.button-pressed ? (root.danger ? #7a271a : root.selected ? ControlStyle.selected-pressed : ControlStyle.pressed) : root.button-hovered ? (root.danger ? (root.selected ? #912018 : #b42318) - : root.selected ? #86baff : #a7ceff) + : root.selected ? ControlStyle.selected-hover : ControlStyle.hover) : root.danger ? (root.selected ? #b42318 : #d92d20) - : root.selected ? #b3d5fd : #deecff; + : root.selected ? ControlStyle.selected : ControlStyle.surface; private property button-foreground: root.muted ? #8a8f96 - : root.danger ? white : #24272d; + : root.danger ? white : root.selected ? ControlStyle.selected-text : #303f55; animate button-fill, button-foreground { duration: 90ms; easing: ease-out; @@ -242,14 +242,17 @@ component VideoSettingRow inherits VerticalLayout { Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; } HorizontalLayout { spacing: 4px; - for option[index] in root.options: Rectangle { + for option[index] in root.options: ShadowSurface { height: 27px; - background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff - : option-touch.has-hover ? #eaf3ff : #f5f6f8; + background: !root.enabled ? ControlStyle.disabled + : root.selected == index ? (option-touch.pressed ? ControlStyle.selected-pressed : option-touch.has-hover ? ControlStyle.selected-hover : ControlStyle.selected) + : option-touch.pressed ? ControlStyle.pressed + : option-touch.has-hover ? ControlStyle.hover : ControlStyle.surface; border-radius: UiRadius.control; - border-width: 1px; - border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6; - Text { text: option; color: root.enabled ? #30343b : #8b8f95; + shadow-color: root.enabled && root.selected == index ? UiShadow.focus : UiShadow.edge; + Text { text: option; color: !root.enabled ? #8b8f95 + : root.selected == index ? ControlStyle.selected-text : #30343b; + font-weight: root.selected == index ? 600 : 400; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } option-touch := TouchArea { mouse-cursor: root.enabled ? pointer : default; @@ -899,10 +902,10 @@ export component StreamWindow inherits Window { clicked => { root.close-stream-window(); } } } - Rectangle { + ShadowSeparator { y: parent.height - 1px; height: 1px; - background: #d5d3d0; + shadow-color: UiShadow.separator; } } @@ -922,11 +925,11 @@ export component StreamWindow inherits Window { z: 10; HorizontalLayout { spacing: 1px; - for tab[index] in root.tabs: tab-item := Rectangle { + for tab[index] in root.tabs: tab-item := ShadowSurface { width: min(190px, max(110px, (parent.width - 12px) / root.tabs.length)); background: index == root.selected-tab ? white : tab-touch.has-hover || root.dragging-tab == index ? #e5e7eb : #d8dbe0; - border-width: root.dragging-tab == index ? 1px : 0px; - border-color: #2463c7; + shadow-enabled: root.dragging-tab == index; + shadow-color: UiShadow.focus; Rectangle { width: parent.width - 27px; height: parent.height; @@ -1199,11 +1202,11 @@ export component StreamWindow inherits Window { tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; clicked => { root.close-control-menus(); root.toggle-fullscreen(); } } - if root.control-content-visible: Rectangle { + if root.control-content-visible: ShadowSeparator { opacity: root.control-content-progress; x: close-button.x - 6px; y: 12px; width: 1px; height: 14px; - background: #dbe5f1; + vertical: true; shadow-color: UiShadow.separator; } close-button := ControlBarButton { reveal-progress: root.control-content-progress; @@ -1408,12 +1411,12 @@ export component StreamWindow inherits Window { vertical-alignment: center; overflow: elide; } - if index < root.stats-rows.length - 2: Rectangle { + if index < root.stats-rows.length - 2: ShadowSeparator { x: 4px; y: parent.height - 1px; width: parent.width - 8px; height: 1px; - background: #edf0f4; + shadow-color: UiShadow.separator; } } @@ -1422,16 +1425,16 @@ export component StreamWindow inherits Window { text: StreamStrings.stats-encoder + "\n" + StreamStrings.stats-decoder; font-size: ImGuiFontStyle.small; } - Rectangle { + ShadowSurface { x: 0px; y: root.stats-header-height + root.stats-rows.length * stats-table.row-height + 70px; width: parent.width; height: 44px; background: #f7f9fc; border-radius: UiRadius.control; - border-width: 1px; border-color: #e8edf4; - Rectangle { + shadow-color: UiShadow.edge; + ShadowSeparator { x: 8px; y: 22px; width: parent.width - 16px; height: 1px; - background: #e8edf4; + shadow-color: UiShadow.separator; } for codec[index] in [ { label: StreamStrings.stats-encoder, value: root.stats-encoder }, @@ -1639,16 +1642,16 @@ export component StreamWindow inherits Window { padding: 8px; spacing: 1px; for display[index] in root.displays: Rectangle { height: 29px; - background: index == display-menu.hovered-index && display-touch.pressed ? #bfd4f2 - : index == display-menu.hovered-index ? #d8e6f8 - : index == root.selected-display ? #e8eef9 - : transparent; + background: index == root.selected-display + ? (index == display-menu.hovered-index ? (display-touch.pressed ? ControlStyle.selected-pressed : ControlStyle.selected-hover) : ControlStyle.selected) + : index == display-menu.hovered-index ? (display-touch.pressed ? ControlStyle.pressed : ControlStyle.hover) : transparent; border-radius: UiRadius.control; Text { x: 8px; width: parent.width - 34px; text: display; - color: #30343b; + color: index == root.selected-display ? ControlStyle.selected-text : #30343b; + font-weight: index == root.selected-display ? 600 : 400; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; @@ -1657,7 +1660,7 @@ export component StreamWindow inherits Window { x: parent.width - 24px; width: 16px; text: FontAwesomeIcons.check; - color: #2463c7; + color: ControlStyle.selected-text; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 10px; @@ -1695,7 +1698,9 @@ export component StreamWindow inherits Window { VerticalLayout { padding: 8px; spacing: 1px; for shortcut in root.shortcut-items: Rectangle { - height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent; + height: 29px; + border-radius: UiRadius.control; + background: shortcut-touch.pressed ? ControlStyle.pressed : shortcut-touch.has-hover ? ControlStyle.hover : transparent; Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; } shortcut-touch := TouchArea { mouse-cursor: MouseCursor.pointer; @@ -1745,7 +1750,7 @@ export component StreamWindow inherits Window { y: min(max(64px, parent.height * 0.18), max(16px, parent.height - self.height - 16px)); width: min(400px, parent.width - 48px); height: remote-update-message.y + remote-update-message.height + 84px; - border-color: #e5e9f0; + shadow-color: UiShadow.edge; border-radius: UiRadius.surface; drop-shadow-color: #07142b30; drop-shadow-blur: 32px; @@ -1774,8 +1779,8 @@ export component StreamWindow inherits Window { IconButton { x: parent.width - 44px; y: 12px; width: 32px; height: 32px; icon: FontAwesomeIcons.xmark; icon-size: 14px; - icon-color: #8a93a3; - hover-background: #f2f4f7; + icon-color: ControlStyle.icon; + normal-background: ControlStyle.surface; clicked => { root.dismiss-remote-update(); } } PrimaryButton { @@ -1789,10 +1794,10 @@ export component StreamWindow inherits Window { } // Transfer progress overlay at bottom-left. - if root.file-transfer-visible: Rectangle { + if root.file-transfer-visible: ShadowSurface { x: root.file-panel-x; y: root.file-panel-y; width: root.file-panel-width; height: root.file-panel-height; - background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.surface; z: 7; + background: #fffffff0; shadow-color: UiShadow.edge; border-radius: UiRadius.surface; z: 7; // Header/padding and drags leaving the panel belong to local UI. TouchArea { mouse-cursor: default; @@ -1801,6 +1806,7 @@ export component StreamWindow inherits Window { Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; } IconButton { x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px; + normal-background: ControlStyle.surface; clicked => { root.close-file-transfer(); } } ScrollView { @@ -1840,12 +1846,11 @@ export component StreamWindow inherits Window { } } - if root.custom-titlebar && !root.fullscreen-enabled: Rectangle { + if root.custom-titlebar && !root.fullscreen-enabled: ShadowSurface { width: parent.width; height: parent.height; background: transparent; - border-width: 1px; - border-color: #aaa8a6; + shadow-color: UiShadow.edge; border-radius: root.window-maximized ? 0px : root.window-corner-radius; z: 200; diff --git a/apps/desktop/src/gui/ui/ui_shadow.slint b/apps/desktop/src/gui/ui/ui_shadow.slint new file mode 100644 index 00000000..48e45ca9 --- /dev/null +++ b/apps/desktop/src/gui/ui/ui_shadow.slint @@ -0,0 +1,63 @@ +// Shared surface shadows use ordinary rectangles and gradients because Slint's +// software renderer does not draw drop-shadow-* effects. Keep layout and hit +// areas unchanged; only the decorative shadow extends beyond the surface. +export global UiShadow { + out property edge: #20334e; + out property hover: #315d92; + out property focus: #3564e3; + out property error: #d65252; + out property separator: #20334e14; +} + +export component ShadowSurface inherits Rectangle { + in property shadow-color: UiShadow.edge; + in property shadow-enabled: true; + in property shadow-focused: false; + // Small controls need a tighter falloff than cards and dialogs. + in property shadow-compact: false; + border-width: 0px; + border-top-left-radius: root.border-radius; + border-top-right-radius: root.border-radius; + border-bottom-left-radius: root.border-radius; + border-bottom-right-radius: root.border-radius; + + // Diffuse outwards, with more weight below the surface. A clipped surface + // keeps a faint inner falloff so native popup edges remain distinguishable. + for layer in (root.shadow-compact ? 3 : 6): Rectangle { + visible: root.shadow-enabled; + private property inset: root.clip ? layer * 0.5px : -(layer + 1) * 1px; + private property weight: (root.shadow-compact + ? (layer == 0 ? 0.09 : layer == 1 ? 0.035 : 0.01) + : (layer == 0 ? 0.09 : layer == 1 ? 0.07 + : layer == 2 ? 0.05 : layer == 3 ? 0.03 : layer == 4 ? 0.015 : 0.005)) + * (root.shadow-focused ? 2.5 : 1); + x: self.inset; y: self.inset; + width: max(0px, root.width - 2 * self.inset); + height: max(0px, root.height - 2 * self.inset); + border-top-left-radius: max(0px, root.border-top-left-radius - self.inset); + border-top-right-radius: max(0px, root.border-top-right-radius - self.inset); + border-bottom-left-radius: max(0px, root.border-bottom-left-radius - self.inset); + border-bottom-right-radius: max(0px, root.border-bottom-right-radius - self.inset); + border-width: 1px; + border-color: @linear-gradient(180deg, + root.shadow-color.mix(transparent, self.weight * 0.45) 0%, + root.shadow-color.mix(transparent, self.weight) 100%); + } + @children +} + +export component ShadowSeparator inherits Rectangle { + in property vertical: false; + in property shadow-color: UiShadow.separator; + height: 1px; + + Rectangle { + x: root.vertical ? -2px : 0px; + y: root.vertical ? 0px : -1px; + width: root.vertical ? 5px : root.width; + height: root.vertical ? root.height : 6px; + background: root.vertical + ? @linear-gradient(90deg, transparent 0%, root.shadow-color 50%, transparent 100%) + : @linear-gradient(180deg, transparent 0%, root.shadow-color 50%, transparent 100%); + } +} diff --git a/apps/desktop/src/gui/ui/workbench_controls.slint b/apps/desktop/src/gui/ui/workbench_controls.slint index 2d4aa93c..6e3c94c9 100644 --- a/apps/desktop/src/gui/ui/workbench_controls.slint +++ b/apps/desktop/src/gui/ui/workbench_controls.slint @@ -1,14 +1,13 @@ +import { ShadowSurface, UiShadow } from "ui_shadow.slint"; import { UiRadius } from "ui_radius.slint"; +import { ControlStyle } from "common.slint"; // Colors and spacing for the connection workbench; shapes are shared app-wide. export global WorkbenchStyle { - out property canvas: #f2f5f9; + out property canvas: #edf2f8; out property surface: #ffffff; - out property border: #c6cfdd; - out property control-border: #b2bfd1; - out property hover-border: #7f97b8; out property field-surface: #f5f7fb; - out property hover-surface: #eef3fb; - out property pressed-surface: #e4ebf6; + out property hover-surface: ControlStyle.hover; + out property pressed-surface: ControlStyle.pressed; out property separator: #e1e7ef; out property text: #1c2940; out property muted: #69788e; @@ -16,18 +15,20 @@ export global WorkbenchStyle { out property success: #18835a; } -export component WorkbenchButton inherits Rectangle { +export component WorkbenchButton inherits ShadowSurface { + shadow-compact: true; in property text; in property text-size: 13px; in property text-color: root.destructive || root.primary ? white : WorkbenchStyle.text; - in property normal-border-color: WorkbenchStyle.control-border; + in property normal-shadow-color: UiShadow.edge; + in property normal-background: ControlStyle.surface; in property icon; in property tooltip; in property tooltip-above: false; in property tooltip-suppressed: false; in property icon-color: root.destructive ? white : root.primary ? (root.soft && !touch.has-hover && !root.show-focus ? WorkbenchStyle.accent : white) - : WorkbenchStyle.muted; + : ControlStyle.icon; in property primary: false; in property destructive: false; in property soft: false; @@ -45,24 +46,23 @@ export component WorkbenchButton inherits Rectangle { width: 112px; height: 36px; border-radius: UiRadius.control; - border-width: root.show-focus ? 2px : root.soft ? 0px : 1px; - border-color: root.destructive - ? (root.show-focus ? #7a271a : touch.pressed ? #912018 : touch.has-hover ? #b42318 : #d92d20) - : root.show-focus ? (root.primary ? #193da0 : WorkbenchStyle.accent) - : root.primary ? WorkbenchStyle.accent - : touch.has-hover ? WorkbenchStyle.hover-border - : root.quiet ? transparent : root.normal-border-color; + shadow-enabled: root.show-focus || !root.soft; + shadow-focused: root.show-focus; + shadow-color: root.destructive ? UiShadow.error + : root.show-focus || root.primary ? UiShadow.focus + : touch.has-hover ? UiShadow.hover + : root.quiet ? transparent : root.normal-shadow-color; background: root.destructive ? (touch.pressed ? #912018 : touch.has-hover ? #b42318 : #d92d20) : root.soft ? (root.primary - ? (touch.pressed ? #254cc0 : touch.has-hover || root.show-focus ? WorkbenchStyle.accent : #edf2ff) - : (touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.field-surface)) + ? (touch.pressed ? #254cc0 : touch.has-hover || root.show-focus ? WorkbenchStyle.accent : #dce7ff) + : (touch.pressed ? ControlStyle.pressed : touch.has-hover ? ControlStyle.hover : ControlStyle.surface)) : root.primary ? (touch.pressed ? #254cc0 : touch.has-hover ? #4574f0 : WorkbenchStyle.accent) - : (touch.pressed ? WorkbenchStyle.pressed-surface : touch.has-hover ? WorkbenchStyle.hover-surface : root.quiet ? transparent : white); + : (touch.pressed ? ControlStyle.pressed : touch.has-hover ? ControlStyle.hover : root.quiet ? transparent : root.normal-background); opacity: root.enabled ? 1 : 0.45; - animate background, border-color { duration: 120ms; } + animate background, shadow-color { duration: 120ms; } forward-focus: button-focus; accessible-role: button; accessible-label: root.tooltip != "" ? root.tooltip : root.text; @@ -150,7 +150,7 @@ export component WorkbenchButton inherits Rectangle { } // A raised surface keeps transient action feedback distinct from the workbench. -export component WorkbenchNotice inherits Rectangle { +export component WorkbenchNotice inherits ShadowSurface { in property text; in property icon; in property error: false; @@ -159,8 +159,7 @@ export component WorkbenchNotice inherits Rectangle { width: min(root.width-limit, max(280px, message.preferred-width + 64px)); height: 64px; background: root.error ? #fff1f0 : #e9f6ef; - border-width: 1px; - border-color: root.error ? #f0c4bf : #b9dfc9; + shadow-color: root.error ? UiShadow.error : UiShadow.edge; border-radius: UiRadius.surface; drop-shadow-color: #14294424; drop-shadow-blur: 24px; @@ -192,9 +191,8 @@ export component WorkbenchNotice inherits Rectangle { } } -export component WorkbenchCard inherits Rectangle { +export component WorkbenchCard inherits ShadowSurface { background: WorkbenchStyle.surface; - border-width: 1px; - border-color: WorkbenchStyle.border; + shadow-color: UiShadow.edge; border-radius: UiRadius.surface; }