[ui] refine desktop layouts and unify control styling

This commit is contained in:
kunkundi
2026-10-14 19:29:08 +08:00
parent 3043b5f110
commit 2cf0d79543
19 changed files with 1366 additions and 816 deletions
@@ -4973,6 +4973,7 @@ void GuiApplication::RestoreWindowGeometry(bool before_show,
auto& window = component->window();
const auto current_size = window.size();
const bool wall_mode = gallery_window;
const float compact_width = component->get_minimum_window_width();
const float compact_height = component->get_minimum_window_height();
if (!before_show && ui_->loaded_window_scale[gallery_window] > 0) {
const float ratio = window.scale_factor() / ui_->loaded_window_scale[gallery_window];
@@ -4986,7 +4987,8 @@ void GuiApplication::RestoreWindowGeometry(bool before_show,
}
ui_->loaded_window_scale[gallery_window] = 0;
}
auto target_size = placement.RestoreSize(wall_mode, window.scale_factor(), compact_height);
auto target_size = placement.RestoreSize(wall_mode, window.scale_factor(),
compact_height, compact_width);
const auto& position = gallery_window
? placement.wall_position
: placement.connection_position;
@@ -5024,7 +5026,8 @@ void GuiApplication::RestoreWindowGeometry(bool before_show,
// placement. Only recover coordinates when its display is unavailable.
if (!found && available.w > 0 && available.h > 0) {
const auto margin = uint32_t(32 * window.scale_factor());
const auto minimum = placement.RestoreSize(false, window.scale_factor(), compact_height);
const auto minimum = placement.RestoreSize(false, window.scale_factor(),
compact_height, compact_width);
safe_size.width = std::max(minimum.width,
std::min(safe_size.width, uint32_t(available.w)));
safe_size.height = std::max(minimum.height, std::min(
@@ -5080,6 +5083,7 @@ void GuiApplication::SyncWindowGeometry(bool gallery_window) {
auto& placement = gallery_window ? ui_->gallery_window : ui_->connection_window;
const bool wall_mode = gallery_window;
const bool transitioning = gallery_window && ui_->gallery_transition.Active();
const float compact_width = component->get_minimum_window_width();
const float compact_height = component->get_minimum_window_height();
#if defined(__APPLE__)
const bool fullscreen = gallery_window && IsGalleryWindowFullscreen();
@@ -5091,16 +5095,17 @@ void GuiApplication::SyncWindowGeometry(bool gallery_window) {
if (!transitioning && placement.RecoverUndersized(wall_mode, window.position(), current_size,
window.scale_factor(), window.is_visible(),
window.is_minimized(), fullscreen, maximized,
compact_height)) {
compact_height, compact_width)) {
ui_->restore_ticks[gallery_window] = 0;
const auto target = placement.RestoreSize(wall_mode, window.scale_factor(),
compact_height);
compact_height, compact_width);
LOG_WARN("DeskHub undersized window: wall={} client={}x{} scale={}; restoring={}x{}",
wall_mode, current_size.width, current_size.height,
window.scale_factor(), target.width, target.height);
}
if (placement.Restoring() && ++ui_->restore_ticks[gallery_window] == 120) {
const auto target = placement.RestoreSize(wall_mode, window.scale_factor(), compact_height);
const auto target = placement.RestoreSize(wall_mode, window.scale_factor(),
compact_height, compact_width);
LOG_WARN("DeskHub window restore delayed: wall={} client={}x{} target={}x{} scale={} fullscreen={} maximized={}",
wall_mode, current_size.width, current_size.height, target.width,
target.height, window.scale_factor(), fullscreen, maximized);
@@ -5114,7 +5119,7 @@ void GuiApplication::SyncWindowGeometry(bool gallery_window) {
current_size.height, window.scale_factor(), fullscreen, maximized);
} else if (window.is_visible() && !window.is_minimized() && !transitioning) {
placement.Observe(wall_mode, window.position(), current_size, window.scale_factor(),
fullscreen, maximized, compact_height);
fullscreen, maximized, compact_height, compact_width);
}
}
@@ -24,14 +24,16 @@ struct WindowState {
void Observe(bool wall, slint::PhysicalPosition position,
slint::PhysicalSize size, float scale, bool fullscreen,
bool maximized, float compact_height = 486) {
bool maximized, float compact_height = 486,
float compact_width = 640) {
if (Restoring())
return;
if (!wall) {
connection_position = position;
return;
}
if (!fullscreen && !maximized && IsUndersized(size, scale, compact_height))
if (!fullscreen && !maximized &&
IsUndersized(size, scale, compact_height, compact_width))
return;
scale = std::max(scale, 1.0f);
wall_fullscreen = fullscreen;
@@ -46,12 +48,13 @@ struct WindowState {
bool RecoverUndersized(bool wall, slint::PhysicalPosition position,
slint::PhysicalSize size, float scale, bool visible,
bool minimized, bool fullscreen, bool maximized,
float compact_height = 486) {
float compact_height = 486,
float compact_width = 640) {
// Hidden/minimized/uninitialized windows can report temporary tiny bounds.
// Leave those alone, and let an in-flight restoration retain its target.
if (Restoring() || !visible || minimized || fullscreen || maximized ||
size.width == 0 || size.height == 0 ||
!IsUndersized(size, scale, compact_height))
!IsUndersized(size, scale, compact_height, compact_width))
return false;
if (wall) {
wall_position = position;
@@ -66,9 +69,11 @@ struct WindowState {
}
slint::PhysicalSize RestoreSize(bool wall, float scale,
float compact_height = 486) const {
float compact_height = 486,
float compact_width = 640) const {
const float width =
wall && wall_size ? std::max(640.0f, wall_size->width) : 640;
wall && wall_size ? std::max(compact_width, wall_size->width)
: compact_width;
const float height = wall && wall_size
? std::max(compact_height, wall_size->height)
: compact_height;
@@ -157,9 +162,9 @@ private:
std::optional<slint::PhysicalSize> target_, last_size_;
int matches_ = 0;
static bool IsUndersized(slint::PhysicalSize size, float scale,
float compact_height) {
float compact_height, float compact_width) {
scale = std::max(scale, 1.0f);
return double(size.width) + 1 < 640 * scale ||
return double(size.width) + 1 < compact_width * scale ||
double(size.height) + 1 < compact_height * scale;
}
static bool SameSize(slint::PhysicalSize a, slint::PhysicalSize b) {
+21 -16
View File
@@ -1,3 +1,4 @@
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 {
@@ -8,7 +9,7 @@ export global ImGuiLineStyle {
}
export global FloatingBarStyle {
out property <length> corner-radius: 6px;
out property <length> corner-radius: UiRadius.menu;
}
// ImGui loads a 32px font atlas and applies per-window font scales. These
@@ -76,7 +77,7 @@ export component IconButton inherits Rectangle {
background: touch.pressed ? (danger ? #d92d20 : root.pressed-background)
: touch.has-hover ? (danger ? #f04438 : root.hover-background)
: root.normal-background;
border-radius: 4px;
border-radius: UiRadius.control;
opacity: root.enabled ? 1 : 0.4;
Text {
@@ -109,7 +110,7 @@ export component IconButton inherits Rectangle {
width: tip.preferred-width + 12px;
height: 24px;
background: #22252be8;
border-radius: 4px;
border-radius: UiRadius.control;
z: 20;
tip := Text {
text: root.tooltip;
@@ -136,7 +137,7 @@ export component ImageButton inherits Rectangle {
background: touch.pressed ? (danger ? #d92d20 : #d9dde5)
: touch.has-hover ? (danger ? #f04438 : #edf0f5)
: transparent;
border-radius: 4px;
border-radius: UiRadius.control;
Image {
x: (parent.width - self.width) / 2;
@@ -157,7 +158,7 @@ export component ImageButton inherits Rectangle {
width: tip.preferred-width + 12px;
height: 24px;
background: #22252be8;
border-radius: 4px;
border-radius: UiRadius.control;
z: 20;
tip := Text {
text: root.tooltip;
@@ -175,6 +176,8 @@ export component CompactComboBox inherits Rectangle {
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;
property <bool> popup-animation-enabled: false;
property <float> popup-progress: 0.0;
callback selected(int);
@@ -189,8 +192,8 @@ export component CompactComboBox inherits Rectangle {
height: 24px;
background: root.enabled ? (root.subtle ? #f8fafc : white) : root.subtle ? #f1f3f6 : #f1f1f1;
border-width: 1px;
border-color: root.subtle ? #dfe6ef : #bfc2c7;
border-radius: root.subtle ? 6px : 3px;
border-color: root.enabled && combo-touch.has-hover ? root.hover-border-color : root.normal-border-color;
border-radius: UiRadius.control;
Text {
x: root.subtle ? 10px : 5px;
@@ -207,6 +210,8 @@ export component CompactComboBox inherits Rectangle {
height: parent.height - 2px;
y: 1px;
background: root.subtle ? transparent : root.enabled ? #9bc9f8 : #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;
@@ -223,7 +228,7 @@ export component CompactComboBox inherits Rectangle {
}
}
}
TouchArea {
combo-touch := TouchArea {
enabled: root.enabled; mouse-cursor: pointer;
clicked => {
root.popup-animation-enabled = false;
@@ -263,8 +268,8 @@ export component CompactComboBox inherits Rectangle {
opacity: root.popup-progress;
background: white;
border-width: 1px;
border-color: root.subtle ? #dfe6ef : #bfc2c7;
border-radius: root.subtle ? 6px : 3px;
border-color: root.normal-border-color;
border-radius: UiRadius.menu;
clip: true;
for choice[index] in root.model: Rectangle {
y: index * 23px;
@@ -288,8 +293,8 @@ export component CompactComboBox inherits Rectangle {
height: parent.height;
background: transparent;
border-width: 1px;
border-color: root.subtle ? #dfe6ef : #bfc2c7;
border-radius: root.subtle ? 6px : 3px;
border-color: root.normal-border-color;
border-radius: UiRadius.menu;
z: 2;
}
}
@@ -361,7 +366,7 @@ export component CompactButton inherits Rectangle {
: root.primary ? #a8d0fa : root.quiet ? transparent : #e2e4e7;
border-width: 1px;
border-color: root.primary ? #9bc3ed : root.quiet ? transparent : #c8cbd0;
border-radius: 3px;
border-radius: UiRadius.control;
Text {
text: root.text;
color: root.enabled ? #24272d : #8e9298;
@@ -385,7 +390,7 @@ export component PrimaryButton inherits Rectangle {
: touch.pressed ? #1849a9
: touch.has-hover ? #2970d6
: #2463c7;
border-radius: 5px;
border-radius: UiRadius.control;
Text {
text: root.text;
@@ -414,7 +419,7 @@ export component SecondaryButton inherits Rectangle {
: touch.has-hover ? #f3f4f6 : white;
border-width: 1px;
border-color: #c9ced7;
border-radius: 5px;
border-radius: UiRadius.control;
clip: true;
Text {
@@ -471,5 +476,5 @@ export component DialogSurface inherits Rectangle {
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 8px;
border-radius: UiRadius.surface;
}
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { IconButton, FontAwesomeIcons } from "common.slint";
import { GalleryStrings } from "device_strings.slint";
@@ -12,6 +13,7 @@ export global DeviceCardStyle {
component CardAction inherits IconButton {
width: 26px; height: 23px;
border-radius: UiRadius.control;
icon-size: 12px; icon-color: white;
hover-background: #ffffff2e;
pressed-background: #598cf273;
@@ -28,7 +30,7 @@ export component DeviceCardActions inherits Rectangle {
callback connect-device();
callback disconnect-device();
width: DeviceCardStyle.toolbar-width; height: 23px;
background: #4a4e55dc; border-radius: 5px;
background: #4a4e55dc; border-radius: UiRadius.control;
HorizontalLayout {
spacing: 0px;
CardAction {
@@ -1,4 +1,6 @@
import { ScrollView, LineEdit } from "std-widgets.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 { GalleryStrings } from "device_strings.slint";
@@ -23,7 +25,7 @@ export component DeviceGroupDialog inherits Rectangle {
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: 8px;
background: white; border-radius: UiRadius.surface;
border-width: 1px; border-color: #b2b2b2;
TouchArea {}
Text {
@@ -42,7 +44,8 @@ export component DeviceGroupDialog inherits Rectangle {
group-list := Rectangle {
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: 5px;
background: #fbfcfd; border-width: 1px; border-color: #e1e5eb; border-radius: UiRadius.surface;
clip: true;
ScrollView {
x: 1px; y: 1px; width: parent.width - 2px; height: parent.height - 2px;
viewport-width: self.width;
@@ -64,8 +67,21 @@ export component DeviceGroupDialog inherits Rectangle {
}
}
}
// Keep row hover backgrounds inside the rounded list on every renderer.
Rectangle {
x: -UiRadius.surface; y: -UiRadius.surface;
width: parent.width + 2 * UiRadius.surface;
height: parent.height + 2 * UiRadius.surface;
border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface;
border-color: white;
}
Rectangle {
width: parent.width; height: parent.height;
border-width: 1px; border-radius: UiRadius.surface;
border-color: #e1e5eb;
}
}
LineEdit {
RoundedLineEdit {
x: 18px; y: group-list.y + group-list.height + 12px;
width: parent.width - 36px; height: 30px;
text <=> root.new-group;
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { CompactButton, FontAwesomeIcons } from "common.slint";
export struct GalleryMenuItem { label: string, enabled: bool }
@@ -61,6 +62,7 @@ export component GalleryMenuButton inherits Rectangle {
key-pressed(event) => { return root.handle-key(event); }
CompactButton {
width: parent.width; height: parent.height;
border-radius: UiRadius.control;
enabled: root.enabled; primary: root.primary;
clicked => { trigger-focus.focus(); root.open-menu(); }
}
@@ -86,10 +88,10 @@ export component GalleryMenuButton inherits Rectangle {
popup-focus := FocusScope {
key-pressed(event) => { return root.handle-key(event); }
Rectangle {
background: white; border-width: 1px; border-color: #c9cdd2; border-radius: 4px;
background: white; border-width: 1px; border-color: #c9cdd2; border-radius: UiRadius.menu;
for item[index] in root.items: Rectangle {
x: 4px; y: 4px + index * 30px; width: parent.width - 8px; height: 30px;
border-radius: 3px;
border-radius: UiRadius.control;
background: item.enabled && index == root.current-index ? #edf4fc : transparent;
accessible-role: button; accessible-label: item.label; accessible-enabled: item.enabled;
accessible-action-default => { root.choose(index); }
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { GalleryStrings } from "device_strings.slint";
import { CompactButton } from "common.slint";
import { ScrollView } from "std-widgets.slint";
@@ -137,7 +138,7 @@ component NetworkRow inherits Rectangle {
}
if !root.heading && name-hover.has-hover && device-name.preferred-width > identity.width: Rectangle {
x: 12px; y: 32px; width: min(name-tip.preferred-width + 20px, root.width - 24px);
height: 26px; background: #293746; border-radius: 4px; z: 10;
height: 26px; background: #293746; border-radius: UiRadius.control; z: 10;
name-tip := Text {
x: 10px; width: parent.width - 20px; height: parent.height;
text: root.device.name; font-size: 11px; color: white;
@@ -172,7 +173,7 @@ export component GalleryNetworkDialog inherits Rectangle {
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: 8px;
background: white; border-radius: UiRadius.surface;
border-width: 1px; border-color: #c6cdd5;
TouchArea {}
FocusScope {
@@ -188,6 +189,7 @@ export component GalleryNetworkDialog inherits Rectangle {
font-size: 17px; font-weight: 600; color: #293746;
}
CompactButton {
border-radius: UiRadius.control;
x: parent.width - 80px; y: 17px; width: 60px; height: 26px;
text: GalleryStrings.tr("关闭", "Close", "Закрыть"); clicked => { root.close(); }
}
@@ -199,7 +201,7 @@ export component GalleryNetworkDialog inherits Rectangle {
}
NetworkRow {
x: 12px; y: 78px; width: parent.width - 38px; height: 30px;
heading: true; background: #f3f5f8; border-radius: 4px;
heading: true; background: #f3f5f8; border-radius: UiRadius.control;
device: {
name: GalleryStrings.tr("设备 / ID", "Device / ID", "Устройство / ID"),
};
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { GalleryStrings } from "device_strings.slint";
import { SettingsGroup, SettingsRow, SettingsSelect } from "settings_controls.slint";
@@ -13,6 +14,7 @@ export component GallerySettings inherits Rectangle {
label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Разрешение");
label-reserve: self.width - 112px;
SettingsSelect {
border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.resolution-index;
model: [GalleryStrings.tr("自动", "Auto", "Авто"),
@@ -24,6 +26,7 @@ export component GallerySettings inherits Rectangle {
label: GalleryStrings.tr("帧率", "Frame rate", "Частота кадров");
label-reserve: self.width - 112px;
SettingsSelect {
border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.frame-rate-index;
model: [GalleryStrings.tr("跟随设备设置", "Follow device settings", "Настройки устройства"), "15 FPS", "30 FPS", "60 FPS"];
@@ -33,22 +36,15 @@ export component GallerySettings inherits Rectangle {
label: GalleryStrings.tr("视频总带宽", "Video budget", "Бюджет видео");
label-reserve: self.width - 112px; divider: false;
SettingsSelect {
border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.bandwidth-index;
model: ["2 Mbps", "5 Mbps", "10 Mbps", "20 Mbps", "50 Mbps", "100 Mbps"];
}
}
}
Text {
x: 2px; y: 144px; width: parent.width - 4px; height: 70px;
text: GalleryStrings.tr(
"可见设备共享视频预算,优先保障展开画面;不可见设备暂停视频。音频与文件传输不计入,旧版设备不支持限额。瞬时速率可能超过预算。",
"Visible devices share the video budget, with priority for expanded views. Hidden video pauses. Audio, files and older devices are excluded; brief bursts may exceed the budget.",
"Бюджет видео делится между видимыми устройствами с приоритетом крупного вида. Скрытое видео приостановлено. Звук, файлы и старые устройства не учитываются; возможны краткие превышения.");
color: #718096; font-size: 11px; wrap: word-wrap;
}
if root.error != "": Text {
x: 2px; y: 224px; width: parent.width - 4px; height: 32px;
x: 2px; y: 144px; width: parent.width - 4px; height: 32px;
text: root.error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
}
}
+51 -31
View File
@@ -1,12 +1,18 @@
import { UiRadius } from "ui_radius.slint";
import { GalleryStrings } from "device_strings.slint";
import { RemoteCursor } from "remote_cursor.slint";
import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
import { GalleryMenuButton } from "gallery_menu_button.slint";
import { GroupFilter } from "group_filter.slint";
import { ScrollView, LineEdit } from "std-widgets.slint";
import { ScrollView } from "std-widgets.slint";
import { RoundedLineEdit } from "rounded_line_edit.slint";
import { CompactButton, SecondaryButton, IconButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint";
component GalleryToolbarButton inherits CompactButton {
component GalleryButton inherits CompactButton {
border-radius: UiRadius.control;
}
component GalleryToolbarButton inherits GalleryButton {
in property <string> alternate-text;
in property <length> horizontal-padding: 12px;
// Reserve both labels so toggling a mode never shifts adjacent controls.
@@ -487,7 +493,7 @@ export component GalleryView inherits Rectangle {
}
if root.batch && !root.simplified: Rectangle {
x: parent.width - 27px; y: parent.height - 27px;
width: 18px; height: 18px; border-radius: 5px; border-width: 1px;
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; }
@@ -560,14 +566,14 @@ export component GalleryView inherits Rectangle {
height: footer.y - 12px - self.y;
HorizontalLayout {
x: 0px; y: 0px; width: parent.width - root.view-controls-space; height: 24px; spacing: 8px;
CompactButton { width: 52px; text: GalleryStrings.tr("返回", "Back", "Назад"); clicked => { root.action("focus", ""); } }
GalleryButton { width: 52px; text: GalleryStrings.tr("返回", "Back", "Назад"); clicked => { root.action("focus", ""); } }
Rectangle {
horizontal-stretch: 1; min-width: 0px;
Text { x: 0px; width: parent.width; height: parent.height; text: root.focused.name; color: root.focused.online ? #303943 : DeviceCardStyle.offline-text; font-size: 12px; font-weight: 600; overflow: elide; horizontal-alignment: left; vertical-alignment: center; }
}
Rectangle {
width: min(focus-state.preferred-width + 16px, 110px); height: 22px;
border-radius: 5px;
border-radius: UiRadius.control;
background: root.controlling ? #e7f1fd : #f0f2f5;
focus-state := Text {
x: 8px; width: parent.width - 16px; height: parent.height;
@@ -589,12 +595,12 @@ export component GalleryView inherits Rectangle {
accessible-action-default => { self.clicked(); }
clicked => { root.action(root.group-controlling ? "group-release" : "group-control", root.focused.id); }
}
CompactButton { 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); } }
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 {
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: 6px;
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;
video := Rectangle {
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
@@ -666,7 +672,7 @@ export component GalleryView inherits Rectangle {
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: 5px;
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;
accessible-role: button;
accessible-label: self.details;
@@ -696,7 +702,8 @@ export component GalleryView inherits Rectangle {
if self.has-hover { self.show-details(); }
else if root.device-tooltip-id == tile.id { root.device-tooltip-id = ""; }
}
moved => { if self.has-hover { self.show-details(); } }
changed mouse-x => { if self.has-hover { self.show-details(); } }
changed mouse-y => { if self.has-hover { self.show-details(); } }
clicked => { root.device-tooltip-id = ""; root.action("focus", tile.id); }
}
}
@@ -786,7 +793,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: 3px;
border-color: #c8cbd0; border-radius: UiRadius.control;
text: GalleryStrings.tr("网络状态", "Network", "Сеть");
accessible-role: button;
accessible-label: self.text;
@@ -796,9 +803,9 @@ export component GalleryView inherits Rectangle {
}
pager := HorizontalLayout {
x: parent.width - self.width; y: 0px; width: 112px; height: 24px; spacing: 6px;
previous-page := CompactButton { primary: root.drag-page-direction < 0; width: 24px; text: "‹"; text-size: 16px; enabled: root.page > 0; clicked => { root.action("page", "previous"); } }
previous-page := GalleryButton { primary: root.drag-page-direction < 0; width: 24px; text: "‹"; text-size: 16px; enabled: root.page > 0; clicked => { root.action("page", "previous"); } }
Text { text: (root.page + 1) + " / " + root.pages; min-width: 0px; overflow: elide; horizontal-alignment: center; vertical-alignment: center; }
next-page := CompactButton { primary: root.drag-page-direction > 0; width: 24px; text: "›"; text-size: 16px; enabled: root.page + 1 < root.pages; clicked => { root.action("page", "next"); } }
next-page := GalleryButton { primary: root.drag-page-direction > 0; width: 24px; text: "›"; text-size: 16px; enabled: root.page + 1 < root.pages; clicked => { root.action("page", "next"); } }
}
}
// Paint the fullscreen tooltip above the grid/rail, below modal overlays.
@@ -815,6 +822,7 @@ export component GalleryView inherits Rectangle {
}
IconButton {
width: 24px; height: 24px;
border-radius: UiRadius.control;
icon: root.fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
clicked => { root.toggle-fullscreen(); }
@@ -836,7 +844,7 @@ export component GalleryView inherits Rectangle {
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: 4px;
background: #fffffff2; border-width: 1px; border-color: #76aeea; border-radius: UiRadius.surface;
Rectangle {
x: 8px; y: 9px; width: 64px; height: 36px;
background: root.dragged-tile.online ? #24272d : DeviceCardStyle.offline-preview;
@@ -857,7 +865,7 @@ export component GalleryView inherits Rectangle {
if root.swap-save-failed: Rectangle {
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: 4px;
background: #fff2f0; border-width: 1px; border-color: #e9b7b0; 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;
}
@@ -867,7 +875,7 @@ export component GalleryView inherits Rectangle {
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: 5px;
background: #22252bf2; border-width: 1px; border-color: #ffffff38; 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;
@@ -878,7 +886,7 @@ export component GalleryView inherits Rectangle {
TouchArea {}
add-dialog := Rectangle {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 400px; height: 306px;
background: white; border-radius: 8px; border-width: 1px; border-color: #b2b2b2;
background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2;
function submit() {
if root.add-device(device-id.text, password.text, group.text, remember.checked) {
password.text = "";
@@ -887,21 +895,21 @@ export component GalleryView inherits Rectangle {
}
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("添加设备", "Add device", "Добавить устройство"); font-size: 16px; }
Text { x: 18px; y: 56px; width: 72px; height: 30px; text: GalleryStrings.tr("设备 ID", "Device ID", "ID"); vertical-alignment: center; font-size: 12px; }
device-id := LineEdit {
device-id := RoundedLineEdit {
x: 96px; y: 56px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("9 位设备 ID", "9-digit device ID", "ID из 9 цифр");
input-type: InputType.number;
accepted => { password.focus(); }
}
Text { x: 18px; y: 100px; width: 72px; height: 30px; text: GalleryStrings.tr("密码", "Password", "Пароль"); vertical-alignment: center; font-size: 12px; }
password := LineEdit {
password := RoundedLineEdit {
x: 96px; y: 100px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("设备连接密码", "Device password", "Пароль устройства");
input-type: InputType.password;
accepted => { group.focus(); }
}
Text { x: 18px; y: 144px; width: 72px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); vertical-alignment: center; font-size: 12px; }
group := LineEdit {
group := RoundedLineEdit {
x: 96px; y: 144px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)");
accepted => { add-dialog.submit(); }
@@ -911,8 +919,8 @@ export component GalleryView inherits Rectangle {
TouchArea { clicked => { remember.checked = !remember.checked; } }
}
Text { x: 18px; y: 216px; width: 364px; height: 28px; text: root.notice; color: #8e4740; font-size: 11px; wrap: word-wrap; overflow: elide; }
CompactButton { x: 172px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } }
CompactButton {
GalleryButton { x: 172px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } }
GalleryButton {
x: 254px; y: 260px; width: 128px; height: 28px; primary: true;
text: GalleryStrings.tr("添加并连接", "Add and connect", "Подключить");
enabled: device-id.text != "" && password.text != "";
@@ -925,7 +933,7 @@ export component GalleryView inherits Rectangle {
TouchArea {}
Rectangle {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px;
background: white; border-radius: 8px; border-width: 1px; border-color: #b2b2b2;
background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2;
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; }
@@ -934,7 +942,7 @@ export component GalleryView inherits Rectangle {
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 {
x: 18px; y: 102px; width: 404px; height: 160px;
border-width: 1px; border-color: #d8dadd; border-radius: 4px; clip: true;
border-width: 1px; border-color: #d8dadd; 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;
@@ -955,12 +963,24 @@ export component GalleryView inherits Rectangle {
}
}
if root.recent-devices.length == 0: Text { text: GalleryStrings.tr("暂无最近连接", "No recent connections", "Нет недавних подключений"); color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
Rectangle {
x: -UiRadius.surface; y: -UiRadius.surface;
width: parent.width + 2 * UiRadius.surface;
height: parent.height + 2 * UiRadius.surface;
border-width: UiRadius.surface; border-radius: 2 * UiRadius.surface;
border-color: white;
}
Rectangle {
width: parent.width; height: parent.height;
border-width: 1px; border-radius: UiRadius.surface;
border-color: #d8dadd;
}
}
Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); font-size: 12px; vertical-alignment: center; }
import-group := LineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); }
import-group := RoundedLineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); }
Text { x: 18px; y: 309px; width: 404px; height: 20px; text: root.notice; font-size: 11px; color: #8e4740; overflow: elide; }
CompactButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } }
CompactButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add selected", "Добавить"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } }
GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } }
GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add selected", "Добавить"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } }
}
}
if root.confirming: Rectangle {
@@ -968,7 +988,7 @@ export component GalleryView inherits Rectangle {
Rectangle {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2;
width: 360px; height: confirmation-list.height + 118px;
background: white; border-radius: 8px; border-width: 1px; border-color: #b2b2b2;
background: white; border-radius: UiRadius.surface; border-width: 1px; border-color: #b2b2b2;
Text {
x: 20px; y: 18px; width: parent.width - 132px; height: 24px;
text: root.action-code == "disconnect" ? GalleryStrings.tr("断开连接", "Disconnect devices", "Отключение") : root.action-name;
@@ -976,13 +996,13 @@ export component GalleryView inherits Rectangle {
}
Rectangle {
x: parent.width - 108px; y: 20px; width: 88px; height: 22px;
background: #eff3f8; border-radius: 11px;
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 {
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: 6px; clip: true;
background: #fcfdfe; border-width: 1px; border-color: #e2e6eb; 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;
@@ -1002,8 +1022,8 @@ export component GalleryView inherits Rectangle {
}
}
}
CompactButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.confirming = false; } }
CompactButton {
GalleryButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.confirming = false; } }
GalleryButton {
x: parent.width - 126px; y: parent.height - 46px; width: 108px; height: 28px; primary: true;
text: root.action-code == "disconnect" ? GalleryStrings.tr("确认断开", "Disconnect", "Отключить") : GalleryStrings.tr("确认执行", "Continue", "Продолжить");
clicked => { root.confirming = false; root.action(root.action-code, ""); }
+9 -4
View File
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint";
import { FontAwesomeIcons } from "common.slint";
@@ -6,6 +7,9 @@ export component GroupFilter inherits Rectangle {
in property <[string]> model;
in property <int> system-count: 2;
in property <bool> enabled: true;
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-out property <int> current-index: 0;
callback selected(int);
private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count;
@@ -23,8 +27,9 @@ export component GroupFilter inherits Rectangle {
width: 155px; height: 28px;
background: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white;
border-width: 1px;
border-color: popup.is-open || trigger-focus.has-focus ? #8db5df : #c9cdd2;
border-radius: 4px;
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;
border-radius: UiRadius.control;
animate background, border-color { duration: 100ms; easing: ease-out; }
forward-focus: trigger-focus;
accessible-role: combobox;
@@ -117,7 +122,7 @@ export component GroupFilter inherits Rectangle {
width: parent.width; height: parent.height;
opacity: root.popup-progress;
background: white; border-width: 1px;
border-color: #c9cdd2; border-radius: 5px;
border-color: root.normal-border-color; border-radius: UiRadius.menu;
ScrollView {
x: 4px; y: 4px; width: parent.width - 8px; height: parent.height - 8px;
viewport-width: self.width;
@@ -131,7 +136,7 @@ export component GroupFilter inherits Rectangle {
}
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: 3px;
width: parent.width; height: 26px; border-radius: UiRadius.control;
background: index == root.current-index ? #edf4fc : option-touch.has-hover ? #f1f3f5 : transparent;
animate background { duration: 100ms; easing: ease-out; }
accessible-role: list-item;
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint";
import { CompactButton, DialogSurface, IconButton, FontAwesomeIcons, ImGuiFontStyle } from "common.slint";
@@ -68,12 +69,11 @@ export component MessagePanel inherits DialogSurface {
}
}
background: white;
border-radius: 7px;
border-radius: UiRadius.surface;
TouchArea { }
title-bar := Rectangle {
x: 0px; y: 0px; width: parent.width; height: 21px;
background: #d3d3d3; border-radius: 7px;
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
background: #d3d3d3; border-top-left-radius: UiRadius.surface; border-top-right-radius: UiRadius.surface;
message-heading := Text {
x: 6px; width: parent.width - 100px; height: parent.height;
text: MessageStrings.messages;
@@ -101,7 +101,7 @@ export component MessagePanel inherits DialogSurface {
}
IconButton {
x: parent.width - 25px; y: 2px; width: 19px; height: parent.height - 4px;
border-radius: 3px;
border-radius: UiRadius.control;
icon: FontAwesomeIcons.xmark; icon-size: ImGuiFontStyle.base;
icon-color: #202124;
hover-background: #bfc2c7; pressed-background: #b2b2b2;
@@ -113,7 +113,7 @@ export component MessagePanel inherits DialogSurface {
}
if root.notice-text != "": Rectangle {
x: 16px; y: title-bar.height + 10px; width: parent.width - 32px; height: 28px;
border-radius: 4px;
border-radius: UiRadius.surface;
background: #fef2f2;
Text {
x: 8px; width: parent.width - 16px; height: parent.height;
@@ -133,7 +133,7 @@ export component MessagePanel inherits DialogSurface {
for item[index] in root.items: Rectangle {
height: 54px;
background: root.selected-id == item.id ? #eaf2ff : entry-touch.has-hover ? #f0f1f2 : transparent;
border-radius: 4px;
border-radius: UiRadius.control;
Text { x: 8px; y: 6px; width: parent.width - 32px; height: 18px; text: item.title; overflow: elide; font-size: ImGuiFontStyle.base; font-weight: item.read ? 400 : 600; color: #202124; vertical-alignment: center; }
if !item.read: Rectangle {
x: parent.width - 17px; y: 13px; width: 5px; height: 5px;
@@ -0,0 +1,50 @@
import { LineEdit } from "std-widgets.slint";
import { UiRadius } from "ui_radius.slint";
// Retain the standard editor's selection, clipboard, clear button and input
// method support. Only its outer frame is replaced by the shared 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 <brush> surrounding-background: white;
callback accepted();
callback edited();
height: 30px;
border-radius: UiRadius.control;
background: white;
clip: true;
forward-focus: editor;
editor := LineEdit {
x: -1px; y: -1px;
width: parent.width + 2px; height: parent.height + 2px;
text <=> root.text;
placeholder-text: root.placeholder-text;
input-type: root.input-type;
enabled: root.enabled;
font-size: root.font-size;
accepted => { root.accepted(); }
edited => { root.edited(); }
}
// The software renderer clips children to rectangular bounds. Mask the
// standard editor's corners before drawing our rounded border on top.
Rectangle {
x: -root.border-radius; y: -root.border-radius;
width: parent.width + 2 * root.border-radius;
height: parent.height + 2 * root.border-radius;
border-width: root.border-radius;
border-radius: 2 * root.border-radius;
border-color: root.surrounding-background;
}
Rectangle {
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;
background: transparent;
}
}
+6 -5
View File
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint";
import { FontAwesomeIcons, ImGuiLineStyle, FloatingBarStyle } from "common.slint";
@@ -27,7 +28,7 @@ component SessionButton inherits Rectangle {
callback clicked;
width: label.preferred-width + 20px;
height: 28px;
border-radius: 5px;
border-radius: UiRadius.control;
border-width: 1px;
border-color: root.danger ? #d92d20 : #c9ced7;
background: root.danger ? (root.pointer-feedback && touch.pressed ? #b42318 : root.pointer-feedback && touch.has-hover ? #f04438 : #d92d20)
@@ -73,7 +74,7 @@ component SessionButton inherits Rectangle {
}
if root.enabled && keyboard.has-focus && root.keyboard-focus: Rectangle {
x: 2px; y: 2px; width: parent.width - 4px; height: parent.height - 4px;
border-radius: 3px; border-width: 1px; border-color: root.danger ? white : #2463c7;
border-radius: UiRadius.control - 2px; border-width: 1px; border-color: root.danger ? white : #2463c7;
}
}
@@ -172,7 +173,7 @@ export component ServerWindow inherits Window {
Rectangle {
width: root.width; height: root.height;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border;
border-radius: 8px;
border-radius: UiRadius.surface;
Text {
x: 10px; y: 10px; width: parent.width - 20px; height: 24px;
text: ServerStrings.heading + (root.controllers.length > 1 ? " · " + root.controllers.length : "");
@@ -189,7 +190,7 @@ export component ServerWindow inherits Window {
select := Rectangle {
x: 0px; y: 0px;
width: parent.width - (root.controllers.length > 1 ? disconnect.width + 4px : status.preferred-width + 8px);
height: parent.height; border-radius: 4px;
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 {
x: 4px; y: (parent.height - 13px) / 2; width: 13px; height: 13px;
@@ -274,7 +275,7 @@ export component ServerWindow inherits Window {
// Draw outside the scrolling list so its last row can reveal its name.
if root.hovered-name != "": Rectangle {
x: 10px; y: root.tooltip-y; width: 205px; height: tip.preferred-height + 12px; z: 20;
background: #24272d; border-radius: 5px;
background: #24272d; border-radius: UiRadius.control;
tip := Text { x: 6px; y: 6px; width: parent.width - 12px; text: root.hovered-name; color: white; font-size: 12px; wrap: word-wrap; }
}
}
@@ -1,4 +1,6 @@
import { UiRadius } from "ui_radius.slint";
import { CompactComboBox, ToggleSwitch } from "common.slint";
import { WorkbenchStyle } from "workbench_controls.slint";
// Shared visual treatment for the global settings pages.
export component SettingsButton inherits Rectangle {
@@ -8,12 +10,13 @@ export component SettingsButton inherits Rectangle {
in property <bool> align-left: false;
in property <bool> enabled: true;
callback clicked();
width: 88px; height: 32px; border-radius: 7px;
width: 88px; height: 32px; border-radius: UiRadius.control;
background: !root.enabled ? #f0f3f7
: 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: #dce3ec;
border-color: !root.enabled ? WorkbenchStyle.border
: touch.has-hover ? WorkbenchStyle.hover-border : WorkbenchStyle.control-border;
accessible-role: button; accessible-label: root.text;
accessible-action-default => { if root.enabled { root.clicked(); } }
Text {
@@ -28,6 +31,8 @@ export component SettingsButton inherits Rectangle {
export component SettingsSelect inherits CompactComboBox {
subtle: true;
show-arrow: false;
normal-border-color: WorkbenchStyle.control-border;
hover-border-color: WorkbenchStyle.hover-border;
height: 28px;
}
@@ -41,7 +46,7 @@ export component SettingsGroup inherits Rectangle {
}
Rectangle {
y: 24px; width: parent.width; height: parent.height - self.y;
background: white; border-radius: 9px; border-color: #e5eaf1; border-width: 1px;
background: white; border-radius: UiRadius.surface; border-color: WorkbenchStyle.border; border-width: 1px;
VerticalLayout { padding: 0px; spacing: 0px; @children }
}
}
@@ -58,7 +63,7 @@ export component SettingsRow inherits Rectangle {
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: #eff2f6;
x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; background: #d9e1ec;
}
@children
}
+14 -13
View File
@@ -1,3 +1,4 @@
import { UiRadius } from "ui_radius.slint";
import { ComboBox, ScrollView } from "std-widgets.slint";
import { RemoteCursor } from "remote_cursor.slint";
import { FontAwesomeIcons, IconButton, PrimaryButton, DialogSurface, ImGuiLineStyle, ImGuiFontStyle, FloatingBarStyle } from "common.slint";
@@ -98,7 +99,7 @@ component ControlBarButton inherits Rectangle {
background: root.muted ? #eef0f3 : touch.pressed && root.action-enabled ? #0f87fa
: touch.has-hover ? #4296fa
: root.selected ? #4296fa : #4296fa66;
border-radius: 3px;
border-radius: UiRadius.control;
Text {
text: root.icon;
@@ -159,7 +160,7 @@ component VideoSettingRow inherits VerticalLayout {
height: 27px;
background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff
: option-touch.has-hover ? #eaf3ff : #f5f6f8;
border-radius: 4px;
border-radius: UiRadius.control;
border-width: 1px;
border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6;
Text { text: option; color: root.enabled ? #30343b : #8b8f95;
@@ -967,7 +968,7 @@ export component StreamWindow inherits Window {
width: parent.width;
height: root.stats-header-height;
background: #f3f5f8;
border-radius: 4px;
border-radius: UiRadius.control;
Text {
x: stats-table.label-column-width + 4px;
width: stats-table.value-column-width - 8px;
@@ -1004,7 +1005,7 @@ export component StreamWindow inherits Window {
width: parent.width;
height: stats-table.row-height;
background: index == root.stats-rows.length - 1 ? #edf3fc : transparent;
border-radius: 4px;
border-radius: UiRadius.control;
Text {
x: 4px;
width: stats-table.label-column-width - 8px;
@@ -1133,7 +1134,7 @@ export component StreamWindow inherits Window {
x: root.video-menu-x; y: root.video-menu-y;
width: root.video-menu-width; height: root.video-menu-height;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border;
border-radius: 5px; z: 11;
border-radius: UiRadius.menu; z: 11;
TouchArea {
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
scroll-event(event) => { accept }
@@ -1183,7 +1184,7 @@ export component StreamWindow inherits Window {
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 5px;
border-radius: UiRadius.menu;
clip: true;
z: 11;
@@ -1195,7 +1196,7 @@ export component StreamWindow inherits Window {
: index == display-menu.hovered-index ? #d8e6f8
: index == root.selected-display ? #e8eef9
: transparent;
border-radius: 3px;
border-radius: UiRadius.control;
Text {
x: 8px;
width: parent.width - 34px;
@@ -1246,7 +1247,7 @@ export component StreamWindow inherits Window {
if root.shortcut-menu-open: Rectangle {
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: 68px;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.menu; z: 11;
VerticalLayout {
padding: 4px; spacing: 1px;
for shortcut in ["Ctrl+Alt+Del", "Win+L"]: Rectangle {
@@ -1280,7 +1281,7 @@ export component StreamWindow inherits Window {
// Anchor to the button row so expanded stats stay beneath the tooltip.
y: control.y + root.control-toolbar-height + self.height + 8px <= video.height
? control.y + root.control-toolbar-height + 4px : max(4px, control.y - self.height - 4px);
background: #30343bf5; border-radius: 5px; z: 12;
background: #30343bf5; border-radius: UiRadius.control; z: 12;
control-tip := Text {
x: 8px; y: 6px; width: parent.width - 16px;
text: root.hovered-control.text;
@@ -1304,7 +1305,7 @@ export component StreamWindow inherits Window {
width: min(520px, parent.width - 48px);
height: max(208px, remote-update-message.y + remote-update-message.height + 92px);
border-color: #e5e9f0;
border-radius: 14px;
border-radius: UiRadius.surface;
drop-shadow-color: #07142b30;
drop-shadow-blur: 32px;
drop-shadow-offset-y: 12px;
@@ -1320,7 +1321,7 @@ export component StreamWindow inherits Window {
}
Rectangle {
x: 32px; y: 32px; width: 48px; height: 48px;
background: #edf4ff; border-radius: 12px;
background: #edf4ff; border-radius: UiRadius.surface;
Path {
x: 12px; y: 12px; width: 24px; height: 24px;
viewbox-width: 24; viewbox-height: 24;
@@ -1355,7 +1356,7 @@ export component StreamWindow inherits Window {
PrimaryButton {
x: parent.width - 136px; y: parent.height - 68px;
width: 104px; height: 36px;
border-radius: 7px;
border-radius: UiRadius.control;
text: StreamStrings.remote-update-dismiss;
text-size: 14px;
clicked => { root.dismiss-remote-update(); }
@@ -1365,7 +1366,7 @@ export component StreamWindow inherits Window {
// Transfer progress overlay at bottom-left.
if root.file-transfer-visible: Rectangle {
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 6px; z: 7;
background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.surface; z: 7;
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; clicked => { root.close-file-transfer(); } }
ScrollView {
+8
View File
@@ -0,0 +1,8 @@
// Logical pixels: compact controls, popup menus, and larger surfaces.
// Circular indicators, switches, progress bars, and native window chrome
// retain radii derived from their own geometry.
export global UiRadius {
out property <length> control: 6px;
out property <length> menu: 8px;
out property <length> surface: 12px;
}
@@ -0,0 +1,124 @@
import { UiRadius } from "ui_radius.slint";
// Colors and spacing for the connection workbench; shapes are shared app-wide.
export global WorkbenchStyle {
out property <color> canvas: #f2f5f9;
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> separator: #e1e7ef;
out property <color> text: #1c2940;
out property <color> muted: #69788e;
out property <color> accent: #3564e3;
out property <color> success: #18835a;
}
export component WorkbenchButton inherits Rectangle {
in property <string> text;
in property <string> icon;
in property <string> tooltip;
in property <bool> primary: false;
in property <bool> quiet: false;
in property <bool> enabled: true;
private property <bool> keyboard-focus: false;
private property <bool> show-focus: button-focus.has-focus && root.keyboard-focus;
callback clicked;
width: 112px;
height: 36px;
border-radius: UiRadius.control;
border-width: root.show-focus ? 2px : 1px;
border-color: root.show-focus ? (root.primary ? #193da0 : WorkbenchStyle.accent)
: root.primary ? WorkbenchStyle.accent
: touch.has-hover ? WorkbenchStyle.hover-border
: root.quiet ? transparent : WorkbenchStyle.control-border;
background: 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);
opacity: root.enabled ? 1 : 0.45;
animate background, border-color { duration: 120ms; }
forward-focus: button-focus;
accessible-role: button;
accessible-label: root.tooltip != "" ? root.tooltip : root.text;
accessible-enabled: root.enabled;
accessible-action-default => { if root.enabled { root.clicked(); } }
HorizontalLayout {
padding-left: 10px;
padding-right: 10px;
spacing: 8px;
alignment: center;
if root.icon != "": Text {
text: root.icon;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 14px;
color: root.primary ? white : WorkbenchStyle.muted;
vertical-alignment: center;
accessible-role: none;
}
if root.text != "": Text {
text: root.text;
font-size: 13px;
font-weight: root.primary ? 600 : 500;
color: root.primary ? white : WorkbenchStyle.text;
vertical-alignment: center;
accessible-role: none;
}
}
button-focus := FocusScope {
enabled: root.enabled;
focus-gained(reason) => {
if reason == FocusReason.tab-navigation {
root.keyboard-focus = true;
} else if reason == FocusReason.pointer-click {
root.keyboard-focus = false;
}
}
key-pressed(event) => {
root.keyboard-focus = true;
if event.text == Key.Return || event.text == " " {
root.clicked();
return accept;
}
return reject;
}
touch := TouchArea {
enabled: root.enabled;
mouse-cursor: pointer;
pointer-event(event) => {
if event.kind == PointerEventKind.down {
root.keyboard-focus = false;
}
}
// Keep focus for subsequent keyboard activation without leaving a
// keyboard focus ring behind after a pointer click.
clicked => { root.keyboard-focus = false; button-focus.focus(); root.clicked(); }
}
}
if touch.has-hover && root.tooltip != "": Rectangle {
x: min(0px, root.width - self.width);
y: root.height + 6px;
width: tip.preferred-width + 16px;
height: 28px;
background: #24324a;
border-radius: UiRadius.control;
z: 20;
tip := Text {
text: root.tooltip;
font-size: 11px;
color: white;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
export component WorkbenchCard inherits Rectangle {
background: WorkbenchStyle.surface;
border-width: 1px;
border-color: WorkbenchStyle.border;
border-radius: UiRadius.surface;
}
+420 -7
View File
@@ -1,5 +1,7 @@
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include "features/gallery/gallery_window_state.h"
#include "ui/release_notes_text.h"
#include "ui/ui_localization.h"
#include <algorithm>
@@ -125,9 +127,16 @@ bool ConfigureDocumentationDemo(
{"987654321", "Dev Workstation", "fixtures/source.png"},
{"246813579", "Office PC", "fixtures/website.png"},
{"135792468", "Home Laptop", "web-client.png"},
{"864209753", "Studio Mac", "fixtures/source.png"},
{"753198642", "Build Server", "fixtures/website.png"},
{"642087531", "Test Device", "web-client.png"},
};
std::vector<crossdesk::ui::RecentConnection> connections;
for (const auto &device : devices) {
if (connections.size() == 3 && options.page != "main-recent-overflow" &&
options.page != "main-recent-scrolled") {
break;
}
const std::string path = options.demo_assets_path + "/" + device.thumbnail;
const auto thumbnail =
slint::Image::load_from_path(slint::SharedString(path.c_str()));
@@ -219,8 +228,23 @@ void ConfigureMainCapturePage(
window->set_wayland_titlebar(false);
#endif
if (options.page == "settings") {
if (options.page == "settings" || options.page == "settings-scroll-hover" ||
options.page == "settings-scrolled" || options.page == "settings-scroll-drag") {
window->set_settings_open(true);
} else if (options.page == "main-menu-badges") {
window->set_announcement_unread(4);
window->set_update_available(true);
window->set_update_open(false);
} else if (options.page == "settings-deskhub" ||
options.page == "settings-deskhub-error") {
window->set_settings_open(true);
window->set_settings_tab(1);
if (options.page == "settings-deskhub-error") {
window->set_gallery_settings_error(
window->global<crossdesk::ui::GalleryStrings>().invoke_tr(
"无法保存设置,请重试。", "Unable to save settings. Please try again.",
"Не удалось сохранить настройки. Повторите попытку."));
}
} else if (options.page == "self-hosted") {
window->set_settings_open(true);
window->set_self_host_settings_open(true);
@@ -238,11 +262,52 @@ void ConfigureMainCapturePage(
? crossdesk::ui::UpdateCheckStatus::Failed
: crossdesk::ui::UpdateCheckStatus::Available);
}
} else if (options.page == "update") {
} else if (options.page == "update" || options.page == "update-scrolled" ||
options.page == "update-short") {
window->set_update_available(true);
window->set_latest_version("v0.0.1");
window->set_latest_version("v1.6.1-20261010");
window->set_release_name("CrossDesk Preview");
window->set_release_date("2026-08-25");
window->set_release_date("2026-10-10");
// Illustrative notes exercise the production Markdown parser and wrapping,
// including enough content to inspect both ends of the scrolling viewport.
const char *notes[] = {
"**新增功能与改进**\n\n"
"1. 增强信令快速重连与连接恢复能力;桌面端配置修改无需重新连接信令服务。\n"
"2. Windows 安装版新增登录前远程控制支持,被控端可随系统启动,在尚未登录时接受远程连接。\n"
"3. Linux 提供普通版与 Xfce 虚拟桌面版独立安装包,普通版不再强制安装 Xfce 和 Xvfb。\n"
"4. 被控端会话窗口改为紧凑的屏幕边缘状态栏。\n"
"5. 完善 iOS / Android 原生控制端,优化远程画面、触控与外接键鼠体验。\n\n"
"**问题修复**\n\n"
"6. 修复连接中断后的状态显示,改善高分辨率屏幕上的文字与按钮边界。\n"
"7. 统一窗口、输入框与按钮的圆角,优化更新提示与长文本滚动体验。",
"**Features and improvements**\n\n"
"1. Improve signaling reconnection and session recovery. Desktop configuration changes no longer require reconnecting to the signaling service.\n"
"2. Add remote control before Windows sign-in, allowing the installed client to accept connections when the system starts.\n"
"3. Provide separate Linux packages for standard installations and Xfce virtual desktops.\n"
"4. Replace the controlled session window with a compact status bar at the edge of the screen.\n"
"5. Improve the native iOS and Android clients, including video, touch, and external keyboard and mouse support.\n\n"
"**Fixes**\n\n"
"6. Correct connection status after interruptions and improve text and control borders on high resolution displays.\n"
"7. Unify corner radii and improve update notifications and scrolling for long release notes.",
"**Новые возможности и улучшения**\n\n"
"1. Улучшено восстановление соединения. Изменение настроек больше не требует повторного подключения к серверу сигнализации.\n"
"2. Добавлено удалённое управление Windows до входа пользователя в систему.\n"
"3. Подготовлены отдельные пакеты Linux для стандартной установки и виртуальных рабочих столов Xfce.\n"
"4. Окно управляемого сеанса заменено компактной строкой состояния у края экрана.\n"
"5. Улучшены клиенты iOS и Android, работа с сенсорным экраном, внешней клавиатурой и мышью.\n\n"
"**Исправления**\n\n"
"6. Исправлено отображение состояния после обрыва соединения, улучшена читаемость на экранах высокого разрешения.\n"
"7. Унифицированы скругления элементов и улучшена прокрутка длинных описаний обновлений."};
std::vector<crossdesk::ui::ReleaseNoteBlock> blocks;
for (const auto &parsed : crossdesk::release_notes_text::Parse(notes[language])) {
crossdesk::ui::ReleaseNoteBlock block{};
block.content = parsed.content;
block.section_gap = parsed.section_gap;
blocks.push_back(std::move(block));
if (options.page == "update-short" && blocks.size() == 2) break;
}
window->set_release_note_blocks(
std::make_shared<slint::VectorModel<crossdesk::ui::ReleaseNoteBlock>>(blocks));
window->set_update_open(true);
} else if (options.page == "reset-password") {
window->set_reset_password_open(true);
@@ -363,10 +428,72 @@ int RunCaptureMode(
slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
slint::ComponentHandle<crossdesk::ui::ServerWindow> &server) {
window->on_format_remote_id([](slint::SharedString id) { return id; });
ConfigureMainCapturePage(window, options);
if (!ConfigureDocumentationDemo(window, options)) {
return 8;
}
// Preview DeskHub with the same demo images, without opening any sessions.
if (options.page.starts_with("gallery")) {
window->set_gallery_mode(true);
std::vector<crossdesk::ui::GalleryTile> tiles;
const auto recent = window->get_recent_connections();
for (size_t index = 0; index < recent->row_count(); ++index) {
const auto device = recent->row_data(index).value();
crossdesk::ui::GalleryTile tile{};
tile.id = device.remote_id;
tile.name = device.display_name;
tile.online = tile.presence_known = tile.connected = true;
tile.has_frame = device.has_thumbnail;
tile.frame = device.thumbnail;
tile.selected = options.page == "gallery-batch";
tiles.push_back(tile);
}
window->set_gallery_tiles(
std::make_shared<slint::VectorModel<crossdesk::ui::GalleryTile>>(tiles));
window->set_gallery_count(static_cast<int>(tiles.size()));
window->set_gallery_connected(static_cast<int>(tiles.size()));
window->set_gallery_groups(
std::make_shared<slint::VectorModel<slint::SharedString>>(
std::vector{window->global<crossdesk::ui::GalleryStrings>().invoke_tr(
"全部设备", "All devices", "Все устройства")}));
window->set_gallery_batch(options.page == "gallery-batch");
window->set_gallery_selected(options.page == "gallery-batch"
? static_cast<int>(tiles.size()) : 0);
window->set_gallery_adding(options.page == "gallery-add");
if (options.page == "gallery-focus" && !tiles.empty())
window->set_gallery_focused(tiles.front());
// A 960 x 720 preview includes the native 22px macOS titlebar.
const float client_height =
#if defined(__APPLE__)
698.0f;
#else
720.0f;
#endif
window->window().set_size(
slint::LogicalSize(slint::Size<float>{960.0f, client_height}));
}
// Workbench states use the same rendered components and demo-only data.
if (options.page == "main-empty" || options.page == "main-empty-group") {
window->set_recent_connections(
std::make_shared<slint::VectorModel<crossdesk::ui::RecentConnection>>());
window->set_recent_group_index(options.page == "main-empty-group" ? 1 : 0);
} else if (options.page == "main-error") {
window->set_remote_id_input("123");
window->set_connection_input_error(
window->global<crossdesk::ui::GalleryStrings>().invoke_tr(
"请输入完整的 9 位设备 ID", "Enter a complete 9-digit device ID",
"Введите полный ID устройства из 9 цифр"));
} else if (options.page == "main-offline") {
window->set_signal_connected(false);
window->set_password_visible(false);
const auto connections = window->get_recent_connections();
for (size_t index = 0; index < connections->row_count(); ++index) {
auto device = connections->row_data(index).value();
device.online = false;
connections->set_row_data(index, device);
}
}
stream->hide();
server->hide();
@@ -422,11 +549,113 @@ int RunCaptureMode(
// taking the snapshot; capturing immediately after show() can yield 1x.
bool snapshot_written = false;
slint::Timer snapshot_timer;
slint::Timer capture_settle_timer;
slint::Timer pointer_move_timer;
snapshot_timer.start(slint::TimerMode::SingleShot,
std::chrono::milliseconds(250), [&] {
snapshot_written =
WriteWindowSnapshot(*capture_window, options.snapshot_path.c_str());
slint::quit_event_loop();
if (options.page == "main-device-tooltip" ||
options.page == "main-device-tooltip-moved" ||
options.page == "main-device-tooltip-edge" ||
options.page == "main-device-tooltip-exit") {
const float titlebar = window->get_minimum_window_height() - 472.0f;
window->window().dispatch_pointer_move_event(
slint::LogicalPosition(slint::Point<float>{90.0f, titlebar + 330.0f}));
if (options.page != "main-device-tooltip") {
// A second event in the same card verifies hover movement without
// pressing the mouse or relying on another enter/leave event.
pointer_move_timer.start(slint::TimerMode::SingleShot,
std::chrono::milliseconds(50), [&, titlebar] {
const bool edge = options.page == "main-device-tooltip-edge";
const bool leave = options.page == "main-device-tooltip-exit";
window->window().dispatch_pointer_move_event(slint::LogicalPosition(
slint::Point<float>{leave ? 2.0f : edge ? window->get_minimum_window_width() - 40.0f : 170.0f,
leave ? 2.0f : titlebar + 340.0f}));
});
}
} else if (options.page == "settings-scroll-hover" ||
options.page == "settings-scrolled" ||
options.page == "settings-scroll-drag") {
const float dialog_x = (window->get_minimum_window_width() - 400.0f) / 2;
const float dialog_y = (window->get_minimum_window_height() - 446.0f) / 2;
const auto scrollbar_position = slint::LogicalPosition(
slint::Point<float>{dialog_x + 389.0f, dialog_y + 150.0f});
if (options.page == "settings-scrolled") {
window->window().dispatch_pointer_scroll_event(
slint::LogicalPosition(slint::Point<float>{320.0f, 260.0f}),
0.0f, -2000.0f);
}
window->window().dispatch_pointer_move_event(scrollbar_position);
if (options.page == "settings-scroll-drag") {
window->window().dispatch_pointer_press_event(
scrollbar_position, slint::PointerEventButton::Left);
const auto drag_end = slint::LogicalPosition(
slint::Point<float>{dialog_x + 389.0f, dialog_y + 340.0f});
window->window().dispatch_pointer_move_event(drag_end);
window->window().dispatch_pointer_release_event(
drag_end, slint::PointerEventButton::Left);
}
} else if (options.page == "update-scrolled") {
window->window().dispatch_pointer_scroll_event(
slint::LogicalPosition(slint::Point<float>{320.0f, 260.0f}),
0.0f, -2000.0f);
window->window().dispatch_pointer_move_event(
slint::LogicalPosition(slint::Point<float>{2.0f, 2.0f}));
} else if (options.page == "main-recent-scrolled") {
window->window().dispatch_pointer_scroll_event(
slint::LogicalPosition(slint::Point<float>{
400.0f, window->get_minimum_window_height() - 472.0f + 360.0f}),
-400.0f, 0.0f);
window->window().dispatch_pointer_move_event(
slint::LogicalPosition(slint::Point<float>{2.0f, 2.0f}));
} else if (options.page == "main-menu" ||
options.page == "main-menu-badges") {
const auto menu_position = slint::LogicalPosition(
slint::Point<float>{window->get_minimum_window_width() - 38.0f,
window->get_minimum_window_height() - 472.0f + 20.0f});
window->window().dispatch_pointer_press_event(
menu_position, slint::PointerEventButton::Left);
window->window().dispatch_pointer_release_event(
menu_position, slint::PointerEventButton::Left);
window->window().dispatch_pointer_move_event(
slint::LogicalPosition(slint::Point<float>{2.0f, 2.0f}));
} else if (options.page == "main-copy-tooltip" ||
options.page == "main-password-tooltip") {
const bool copy = options.page == "main-copy-tooltip";
window->window().dispatch_pointer_move_event(slint::LogicalPosition(
slint::Point<float>{window->get_minimum_window_width() / 2 - (copy ? 48.0f : 42.0f),
window->get_minimum_window_height() - 472.0f +
(copy ? 150.0f : 216.0f)}));
} else if (options.page == "main-copy-released" ||
options.page == "main-copy-released-outside" ||
options.page == "main-keyboard-tab") {
const auto copy_position = slint::LogicalPosition(slint::Point<float>{
window->get_minimum_window_width() / 2 - 48.0f,
window->get_minimum_window_height() - 472.0f + 150.0f});
window->window().dispatch_pointer_move_event(copy_position);
window->window().dispatch_pointer_press_event(
copy_position, slint::PointerEventButton::Left);
window->window().dispatch_pointer_release_event(
copy_position, slint::PointerEventButton::Left);
if (options.page != "main-copy-released") {
window->window().dispatch_pointer_move_event(
slint::LogicalPosition(slint::Point<float>{2.0f, 2.0f}));
}
if (options.page == "main-keyboard-tab") {
// Traverse the remote ID input and focus the Connect button.
for (int index = 0; index < 2; ++index) {
window->window().dispatch_key_press_event("\t");
window->window().dispatch_key_release_event("\t");
}
}
}
// Let settings scroll animations settle as well as hover/focus feedback.
capture_settle_timer.start(slint::TimerMode::SingleShot,
std::chrono::milliseconds(
options.page.rfind("settings", 0) == 0 ? 350 : 150), [&] {
snapshot_written =
WriteWindowSnapshot(*capture_window, options.snapshot_path.c_str());
slint::quit_event_loop();
});
});
slint::run_event_loop();
return snapshot_written ? 0 : 7;
@@ -440,10 +669,194 @@ int RunCaptureMode(
return 0;
}
// Sample the native window while the same restoration used by the application
// runs. A component-only screenshot used to miss the conflicting restore width.
bool VerifyMainWindowGeometry() {
for (bool restore : {false, true}) {
auto component = crossdesk::ui::MainWindow::create();
auto &window = component->window();
crossdesk::gallery::WindowState placement;
const float width = component->get_minimum_window_width();
const float height = component->get_minimum_window_height();
if (width != 640.0f || height != 472.0f) return false;
if (restore) {
placement.BeginRestore();
placement.PrepareForShow(
window, false,
placement.RestoreSize(false, window.scale_factor(), height, width),
std::nullopt);
}
window.show();
bool stable = true;
int samples = 0;
slint::Timer timer;
timer.start(slint::TimerMode::Repeated, std::chrono::milliseconds(16), [&] {
const auto actual = window.size();
const auto expected = placement.RestoreSize(
false, window.scale_factor(), height, width);
if (actual.width != expected.width || actual.height != expected.height) {
std::cerr << "Startup geometry changed: restore=" << restore
<< " sample=" << samples << " actual=" << actual.width
<< 'x' << actual.height << " expected=" << expected.width
<< 'x' << expected.height << '\n';
stable = false;
}
if (placement.RestoringNormal()) {
if (!placement.NormalSizeRestored(actual, expected))
window.set_size(expected);
} else if (placement.Restoring()) {
// Match the application's fixed connection-window role. AppKit can
// report a fixed-size window as zoomed even at its normal dimensions.
placement.FinishRestore(false, actual, false, false);
}
if (++samples == 20) slint::quit_event_loop();
});
slint::run_event_loop();
timer.stop();
window.hide();
if (!stable || placement.Restoring()) return false;
}
return true;
}
// Exercise hit targets after the workbench layout change, rather than calling
// callbacks directly: overlapping surfaces must not swallow these actions.
bool VerifyWorkbenchActions() {
auto window = crossdesk::ui::MainWindow::create();
const float width = window->get_minimum_window_width();
window->window().set_size(
slint::LogicalSize(slint::Size<float>{width, window->get_minimum_window_height()}));
auto click = [&](float x, float y) {
const auto position = slint::LogicalPosition(slint::Point<float>{x, y});
window->window().dispatch_pointer_press_event(
position, slint::PointerEventButton::Left);
window->window().dispatch_pointer_release_event(
position, slint::PointerEventButton::Left);
};
int copies = 0, visibility_changes = 0, refreshes = 0, connections = 0;
bool gallery_opened = false;
window->on_copy_local_id([&] { ++copies; return true; });
window->on_toggle_password_visibility([&] { ++visibility_changes; });
window->on_refresh_password([&] { ++refreshes; });
window->on_open_gallery([&] { gallery_opened = true; });
window->set_remote_id_input("987 654 321");
window->on_connect_requested([&](slint::SharedString id) {
if (id == "987 654 321") ++connections;
});
click(width / 2 - 48, 150);
click(width / 2 - 118, 216);
click(width / 2 - 80, 216);
click(width / 2 - 42, 216);
if (copies != 1 || visibility_changes != 1 || refreshes != 1 ||
!window->get_reset_password_open()) return false;
window->set_reset_password_open(false);
window->set_password_change_pending(true);
click(width / 2 - 80, 216);
click(width / 2 - 42, 216);
if (refreshes != 1 || window->get_reset_password_open()) return false;
click(width * 0.75f - 6, 216);
// Space activates the focused primary button; pending state blocks repeats.
window->window().dispatch_key_press_event(" ");
window->window().dispatch_key_release_event(" ");
if (connections != 2) return false;
window->set_connection_pending(true);
click(width * 0.75f - 6, 216);
if (connections != 2) return false;
window->set_connection_pending(false);
click(width - 38, 20);
click(width - 78, 56);
if (!window->get_settings_open()) return false;
// Both tabs must remain at the same position, including with a save error.
click(width / 2 + 87, 89);
if (window->get_settings_tab() != 1) return false;
click(width / 2 - 87, 89);
if (window->get_settings_tab() != 0) return false;
click(width / 2 + 87, 89);
window->set_gallery_settings_error("Unable to save settings. Please try again.");
click(width / 2 - 87, 89);
if (window->get_settings_tab() != 0) return false;
window->set_gallery_settings_error("");
window->set_settings_open(false);
click(60, 20);
if (!gallery_opened) return false;
crossdesk::ui::RecentConnection device{};
device.remote_id = "987654321";
device.display_name = "Workstation";
device.online = true;
window->set_recent_connections(
std::make_shared<slint::VectorModel<crossdesk::ui::RecentConnection>>(
std::vector{device}));
bool recent_connected = false;
window->on_recent_connect([&](slint::SharedString id) {
recent_connected = id == "987654321";
});
click(100, 344);
if (!recent_connected) return false;
bool password_submitted = false, cancelled = false;
window->on_connection_submit_password([&](slint::SharedString value, bool) {
password_submitted = value == "654321";
});
window->on_connection_cancel([&] { cancelled = true; });
window->set_connection_password("654321");
window->set_connection_password_required(true);
window->set_connection_dialog_open(true);
click(width / 2 + 62, 312);
click(width / 2 - 62, 312);
if (!password_submitted || !cancelled) return false;
window->set_connection_dialog_open(false);
// Rounded frames must preserve editor focus, text bindings and Enter actions.
auto type = [&](std::string_view text) {
for (char character : text) {
const slint::SharedString key(std::string(1, character).c_str());
window->window().dispatch_key_press_event(key);
window->window().dispatch_key_release_event(key);
}
};
bool renamed = false;
window->on_recent_edit_alias([&](slint::SharedString, slint::SharedString name) {
renamed = name == "Office";
});
window->set_alias_input("");
window->set_alias_open(true);
click(width / 2, 238);
type("Office\n");
if (!renamed || window->get_alias_open()) {
std::cerr << "Alias editor did not accept the typed name; text="
<< std::string(window->get_alias_input()) << '\n';
return false;
}
bool added = false;
window->on_gallery_add_device([&](slint::SharedString id,
slint::SharedString password, slint::SharedString group, bool) {
added = id == "123456789" && password == "123456" && group == "Lab";
return added;
});
window->set_gallery_mode(true);
window->window().set_size(slint::LogicalSize(slint::Size<float>{width, 448.0f}));
window->set_gallery_adding(true);
click(width / 2, 142);
type("123456789\n123456\nLab\n");
if (!added || window->get_gallery_adding()) {
std::cerr << "Add-device editors did not retain Enter focus traversal\n";
return false;
}
return true;
}
} // namespace
int main() {
const CaptureOptions capture_options = LoadCaptureOptions();
if (!VerifyMainWindowGeometry()) {
std::cerr << "Main window dimensions must stay stable through startup\n";
return 11;
}
if (!VerifyWorkbenchActions()) {
std::cerr << "Workbench controls must retain their pointer and keyboard actions\n";
return 10;
}
auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::create();
auto server = crossdesk::ui::ServerWindow::create();