[ui] refine desktop shadows and control contrast

This commit is contained in:
dijunkun
2026-10-26 11:18:41 +08:00
parent e3b3f78b06
commit e551e04e8f
16 changed files with 557 additions and 412 deletions
+11 -10
View File
@@ -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 <string> 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 <string> title;
in property <string> subtitle;
in property <string> 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(); } }
+88 -61
View File
@@ -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 <color> border: #0000004d;
out property <color> separator: #6363639e;
out property <color> divider: #0000007a;
out property <color> control-separator: #b2b2b2;
}
export global FloatingBarStyle {
out property <length> corner-radius: UiRadius.menu;
}
// Filled controls remain distinct from white cards without restoring outlines.
export global ControlStyle {
out property <color> surface: #dce6f3;
out property <color> hover: #cedef1;
out property <color> pressed: #bfd2ec;
out property <color> selected: #bcd4f7;
out property <color> selected-hover: #adcaf1;
out property <color> selected-pressed: #99bce8;
out property <color> selected-text: #1d4e9e;
out property <color> disabled: #f0f2f5;
out property <color> icon: #52657f;
out property <color> input-surface: #dee7f2;
out property <color> input-focused: #e5ecf5;
out property <color> input-placeholder: #566a84;
// These tints over white match the field fills for standard LineEdit.
out property <color> input-tint: #235eaa26;
out property <color> 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 <bool> enabled: true;
in property <bool> danger: false;
in property <length> icon-size: 16px;
in property <color> icon-color: #24272d;
in property <color> icon-color: ControlStyle.icon;
in property <color> normal-background: transparent;
in property <color> hover-background: #edf0f5;
in property <color> pressed-background: #d9dde5;
in property <color> hover-background: ControlStyle.hover;
in property <color> pressed-background: ControlStyle.pressed;
in property <bool> 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 <int> current-index: 0;
in property <bool> enabled: true;
in property <bool> subtle: false;
in property <bool> show-arrow: true;
in property <color> normal-border-color: root.subtle ? #dfe6ef : #bfc2c7;
in property <color> hover-border-color: root.normal-border-color;
in property <color> normal-shadow-color: UiShadow.edge;
in property <color> hover-shadow-color: UiShadow.hover;
property <bool> popup-animation-enabled: false;
property <float> 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 <bool> checked;
in property <bool> 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 <string> text;
in property <length> text-size: ImGuiFontStyle.body;
in property <bool> 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 <string> text;
in property <length> text-size: ImGuiFontStyle.body;
in property <length> text-size: 12px;
in property <bool> 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 <string> text;
in property <length> text-size: ImGuiFontStyle.body;
in property <length> text-size: 12px;
in property <bool> 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;
}
@@ -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 <bool> 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 {
@@ -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(); }
}
}
@@ -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 <bool> 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; } }
@@ -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 {
+49 -44
View File
@@ -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 <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
private property <string> 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 <bool> 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; }
}
}
}
+29 -20
View File
@@ -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 <int> system-count: 2;
in property <bool> 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 <bool> embedded: false;
in property <bool> show-indicator: true;
in property <length> popup-min-width: 0px;
in property <length> text-size: 12px;
in property <int> text-weight: 400;
in property <color> text-color: #30343b;
in property <color> normal-border-color: #c9cdd2;
in property <color> hover-border-color: root.normal-border-color;
in property <color> focus-border-color: #8db5df;
in property <color> normal-background: ControlStyle.surface;
in property <color> normal-shadow-color: UiShadow.edge;
in property <color> hover-shadow-color: UiShadow.hover;
in property <color> focus-shadow-color: UiShadow.focus;
in-out property <int> current-index: 0;
out property <bool> has-focus: trigger-focus.has-focus || popup.is-open;
out property <bool> 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;
}
+140 -132
View File
@@ -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 <string> text;
in property <int> max-length: 0;
in property <length> text-size: ImGuiFontStyle.base;
in property <bool> error: false;
private property <string> 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 <string> device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name;
private property <string> host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host;
private property <string> 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;
+13 -11
View File
@@ -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;
@@ -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 <string> text;
in property <string> placeholder-text;
in property <InputType> input-type: InputType.text;
in property <bool> enabled: true;
in property <length> font-size: 12px;
in property <color> normal-border-color: #dfe6ef;
in property <color> normal-shadow-color: UiShadow.edge;
in property <brush> 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;
}
}
+14 -13
View File
@@ -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 <string> cancel: "取消";
}
component SessionButton inherits Rectangle {
component SessionButton inherits ShadowSurface {
in property <string> text;
in property <bool> danger: false;
in property <bool> 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 {
+18 -14
View File
@@ -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 <string> text;
in property <image> icon;
in property <bool> primary: false;
@@ -14,12 +16,13 @@ export component SettingsButton inherits Rectangle {
private property <length> 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
}
+48 -43
View File
@@ -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 <color> button-fill: root.muted ? #eef0f3
: root.button-pressed ? (root.danger ? #7a271a : #65a5f2)
private property <color> 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 <color> 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;
+63
View File
@@ -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 <color> edge: #20334e;
out property <color> hover: #315d92;
out property <color> focus: #3564e3;
out property <color> error: #d65252;
out property <color> separator: #20334e14;
}
export component ShadowSurface inherits Rectangle {
in property <color> shadow-color: UiShadow.edge;
in property <bool> shadow-enabled: true;
in property <bool> shadow-focused: false;
// Small controls need a tighter falloff than cards and dialogs.
in property <bool> 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 <length> inset: root.clip ? layer * 0.5px : -(layer + 1) * 1px;
private property <float> 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 <bool> vertical: false;
in property <color> 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%);
}
}
@@ -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 <color> canvas: #f2f5f9;
out property <color> canvas: #edf2f8;
out property <color> surface: #ffffff;
out property <color> border: #c6cfdd;
out property <color> control-border: #b2bfd1;
out property <color> hover-border: #7f97b8;
out property <color> field-surface: #f5f7fb;
out property <color> hover-surface: #eef3fb;
out property <color> pressed-surface: #e4ebf6;
out property <color> hover-surface: ControlStyle.hover;
out property <color> pressed-surface: ControlStyle.pressed;
out property <color> separator: #e1e7ef;
out property <color> text: #1c2940;
out property <color> muted: #69788e;
@@ -16,18 +15,20 @@ export global WorkbenchStyle {
out property <color> success: #18835a;
}
export component WorkbenchButton inherits Rectangle {
export component WorkbenchButton inherits ShadowSurface {
shadow-compact: true;
in property <string> text;
in property <length> text-size: 13px;
in property <color> text-color: root.destructive || root.primary ? white : WorkbenchStyle.text;
in property <color> normal-border-color: WorkbenchStyle.control-border;
in property <color> normal-shadow-color: UiShadow.edge;
in property <color> normal-background: ControlStyle.surface;
in property <string> icon;
in property <string> tooltip;
in property <bool> tooltip-above: false;
in property <bool> tooltip-suppressed: false;
in property <color> icon-color: root.destructive ? white : root.primary
? (root.soft && !touch.has-hover && !root.show-focus ? WorkbenchStyle.accent : white)
: WorkbenchStyle.muted;
: ControlStyle.icon;
in property <bool> primary: false;
in property <bool> destructive: false;
in property <bool> 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 <string> text;
in property <string> icon;
in property <bool> 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;
}