mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 17:10:20 +08:00
[ui] refine desktop layouts and unify control styling
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ""); }
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user