Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions components/button.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct ButtonStyle {
icon = text;
border = theme::buttonBorder(tokens, primary);
shadow = theme::buttonShadow(tokens);
focusRing = theme::focusRing(tokens);
disabled = tokens.interaction.disabledFill;
radius = tokens.metrics.radius.overlay;
}

Expand All @@ -32,6 +34,8 @@ struct ButtonStyle {
core::Color icon;
core::Border border;
core::Shadow shadow;
core::Border focusRing;
core::Color disabled;
float radius = 16.0f;
float opacity = 1.0f;
float pressScale = 0.965f;
Expand Down Expand Up @@ -105,7 +109,13 @@ class ButtonBuilder {
const float labelWidth = hasIcon && hasText
? std::max(0.0f, w - iconWidth - gap - metrics_.spacing.section * 2.0f * scale_)
: w;
const bool focused = ui_.isFocused(id_ + ".bg");
core::Border border = style_.border;
if (disabled_) {
border = {0.0f, style_.disabled};
} else if (focused) {
border = style_.focusRing;
}
border.width *= scale_;

core::Shadow shadow = style_.shadow;
Expand All @@ -115,6 +125,10 @@ class ButtonBuilder {
shadow.spread *= scale_;
core::Color textColor = style_.text;
core::Color iconColor = style_.icon;
if (disabled_) {
textColor = style_.disabled;
iconColor = style_.disabled;
}
textColor.a *= style_.opacity;
iconColor.a *= style_.opacity;
const std::function<void()> onPress = onPress_;
Expand All @@ -140,6 +154,7 @@ class ButtonBuilder {
.translate(translateX_, translateY_)
.transition(transition_)
.disabled(disabled_)
.focusable(!disabled_)
.preserveFocusOnPress(preserveFocusOnPress_)
.onClick(onClick_)
.onContextMenu(onContextMenu_);
Expand Down
22 changes: 20 additions & 2 deletions components/slider.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ struct SliderStyle {
track = core::mixColor(tokens.surfaceHover, tokens.surfaceActive, tokens.dark ? 0.24f : 0.18f);
fill = tokens.primary;
knob = tokens.text;
hoverKnob = theme::buttonHover(tokens, knob);
focusRing = theme::focusRing(tokens);
disabledKnob = tokens.interaction.disabledContent;
}

core::Color track;
core::Color fill;
core::Color knob;
core::Color hoverKnob;
core::Border focusRing;
core::Color disabledKnob;
};

class SliderBuilder {
Expand All @@ -49,13 +55,21 @@ class SliderBuilder {
return *this;
}
SliderBuilder& onChange(std::function<void(float)> callback) { onChange_ = std::move(callback); return *this; }
SliderBuilder& disabled(bool value = true) { disabled_ = value; return *this; }

void build() {
const float trackHeight = std::max(metrics_.spacing.tiny - metrics_.spacing.hairline,
height_ * 0.18f);
const float trackY = (height_ - trackHeight) * 0.5f;
const float knobSize = std::max(metrics_.typography.label, height_ * 0.72f);
const std::function<void(float)> onChange = onChange_;
const bool focused = ui_.isFocused(id_ + ".hit");
const auto state = theme::controlState(disabled_, false, false, focused);
const core::Color knobColor = state == theme::ControlState::Disabled
? style_.disabledKnob
: (state == theme::ControlState::Focused ? style_.hoverKnob : style_.knob);
const core::Border knobBorder = state == theme::ControlState::Focused
? style_.focusRing : core::Border{};

ui_.stack(id_)
.size(width_, height_)
Expand All @@ -81,8 +95,9 @@ class SliderBuilder {
ui_.rect(id_ + ".knob")
.y((height_ - knobSize) * 0.5f)
.size(knobSize, knobSize)
.color(style_.knob)
.color(knobColor)
.radius(knobSize * 0.5f)
.border(knobBorder)
.shadow(12.0f, 0.0f, 4.0f, theme::withAlpha(style_.fill, 0.20f))
.transition(transition_)
.animate(core::AnimProperty::Color | core::AnimProperty::Shadow)
Expand All @@ -95,8 +110,10 @@ class SliderBuilder {
theme::color(0.0f, 0.0f, 0.0f, 0.0f),
theme::color(0.0f, 0.0f, 0.0f, 0.0f))
.zIndex(10)
.interactive()
.sliderInputFrom(id_)
.interactive(!disabled_)
.disabled(disabled_)
.focusable(!disabled_)
.build();
})
.build();
Expand All @@ -112,6 +129,7 @@ class SliderBuilder {
float width_ = 300.0f;
float height_ = 32.0f;
float value_ = 0.0f;
bool disabled_ = false;
};

inline SliderBuilder slider(core::dsl::Ui& ui, const std::string& id) {
Expand Down
88 changes: 86 additions & 2 deletions components/theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ struct ThemeMetricTokens {
ControlSizeTokens control;
};

enum class ControlState {
Normal,
Hovered,
Pressed,
Focused,
Disabled
};

// Visual tokens shared by interactive controls. Checked, selected, expanded,
// loading, and error remain orthogonal component states and are not folded into
// this mutually exclusive base state.
struct InteractionTokens {
core::Color hoverBorder;
core::Color pressedOverlay;
core::Color focusRing;
core::Color disabledFill;
core::Color disabledContent;
float focusRingWidth = 2.0f;
float focusRingOffset = 2.0f;
float disabledOpacity = 0.50f;
};

struct ThemeColorTokens {
core::Color background;
core::Color primary;
Expand All @@ -95,6 +117,7 @@ struct ThemeColorTokens {
core::Color border;
bool dark = false;
ThemeMetricTokens metrics;
InteractionTokens interaction;
};

struct PageVisualTokens {
Expand Down Expand Up @@ -153,7 +176,7 @@ inline core::Color withOpacity(core::Color value, float opacity) {
}

inline ThemeColorTokens light() {
return {
ThemeColorTokens result{
color(0.95f, 0.95f, 0.97f),
defaultPrimary(),
color(1.00f, 1.00f, 1.00f),
Expand All @@ -163,10 +186,21 @@ inline ThemeColorTokens light() {
color(0.80f, 0.80f, 0.80f),
false
};
result.interaction = {
withAlpha(result.primary, 0.85f),
withAlpha(result.surfaceActive, 0.35f),
withAlpha(result.primary, 1.0f),
withAlpha(result.surfaceActive, 0.55f),
withAlpha(result.text, 0.45f),
2.0f,
2.0f,
0.50f
};
return result;
}

inline ThemeColorTokens dark() {
return {
ThemeColorTokens result{
color(0.10f, 0.10f, 0.12f),
defaultPrimary(),
color(0.15f, 0.15f, 0.18f),
Expand All @@ -176,6 +210,56 @@ inline ThemeColorTokens dark() {
color(0.30f, 0.30f, 0.30f),
true
};
result.interaction = {
withAlpha(result.primary, 0.95f),
color(0.0f, 0.0f, 0.0f, 0.34f),
color(0.40f, 0.70f, 1.0f, 1.0f),
color(0.0f, 0.0f, 0.0f, 0.30f),
withAlpha(result.text, 0.45f),
2.0f,
2.0f,
0.50f
};
return result;
}

inline ControlState controlState(bool disabled, bool pressed, bool hovered, bool focused) {
if (disabled) {
return ControlState::Disabled;
}
if (pressed) {
return ControlState::Pressed;
}
if (focused) {
return ControlState::Focused;
}
if (hovered) {
return ControlState::Hovered;
}
return ControlState::Normal;
}

inline core::Color controlFill(const ThemeColorTokens& tokens,
core::Color normal,
core::Color hover,
core::Color pressed,
ControlState state) {
switch (state) {
case ControlState::Disabled:
return core::mixColor(normal, tokens.interaction.disabledFill, 0.65f);
case ControlState::Pressed:
return pressed;
case ControlState::Focused:
case ControlState::Hovered:
return hover;
case ControlState::Normal:
default:
return normal;
}
}

inline core::Border focusRing(const ThemeColorTokens& tokens) {
return {tokens.interaction.focusRingWidth, tokens.interaction.focusRing};
}

inline PageVisualTokens pageVisuals(const ThemeColorTokens& tokens) {
Expand Down
2 changes: 1 addition & 1 deletion docs/组件.md
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ components::pieChart(ui, "usage.pie")
- `dialog`、`toast`、`contextMenu`、`dropdown`、`datePicker`、`timePicker`、`colorPicker` 需要页面传入 open/visible 状态,组件不会自己持久化开关状态。
- 容器 transform 会继承到组件内部子树渲染,包含 2D transform 和 2.5D 透视投影;默认 hit-test 仍按布局矩形计算,组件确实需要跟随视觉变换时可在内部元素上开启 `.transformedHitTest()`。
- Runtime 按绘制顺序从最上层往下命中 interactive 元素,同一位置只会触发最上层交互层;focus 命中会被上层 interactive blocker 阻断。
- 复杂嵌套滚动、圆角 clip、事件冒泡等能力仍属于 Runtime 后续增强范围
- 交互状态基础层区分 `normal`、`hovered`、`pressed`、`focused`、`disabled`;鼠标悬停表示指针位置,焦点表示键盘/程序操作目标,两者可以同时存在。`checked`、`selected`、`expanded`、`loading`、`error` 是可叠加的控件状态,不应覆盖基础交互状态。当前版本先提供统一状态优先级和主题 token,具体控件的 focus ring、键盘导航和无障碍语义会分阶段接入

## 写新组件时的底线

Expand Down
Loading