feat(ui): add dynamic scaling support

- Introduced `scale` property across components for responsive design.
- Adjusted dimensions and positions dynamically based on scale.
- Enhanced flexibility for varying display resolutions.
This commit is contained in:
Kieran Kihn
2025-12-18 11:42:29 +08:00
parent 6276dbdea3
commit 150e29c25c
5 changed files with 240 additions and 184 deletions

View File

@@ -1,17 +1,19 @@
import { LineEdit } from "std-widgets.slint"; import { LineEdit } from "std-widgets.slint";
export component Button inherits Rectangle { export component Button inherits Rectangle {
in property <float> scale;
in property <string> text; in property <string> text;
in property <bool> enabled: true; in property <bool> enabled: true;
callback clicked; callback clicked;
width: 100px; width: 100px * scale;
height: 36px; height: 36px * scale;
background: !root.enabled ? #555555 // 禁用状态 background: !root.enabled ? #555555 // 禁用状态
: touch.pressed ? #444444 // 按下 : touch.pressed ? #444444 // 按下
: #2C2C2C; // 默认 : #2C2C2C; // 默认
border-radius: 6px; border-radius: 6px * scale;
touch := TouchArea { touch := TouchArea {
enabled: root.enabled; enabled: root.enabled;
@@ -23,41 +25,43 @@ export component Button inherits Rectangle {
Text { Text {
text: root.text; text: root.text;
color: #FFFFFF; color: #FFFFFF;
font-size: 14px; font-size: 14px * scale;
vertical-alignment: center; vertical-alignment: center;
horizontal-alignment: center; horizontal-alignment: center;
} }
} }
export component LabeledInput inherits VerticalLayout { export component LabeledInput inherits VerticalLayout {
in property <float> scale;
in property <InputType> input-type: InputType.text; in property <InputType> input-type: InputType.text;
in property <string> label-text; in property <string> label-text;
in-out property <string> value <=> input.text; in-out property <string> value <=> input.text;
in property <string> placeholder; in property <string> placeholder;
spacing: 6px; spacing: 6px * scale;
// 标签文本 // 标签文本
Text { Text {
text: root.label-text; text: root.label-text;
font-size: 12px; font-size: 12px * scale;
font-weight: 500; font-weight: 500;
color: #555555; color: #555555;
} }
// 输入框背景和实体 // 输入框背景和实体
Rectangle { Rectangle {
height: 36px; height: 36px * scale;
background: #FFFFFF; background: #FFFFFF;
border-radius: 6px; border-radius: 6px * scale;
border-width: 1px; border-width: 1px * scale;
border-color: input.has-focus ? #888888 : #E0E0E0; // 聚焦时边框变深 border-color: input.has-focus ? #888888 : #E0E0E0; // 聚焦时边框变深
input := LineEdit { input := LineEdit {
width: 100%; width: 100%;
height: 100%; height: 100%;
font-size: 14px; font-size: 14px * scale;
placeholder-text: root.placeholder; placeholder-text: root.placeholder;
input-type: root.input-type; input-type: root.input-type;
} }

View File

@@ -4,13 +4,15 @@ import {
} from "std-widgets.slint"; } from "std-widgets.slint";
import { Button, LabeledInput } from "Components.slint"; import { Button, LabeledInput } from "Components.slint";
export component ConnectPage inherits Window { export component ConnectPage inherits Rectangle {
in property <float> scale;
in property <bool> is-connecting; in property <bool> is-connecting;
callback request-connect(string, string, string); callback request-connect(string, string, string);
width: 1920px; width: 100%;
height: 1080px; height: 100%;
// 背景渐变 (从左上角的米色到右下角的淡粉色) // 背景渐变 (从左上角的米色到右下角的淡粉色)
Rectangle { Rectangle {
@@ -19,29 +21,29 @@ export component ConnectPage inherits Window {
// 中心卡片 // 中心卡片
Rectangle { Rectangle {
width: 600px; width: 600px * scale;
height: 600px; // 根据内容高度调整 height: 600px * scale; // 根据内容高度调整
background: #FDFBF8; // 略微区别于背景的米白色 background: #FDFBF8; // 略微区别于背景的米白色
border-radius: 20px; border-radius: 20px * scale;
// 简单的阴影模拟 // 简单的阴影模拟
drop-shadow-blur: 20px; drop-shadow-blur: 20px * scale;
drop-shadow-color: #00000015; drop-shadow-color: #00000015;
drop-shadow-offset-y: 4px; drop-shadow-offset-y: 4px * scale;
VerticalLayout { VerticalLayout {
padding: 40px; padding: 40px * scale;
spacing: 20px; spacing: 20px * scale;
alignment: center; alignment: center;
// 标题区域 // 标题区域
VerticalLayout { VerticalLayout {
spacing: 8px; spacing: 8px * scale;
alignment: center; alignment: center;
Text { Text {
text: "UNO"; text: "UNO";
font-size: 96px; font-size: 96px * scale;
font-weight: 700; font-weight: 700;
color: #222222; color: #222222;
horizontal-alignment: center; horizontal-alignment: center;
@@ -49,7 +51,7 @@ export component ConnectPage inherits Window {
Text { Text {
text: "连接至服务器以开始游戏"; text: "连接至服务器以开始游戏";
font-size: 14px; font-size: 14px * scale;
color: #666666; color: #666666;
horizontal-alignment: center; horizontal-alignment: center;
} }
@@ -57,32 +59,36 @@ export component ConnectPage inherits Window {
// 占位间隔 // 占位间隔
Rectangle { Rectangle {
height: 10px; height: 10px * scale;
} }
// 表单区域 // 表单区域
address-input := LabeledInput { address-input := LabeledInput {
scale: root.scale;
label-text: "服务器地址"; label-text: "服务器地址";
value: "localhost"; value: "localhost";
} }
port-input := LabeledInput { port-input := LabeledInput {
scale: root.scale;
label-text: "服务器端口"; label-text: "服务器端口";
value: "10001"; value: "10001";
input-type: InputType.number; input-type: InputType.number;
} }
name-input := LabeledInput { name-input := LabeledInput {
scale: root.scale;
label-text: "玩家昵称"; label-text: "玩家昵称";
value: "Player"; value: "Player";
} }
// 底部按钮区域 (增加一点顶部间距) // 底部按钮区域 (增加一点顶部间距)
HorizontalLayout { HorizontalLayout {
padding-top: 10px; padding-top: 10px * scale;
alignment: center; alignment: center;
Button { Button {
scale: root.scale;
text: is-connecting ? "正在连接..." : "连接"; text: is-connecting ? "正在连接..." : "连接";
enabled: !is-connecting; enabled: !is-connecting;
clicked => { clicked => {

View File

@@ -34,28 +34,30 @@ export struct HandCard {
// 玩家头像组件 // 玩家头像组件
component PlayerAvatar inherits Rectangle { component PlayerAvatar inherits Rectangle {
in property <float> scale;
in property <string> player-name; in property <string> player-name;
in property <int> card-count; in property <int> card-count;
in property <bool> has-uno; in property <bool> has-uno;
in property <bool> is-current-turn; in property <bool> is-current-turn;
width: 100px; width: 100px * scale;
height: 130px; height: 130px * scale;
background: transparent; background: transparent;
VerticalLayout { VerticalLayout {
spacing: 4px; spacing: 4px * scale;
alignment: center; alignment: center;
// UNO 标志占位区域 (固定高度) // UNO 标志占位区域 (固定高度)
HorizontalLayout { HorizontalLayout {
alignment: center; alignment: center;
Rectangle { Rectangle {
width: 50px; width: 50px * scale;
height: 20px; height: 20px * scale;
background: has-uno ? #FF5722 : transparent; background: has-uno ? #FF5722 : transparent;
border-radius: 4px; border-radius: 4px * scale;
Text { Text {
text: "UNO!"; text: "UNO!";
font-size: 11px; font-size: 11px * scale;
font-weight: 700; font-weight: 700;
color: has-uno ? #FFFFFF : transparent; color: has-uno ? #FFFFFF : transparent;
horizontal-alignment: center; horizontal-alignment: center;
@@ -68,15 +70,15 @@ component PlayerAvatar inherits Rectangle {
HorizontalLayout { HorizontalLayout {
alignment: center; alignment: center;
Rectangle { Rectangle {
width: 60px; width: 60px * scale;
height: 60px; height: 60px * scale;
background: is-current-turn ? #FFD54F : #E0E0E0; background: is-current-turn ? #FFD54F : #E0E0E0;
border-radius: 30px; border-radius: 30px * scale;
border-width: is-current-turn ? 3px : 0px; border-width: is-current-turn ? 3px * scale : 0px;
border-color: #FF9800; border-color: #FF9800;
Text { Text {
text: "👤"; text: "👤";
font-size: 28px; font-size: 28px * scale;
horizontal-alignment: center; horizontal-alignment: center;
vertical-alignment: center; vertical-alignment: center;
} }
@@ -86,7 +88,7 @@ component PlayerAvatar inherits Rectangle {
// 玩家名称 // 玩家名称
Text { Text {
text: player-name; text: player-name;
font-size: 12px; font-size: 12px * scale;
font-weight: 500; font-weight: 500;
color: #333333; color: #333333;
horizontal-alignment: center; horizontal-alignment: center;
@@ -95,7 +97,7 @@ component PlayerAvatar inherits Rectangle {
// 剩余手牌数 // 剩余手牌数
Text { Text {
text: "剩余: " + card-count + " 张"; text: "剩余: " + card-count + " 张";
font-size: 10px; font-size: 10px * scale;
color: #666666; color: #666666;
horizontal-alignment: center; horizontal-alignment: center;
} }
@@ -104,22 +106,24 @@ component PlayerAvatar inherits Rectangle {
// 卡牌背面组件 (起牌堆) // 卡牌背面组件 (起牌堆)
component CardBack inherits Rectangle { component CardBack inherits Rectangle {
in property <float> scale;
in property <int> card-width: 100; in property <int> card-width: 100;
in property <int> card-height: 150; in property <int> card-height: 150;
width: card-width * 1px; width: card-width * 1px * scale;
height: card-height * 1px; height: card-height * 1px * scale;
background: #2C2C2C; background: #2C2C2C;
border-radius: 10px; border-radius: 10px * scale;
border-width: 2px; border-width: 2px * scale;
border-color: #444444; border-color: #444444;
Rectangle { Rectangle {
width: parent.width * 0.75; width: parent.width * 0.75;
height: parent.height * 0.83; height: parent.height * 0.83;
background: #1a1a1a; background: #1a1a1a;
border-radius: 8px; border-radius: 8px * scale;
Text { Text {
text: "UNO"; text: "UNO";
font-size: 20px; font-size: 20px * scale;
font-weight: 700; font-weight: 700;
color: #888888; color: #888888;
horizontal-alignment: center; horizontal-alignment: center;
@@ -130,16 +134,18 @@ component CardBack inherits Rectangle {
// 弃牌堆卡牌组件 // 弃牌堆卡牌组件
component DiscardCard inherits Rectangle { component DiscardCard inherits Rectangle {
in property <float> scale;
in property <image> card-image; in property <image> card-image;
in property <int> card-width: 100; in property <int> card-width: 100;
in property <int> card-height: 150; in property <int> card-height: 150;
width: card-width * 1px; width: card-width * 1px * scale;
height: card-height * 1px; height: card-height * 1px * scale;
background: transparent; background: transparent;
border-radius: 10px; border-radius: 10px * scale;
drop-shadow-blur: 12px; drop-shadow-blur: 12px * scale;
drop-shadow-color: #00000040; drop-shadow-color: #00000040;
drop-shadow-offset-y: 4px; drop-shadow-offset-y: 4px * scale;
Image { Image {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -150,6 +156,8 @@ component DiscardCard inherits Rectangle {
// 方向环背景组件 - 只绘制环线 // 方向环背景组件 - 只绘制环线
component DirectionRingBackground inherits Rectangle { component DirectionRingBackground inherits Rectangle {
in property <float> scale;
in property <CardColor> current-color: CardColor.Red; in property <CardColor> current-color: CardColor.Red;
in property <int> ring-width: 500; in property <int> ring-width: 500;
in property <int> ring-height: 280; in property <int> ring-height: 280;
@@ -157,8 +165,8 @@ component DirectionRingBackground inherits Rectangle {
property <color> ring-color-dim: current-color == CardColor.Red ? #E57373 : current-color == CardColor.Blue ? #64B5F6 : current-color == CardColor.Green ? #81C784 : current-color == CardColor.Yellow ? #FFF176 : #BDBDBD; property <color> ring-color-dim: current-color == CardColor.Red ? #E57373 : current-color == CardColor.Blue ? #64B5F6 : current-color == CardColor.Green ? #81C784 : current-color == CardColor.Yellow ? #FFF176 : #BDBDBD;
property <float> a: ring-width / 2; property <float> a: ring-width / 2;
property <float> b: ring-height / 2; property <float> b: ring-height / 2;
width: ring-width * 1px; width: ring-width * 1px * scale;
height: ring-height * 1px; height: ring-height * 1px * scale;
background: transparent; background: transparent;
// 底层光晕环 // 底层光晕环
@@ -166,28 +174,28 @@ component DirectionRingBackground inherits Rectangle {
width: 100%; width: 100%;
height: 100%; height: 100%;
stroke: ring-color-dim; stroke: ring-color-dim;
stroke-width: 8px; stroke-width: 8px * scale;
fill: transparent; fill: transparent;
opacity: 0.3; opacity: 0.3;
MoveTo { MoveTo {
x: ring-width / 2 + a; x: (ring-width / 2 + a) * scale;
y: ring-height / 2; y: (ring-height / 2) * scale;
} }
ArcTo { ArcTo {
x: ring-width / 2 - a; x: (ring-width / 2 - a) * scale;
y: ring-height / 2; y: (ring-height / 2) * scale;
radius-x: a; radius-x: a * scale;
radius-y: b; radius-y: b * scale;
sweep: true; sweep: true;
large-arc: true; large-arc: true;
} }
ArcTo { ArcTo {
x: ring-width / 2 + a; x: (ring-width / 2 + a) * scale;
y: ring-height / 2; y: (ring-height / 2) * scale;
radius-x: a; radius-x: a * scale;
radius-y: b; radius-y: b * scale;
sweep: true; sweep: true;
large-arc: true; large-arc: true;
} }
@@ -198,28 +206,28 @@ component DirectionRingBackground inherits Rectangle {
width: 100%; width: 100%;
height: 100%; height: 100%;
stroke: ring-color; stroke: ring-color;
stroke-width: 3px; stroke-width: 3px * scale;
fill: transparent; fill: transparent;
opacity: 0.9; opacity: 0.9;
MoveTo { MoveTo {
x: ring-width / 2 + a; x: (ring-width / 2 + a) * scale;
y: ring-height / 2; y: (ring-height / 2) * scale;
} }
ArcTo { ArcTo {
x: ring-width / 2 - a; x: (ring-width / 2 - a) * scale;
y: ring-height / 2; y: (ring-height / 2) * scale;
radius-x: a; radius-x: a * scale;
radius-y: b; radius-y: b * scale;
sweep: true; sweep: true;
large-arc: true; large-arc: true;
} }
ArcTo { ArcTo {
x: ring-width / 2 + a; x: (ring-width / 2 + a) * scale;
y: ring-height / 2; y: (ring-height / 2) * scale;
radius-x: a; radius-x: a * scale;
radius-y: b; radius-y: b * scale;
sweep: true; sweep: true;
large-arc: true; large-arc: true;
} }
@@ -228,6 +236,8 @@ component DirectionRingBackground inherits Rectangle {
// 方向环光点组件 - 使用内置动画实现平滑移动 // 方向环光点组件 - 使用内置动画实现平滑移动
component DirectionRingOrbs inherits Rectangle { component DirectionRingOrbs inherits Rectangle {
in property <float> scale;
in property <GameDirection> direction: GameDirection.Clockwise; in property <GameDirection> direction: GameDirection.Clockwise;
in property <CardColor> current-color: CardColor.Red; in property <CardColor> current-color: CardColor.Red;
in property <int> ring-width: 500; in property <int> ring-width: 500;
@@ -248,55 +258,55 @@ component DirectionRingOrbs inherits Rectangle {
// 实际角度 = 基础角度 * 方向系数 // 实际角度 = 基础角度 * 方向系数
property <float> actual-angle: base-angle * dir-sign; property <float> actual-angle: base-angle * dir-sign;
width: ring-width * 1px; width: ring-width * 1px * scale;
height: ring-height * 1px; height: ring-height * 1px * scale;
background: transparent; background: transparent;
// 光点1 // 光点1
Rectangle { Rectangle {
x: parent.width / 2 - 9px + cos(actual-angle * 1deg) * a * 1px; x: (parent.width / 2 - 9px * scale) + cos(actual-angle * 1deg) * a * 1px * scale;
y: parent.height / 2 - 9px + sin(actual-angle * 1deg) * b * 1px; y: (parent.height / 2 - 9px * scale) + sin(actual-angle * 1deg) * b * 1px * scale;
width: 18px; width: 18px * scale;
height: 18px; height: 18px * scale;
background: ring-color; background: ring-color;
border-radius: 9px; border-radius: 9px * scale;
drop-shadow-blur: 8px; drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color; drop-shadow-color: ring-color;
} }
// 光点2 // 光点2
Rectangle { Rectangle {
x: parent.width / 2 - 9px + cos((actual-angle + 90) * 1deg) * a * 1px; x: (parent.width / 2 - 9px * scale) + cos((actual-angle + 90) * 1deg) * a * 1px * scale;
y: parent.height / 2 - 9px + sin((actual-angle + 90) * 1deg) * b * 1px; y: (parent.height / 2 - 9px * scale) + sin((actual-angle + 90) * 1deg) * b * 1px * scale;
width: 18px; width: 18px * scale;
height: 18px; height: 18px * scale;
background: ring-color; background: ring-color;
border-radius: 9px; border-radius: 9px * scale;
drop-shadow-blur: 8px; drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color; drop-shadow-color: ring-color;
} }
// 光点3 // 光点3
Rectangle { Rectangle {
x: parent.width / 2 - 9px + cos((actual-angle + 180) * 1deg) * a * 1px; x: (parent.width / 2 - 9px * scale) + cos((actual-angle + 180) * 1deg) * a * 1px * scale;
y: parent.height / 2 - 9px + sin((actual-angle + 180) * 1deg) * b * 1px; y: (parent.height / 2 - 9px * scale) + sin((actual-angle + 180) * 1deg) * b * 1px * scale;
width: 18px; width: 18px * scale;
height: 18px; height: 18px * scale;
background: ring-color; background: ring-color;
border-radius: 9px; border-radius: 9px * scale;
drop-shadow-blur: 8px; drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color; drop-shadow-color: ring-color;
} }
// 光点4 // 光点4
Rectangle { Rectangle {
x: parent.width / 2 - 9px + cos((actual-angle + 270) * 1deg) * a * 1px; x: (parent.width / 2 - 9px * scale) + cos((actual-angle + 270) * 1deg) * a * 1px * scale;
y: parent.height / 2 - 9px + sin((actual-angle + 270) * 1deg) * b * 1px; y: (parent.height / 2 - 9px * scale) + sin((actual-angle + 270) * 1deg) * b * 1px * scale;
width: 18px; width: 18px * scale;
height: 18px; height: 18px * scale;
background: ring-color; background: ring-color;
border-radius: 9px; border-radius: 9px * scale;
drop-shadow-blur: 8px; drop-shadow-blur: 8px * scale;
drop-shadow-color: ring-color; drop-shadow-color: ring-color;
} }
@@ -308,17 +318,19 @@ component DirectionRingOrbs inherits Rectangle {
// 手牌卡牌组件 // 手牌卡牌组件
component HandCardItem inherits Rectangle { component HandCardItem inherits Rectangle {
in property <float> scale;
in property <image> card-image; in property <image> card-image;
in property <bool> is-selected: false; in property <bool> is-selected: false;
callback clicked; callback clicked;
width: 120px; width: 120px * scale;
height: 180px; height: 180px * scale;
y: is-selected ? -25px : 0px; y: is-selected ? -25px * scale : 0px;
background: transparent; background: transparent;
border-radius: 10px; border-radius: 10px * scale;
drop-shadow-blur: is-selected ? 16px : 6px; drop-shadow-blur: is-selected ? 16px * scale : 6px * scale;
drop-shadow-color: is-selected ? #00000040 : #00000025; drop-shadow-color: is-selected ? #00000040 : #00000025;
drop-shadow-offset-y: is-selected ? 6px : 3px; drop-shadow-offset-y: is-selected ? 6px * scale : 3px * scale;
animate y { animate y {
duration: 150ms; duration: 150ms;
easing: ease-out; easing: ease-out;
@@ -340,14 +352,15 @@ component HandCardItem inherits Rectangle {
// UNO 圆形按钮组件 // UNO 圆形按钮组件
component UnoButton inherits Rectangle { component UnoButton inherits Rectangle {
in property <float> scale;
callback clicked; callback clicked;
width: 90px; width: 90px * scale;
height: 90px; height: 90px * scale;
background: touch.pressed ? #D32F2F : #F44336; background: touch.pressed ? #D32F2F : #F44336;
border-radius: 45px; border-radius: 45px * scale;
drop-shadow-blur: 12px; drop-shadow-blur: 12px * scale;
drop-shadow-color: #F4433660; drop-shadow-color: #F4433660;
drop-shadow-offset-y: 4px; drop-shadow-offset-y: 4px * scale;
touch := TouchArea { touch := TouchArea {
clicked => { clicked => {
root.clicked(); root.clicked();
@@ -356,7 +369,7 @@ component UnoButton inherits Rectangle {
Text { Text {
text: "UNO!"; text: "UNO!";
font-size: 20px; font-size: 20px * scale;
font-weight: 700; font-weight: 700;
color: #FFFFFF; color: #FFFFFF;
horizontal-alignment: center; horizontal-alignment: center;
@@ -366,18 +379,19 @@ component UnoButton inherits Rectangle {
// 颜色选择按钮组件 // 颜色选择按钮组件
component ColorButton inherits Rectangle { component ColorButton inherits Rectangle {
in property <float> scale;
in property <color> btn-color; in property <color> btn-color;
in property <string> color-name; in property <string> color-name;
callback clicked; callback clicked;
width: 80px; width: 80px * scale;
height: 80px; height: 80px * scale;
background: touch.pressed ? btn-color.darker(20%) : btn-color; background: touch.pressed ? btn-color.darker(20%) : btn-color;
border-radius: 12px; border-radius: 12px * scale;
border-width: 3px; border-width: 3px * scale;
border-color: btn-color.darker(30%); border-color: btn-color.darker(30%);
drop-shadow-blur: 8px; drop-shadow-blur: 8px * scale;
drop-shadow-color: btn-color.with-alpha(0.4); drop-shadow-color: btn-color.with-alpha(0.4);
drop-shadow-offset-y: 3px; drop-shadow-offset-y: 3px * scale;
touch := TouchArea { touch := TouchArea {
clicked => { clicked => {
root.clicked(); root.clicked();
@@ -386,7 +400,7 @@ component ColorButton inherits Rectangle {
Text { Text {
text: color-name; text: color-name;
font-size: 14px; font-size: 14px * scale;
font-weight: 600; font-weight: 600;
color: #FFFFFF; color: #FFFFFF;
horizontal-alignment: center; horizontal-alignment: center;
@@ -396,6 +410,7 @@ component ColorButton inherits Rectangle {
// 颜色选择弹窗组件 // 颜色选择弹窗组件
component ColorPickerDialog inherits Rectangle { component ColorPickerDialog inherits Rectangle {
in property <float> scale;
in property <bool> show: false; in property <bool> show: false;
callback color-selected(CardColor); callback color-selected(CardColor);
callback cancel; callback cancel;
@@ -421,13 +436,13 @@ component ColorPickerDialog inherits Rectangle {
Rectangle { Rectangle {
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
width: 600px; width: 600px * scale;
height: 280px; height: 280px * scale;
background: #FFFFFF; background: #FFFFFF;
border-radius: 20px; border-radius: 20px * scale;
drop-shadow-blur: 30px; drop-shadow-blur: 30px * scale;
drop-shadow-color: #00000040; drop-shadow-color: #00000040;
drop-shadow-offset-y: 10px; drop-shadow-offset-y: 10px * scale;
// 阻止点击弹窗内容时关闭 // 阻止点击弹窗内容时关闭
TouchArea { TouchArea {
@@ -435,14 +450,14 @@ component ColorPickerDialog inherits Rectangle {
} }
VerticalLayout { VerticalLayout {
padding: 30px; padding: 30px * scale;
spacing: 25px; spacing: 25px * scale;
alignment: center; alignment: center;
// 标题 // 标题
Text { Text {
text: "选择颜色"; text: "选择颜色";
font-size: 24px; font-size: 24px * scale;
font-weight: 700; font-weight: 700;
color: #333333; color: #333333;
horizontal-alignment: center; horizontal-alignment: center;
@@ -450,9 +465,10 @@ component ColorPickerDialog inherits Rectangle {
// 颜色按钮网格 // 颜色按钮网格
HorizontalLayout { HorizontalLayout {
spacing: 20px; spacing: 20px * scale;
alignment: center; alignment: center;
ColorButton { ColorButton {
scale: root.scale;
btn-color: #F44336; btn-color: #F44336;
color-name: "红色"; color-name: "红色";
clicked => { clicked => {
@@ -461,6 +477,7 @@ component ColorPickerDialog inherits Rectangle {
} }
ColorButton { ColorButton {
scale: root.scale;
btn-color: #2196F3; btn-color: #2196F3;
color-name: "蓝色"; color-name: "蓝色";
clicked => { clicked => {
@@ -469,6 +486,7 @@ component ColorPickerDialog inherits Rectangle {
} }
ColorButton { ColorButton {
scale: root.scale;
btn-color: #4CAF50; btn-color: #4CAF50;
color-name: "绿色"; color-name: "绿色";
clicked => { clicked => {
@@ -477,6 +495,7 @@ component ColorPickerDialog inherits Rectangle {
} }
ColorButton { ColorButton {
scale: root.scale;
btn-color: #FFEB3B; btn-color: #FFEB3B;
color-name: "黄色"; color-name: "黄色";
clicked => { clicked => {
@@ -489,6 +508,7 @@ component ColorPickerDialog inherits Rectangle {
HorizontalLayout { HorizontalLayout {
alignment: center; alignment: center;
Button { Button {
scale: root.scale;
text: "取消"; text: "取消";
clicked => { clicked => {
root.cancel(); root.cancel();
@@ -500,7 +520,8 @@ component ColorPickerDialog inherits Rectangle {
} }
// 主游戏页面 // 主游戏页面
export component GamePage inherits Window { export component GamePage inherits Rectangle {
in property <float> scale;
// 其他玩家列表 // 其他玩家列表
in property <[OtherPlayer]> other-players; in property <[OtherPlayer]> other-players;
@@ -530,8 +551,8 @@ export component GamePage inherits Window {
callback request-play-card(int, CardColor); callback request-play-card(int, CardColor);
callback request-draw-card; callback request-draw-card;
callback request-uno; callback request-uno;
width: 1920px; width: 100%;
height: 1080px; height: 100%;
// 背景渐变 // 背景渐变
Rectangle { Rectangle {
@@ -540,25 +561,26 @@ export component GamePage inherits Window {
// 主布局 // 主布局
VerticalLayout { VerticalLayout {
padding: 30px; padding: 30px * scale;
spacing: 20px; spacing: 20px * scale;
// ===== 顶部:其他玩家区域 ===== // ===== 顶部:其他玩家区域 =====
Rectangle { Rectangle {
height: 150px; height: 150px * scale;
background: #FDFBF8; background: #FDFBF8;
border-radius: 16px; border-radius: 16px * scale;
drop-shadow-blur: 10px; drop-shadow-blur: 10px * scale;
drop-shadow-color: #00000010; drop-shadow-color: #00000010;
drop-shadow-offset-y: 2px; drop-shadow-offset-y: 2px * scale;
HorizontalLayout { HorizontalLayout {
padding-top: 10px; padding-top: 10px * scale;
padding-bottom: 15px; padding-bottom: 15px * scale;
padding-left: 15px; padding-left: 15px * scale;
padding-right: 15px; padding-right: 15px * scale;
spacing: 40px; spacing: 40px * scale;
alignment: center; alignment: center;
for player[index] in other-players: PlayerAvatar { for player[index] in other-players: PlayerAvatar {
scale: root.scale;
player-name: player.name; player-name: player.name;
card-count: player.card-count; card-count: player.card-count;
has-uno: player.has-uno; has-uno: player.has-uno;
@@ -574,14 +596,15 @@ export component GamePage inherits Window {
// 中心容器:包含环和两个牌堆 // 中心容器:包含环和两个牌堆
Rectangle { Rectangle {
width: 560px; width: 560px * scale;
height: 320px; height: 320px * scale;
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
background: transparent; background: transparent;
// 方向环背景 - 在牌堆下面 // 方向环背景 - 在牌堆下面
DirectionRingBackground { DirectionRingBackground {
scale: root.scale;
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
ring-width: 560; ring-width: 560;
@@ -591,11 +614,12 @@ export component GamePage inherits Window {
// 起牌堆 - 左侧 // 起牌堆 - 左侧
Rectangle { Rectangle {
x: 80px; x: 80px * scale;
y: (parent.height - 200px) / 2; y: (parent.height - 200px * scale) / 2;
width: 130px; width: 130px * scale;
height: 200px; height: 200px * scale;
CardBack { CardBack {
scale: root.scale;
card-width: 130; card-width: 130;
card-height: 200; card-height: 200;
} }
@@ -613,8 +637,9 @@ export component GamePage inherits Window {
// 弃牌堆 - 右侧 // 弃牌堆 - 右侧
DiscardCard { DiscardCard {
x: parent.width - 130px - 80px; scale: root.scale;
y: (parent.height - 200px) / 2; x: parent.width - 130px * scale - 80px * scale;
y: (parent.height - 200px * scale) / 2;
card-width: 130; card-width: 130;
card-height: 200; card-height: 200;
card-image: root.discard-top-card; card-image: root.discard-top-card;
@@ -622,6 +647,7 @@ export component GamePage inherits Window {
// 方向环光点 - 在牌堆上面 // 方向环光点 - 在牌堆上面
DirectionRingOrbs { DirectionRingOrbs {
scale: root.scale;
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2; y: (parent.height - self.height) / 2;
ring-width: 560; ring-width: 560;
@@ -634,23 +660,24 @@ export component GamePage inherits Window {
// ===== 底部:当前玩家区域 ===== // ===== 底部:当前玩家区域 =====
Rectangle { Rectangle {
height: 320px; height: 320px * scale;
background: #FDFBF8; background: #FDFBF8;
border-radius: 16px; border-radius: 16px * scale;
drop-shadow-blur: 10px; drop-shadow-blur: 10px * scale;
drop-shadow-color: #00000010; drop-shadow-color: #00000010;
drop-shadow-offset-y: -2px; drop-shadow-offset-y: -2px * scale;
HorizontalLayout { HorizontalLayout {
padding: 20px; padding: 20px * scale;
padding-left: 40px; padding-left: 40px * scale;
padding-right: 40px; padding-right: 40px * scale;
spacing: 30px; spacing: 30px * scale;
alignment: space-between; alignment: space-between;
// 当前玩家头像 - 靠左垂直居中与UNO按钮对齐 // 当前玩家头像 - 靠左垂直居中与UNO按钮对齐
VerticalLayout { VerticalLayout {
alignment: center; alignment: center;
PlayerAvatar { PlayerAvatar {
scale: root.scale;
player-name: current-player-name; player-name: current-player-name;
card-count: current-player-card-count; card-count: current-player-card-count;
has-uno: current-player-has-uno; has-uno: current-player-has-uno;
@@ -661,17 +688,18 @@ export component GamePage inherits Window {
// 手牌区域 // 手牌区域
VerticalLayout { VerticalLayout {
horizontal-stretch: 1; horizontal-stretch: 1;
spacing: 10px; spacing: 10px * scale;
alignment: center; alignment: center;
// 出牌按钮 (放在手牌上方) // 出牌按钮 (放在手牌上方)
Rectangle { Rectangle {
height: 50px; height: 50px * scale;
background: transparent; background: transparent;
HorizontalLayout { HorizontalLayout {
alignment: center; alignment: center;
padding-bottom: 10px; padding-bottom: 10px * scale;
Button { Button {
scale: root.scale;
text: "出牌"; text: "出牌";
enabled: is-current-player-turn && selected-card-index >= 0 && hand-cards[selected-card-index].can-be-played; enabled: is-current-player-turn && selected-card-index >= 0 && hand-cards[selected-card-index].can-be-played;
clicked => { clicked => {
@@ -693,20 +721,21 @@ export component GamePage inherits Window {
// 手牌列表 - 上方留出空间给选中效果 // 手牌列表 - 上方留出空间给选中效果
Rectangle { Rectangle {
height: 210px; height: 210px * scale;
background: transparent; background: transparent;
// 手牌容器,向下偏移以给选中卡牌留空间,居中显示 // 手牌容器,向下偏移以给选中卡牌留空间,居中显示
Rectangle { Rectangle {
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: 25px; y: 25px * scale;
width: hand-cards-layout.preferred-width; width: hand-cards-layout.preferred-width;
height: 180px; height: 180px * scale;
hand-cards-layout := HorizontalLayout { hand-cards-layout := HorizontalLayout {
alignment: center; alignment: center;
spacing: -35px; // 卡牌重叠效果 spacing: -35px * scale; // 卡牌重叠效果
for card[index] in hand-cards: HandCardItem { for card[index] in hand-cards: HandCardItem {
scale: root.scale;
card-image: card.image-path; card-image: card.image-path;
is-selected: index == selected-card-index; is-selected: index == selected-card-index;
clicked => { clicked => {
@@ -729,6 +758,7 @@ export component GamePage inherits Window {
VerticalLayout { VerticalLayout {
alignment: center; alignment: center;
UnoButton { UnoButton {
scale: root.scale;
clicked => { clicked => {
root.request-uno(); root.request-uno();
} }
@@ -740,6 +770,7 @@ export component GamePage inherits Window {
// 颜色选择弹窗 (覆盖在最上层) // 颜色选择弹窗 (覆盖在最上层)
ColorPickerDialog { ColorPickerDialog {
scale: root.scale;
show: root.show-color-picker; show: root.show-color-picker;
color-selected(color) => { color-selected(color) => {
root.show-color-picker = false; root.show-color-picker = false;

View File

@@ -1,6 +1,12 @@
import { ConnectPage } from "ConnectPage.slint"; import { ConnectPage } from "ConnectPage.slint";
import { StartPage } from "StartPage.slint"; import { StartPage } from "StartPage.slint";
import { GamePage, OtherPlayer, HandCard, CardColor, GameDirection } from "GamePage.slint"; import {
GamePage,
OtherPlayer,
HandCard,
CardColor,
GameDirection,
} from "GamePage.slint";
enum PageType { enum PageType {
ConnectPage, ConnectPage,
@@ -35,17 +41,23 @@ export component MainWindow inherits Window {
callback request-draw-card; callback request-draw-card;
callback request-uno; callback request-uno;
width: 1920px; preferred-width: 1920px;
height: 1080px; preferred-height: 1080px;
min-width: 960px;
min-height: 600px;
title: "UNO!"; title: "UNO!";
property <float> scale: min(self.width / 1920px, self.height / 1080px);
if root.active-page == PageType.ConnectPage: connect-page := ConnectPage { if root.active-page == PageType.ConnectPage: connect-page := ConnectPage {
scale: root.scale;
is-connecting: root.is-connecting; is-connecting: root.is-connecting;
request-connect(server-address, server-port, player-name) => { request-connect(server-address, server-port, player-name) => {
root.request-connect(server-address, server-port, player-name); root.request-connect(server-address, server-port, player-name);
} }
} }
if root.active-page == PageType.StartPage: start-page := StartPage { if root.active-page == PageType.StartPage: start-page := StartPage {
scale: root.scale;
is-ready: root.is-ready; is-ready: root.is-ready;
is-restart: root.is-restart; is-restart: root.is-restart;
request-start => { request-start => {
@@ -53,6 +65,7 @@ export component MainWindow inherits Window {
} }
} }
if root.active-page == PageType.GamePage: game-page := GamePage { if root.active-page == PageType.GamePage: game-page := GamePage {
scale: root.scale;
other-players: root.other-players; other-players: root.other-players;
current-player-name: root.current-player-name; current-player-name: root.current-player-name;
current-player-card-count: root.current-player-card-count; current-player-card-count: root.current-player-card-count;

View File

@@ -1,13 +1,14 @@
import {Button} from "Components.slint"; import {Button} from "Components.slint";
export component StartPage inherits Window { export component StartPage inherits Rectangle {
in property <float> scale;
in property <bool> is-ready; in property <bool> is-ready;
in property <bool> is-restart; in property <bool> is-restart;
callback request-start; callback request-start;
width: 1920px; width: 100%;
height: 1080px; height: 100%;
// 背景渐变 (从左上角的米色到右下角的淡粉色) // 背景渐变 (从左上角的米色到右下角的淡粉色)
Rectangle { Rectangle {
@@ -16,29 +17,29 @@ export component StartPage inherits Window {
// 中心卡片 // 中心卡片
Rectangle { Rectangle {
width: 600px; width: 600px * scale;
height: 600px; // 根据内容高度调整 height: 600px * scale; // 根据内容高度调整
background: #FDFBF8; // 略微区别于背景的米白色 background: #FDFBF8; // 略微区别于背景的米白色
border-radius: 20px; border-radius: 20px * scale;
// 简单的阴影模拟 // 简单的阴影模拟
drop-shadow-blur: 20px; drop-shadow-blur: 20px * scale;
drop-shadow-color: #00000015; drop-shadow-color: #00000015;
drop-shadow-offset-y: 4px; drop-shadow-offset-y: 4px * scale;
VerticalLayout { VerticalLayout {
padding: 40px; padding: 40px * scale;
spacing: 20px; spacing: 20px * scale;
alignment: center; alignment: center;
// 标题区域 // 标题区域
VerticalLayout { VerticalLayout {
spacing: 8px; spacing: 8px * scale;
alignment: center; alignment: center;
Text { Text {
text: "UNO"; text: "UNO";
font-size: 96px; font-size: 96px * scale;
font-weight: 700; font-weight: 700;
color: #222222; color: #222222;
horizontal-alignment: center; horizontal-alignment: center;
@@ -46,7 +47,7 @@ export component StartPage inherits Window {
Text { Text {
text: is-restart ? "游戏结束,重新准备以重新开始游戏" : "连接成功,所有玩家准备后开始游戏"; text: is-restart ? "游戏结束,重新准备以重新开始游戏" : "连接成功,所有玩家准备后开始游戏";
font-size: 14px; font-size: 14px * scale;
color: #666666; color: #666666;
horizontal-alignment: center; horizontal-alignment: center;
} }
@@ -54,15 +55,16 @@ export component StartPage inherits Window {
// 占位间隔 // 占位间隔
Rectangle { Rectangle {
height: 10px; height: 10px * scale;
} }
// 底部按钮区域 (增加一点顶部间距) // 底部按钮区域 (增加一点顶部间距)
HorizontalLayout { HorizontalLayout {
padding-top: 10px; padding-top: 10px * scale;
alignment: center; alignment: center;
Button { Button {
scale: root.scale;
enabled: !is-ready; enabled: !is-ready;
text: is-ready ? "已准备" : "准备"; text: is-ready ? "已准备" : "准备";
clicked => { clicked => {