///|
/// Component theme tokens. This file lifts per-control design tokens from the
/// `moui_theme` addon into `core` so `Theme` can carry a first-class
/// `components` field. Token structs hold raw `Color`/`Double` values (the
/// "token" layer); `ControlStateStyle` holds the resolved `Brush`/`BorderStyle`
/// values that paint consumes. Both layers live in `core` because they only
/// depend on `core` value types (`Color`, `Brush`, `BorderStyle`,
/// `ShadowStyle`).
///
/// Resolution flow:
/// `ControlStateTokens` --resolve()--> `ControlStateStyle`
/// `ButtonTheme` --resolve(variant, state)--> `ControlStateStyle`
///
/// Controls read `ctx.environment.theme.components.` at paint time
/// and resolve the matching variant/state; `style?` on each control
/// constructor is a one-shot override that bypasses this resolution.
///|
/// Control interaction state for token resolution. This is the token-layer
/// state enum used to select a `ControlStateStyle` from a component theme. It
/// is distinct from the runtime `ButtonState` (a three-state gesture enum):
/// `InteractionState` covers the union of states every component theme needs
/// to resolve (normal/hovered/pressed/focused/disabled/selected/invalid), and
/// individual controls only pass the subset they support.
pub(all) enum InteractionState {
Normal
Hovered
Pressed
Focused
Disabled
Selected
Invalid
} derive(Eq, Debug, ToJson)
///|
/// Convert a runtime `ButtonState` (gesture-driven three-state) into the
/// token-layer `InteractionState` for button resolution.
pub fn ButtonState::to_interaction(self : ButtonState) -> InteractionState {
match self {
Normal => InteractionState::Normal
Hovered => InteractionState::Hovered
Pressed => InteractionState::Pressed
}
}
///|
/// Raw per-state token. Token layer: colors are plain `Color`, background is a
/// `Color` (paint resolves it to `Brush::solid`). `border_width == 0.0` means
/// "no border". `bottom_border_only` paints only the bottom edge (Fluent 2
/// underline inputs). `inner_focus_border` paints an inner stroke inside the
/// control frame on focus (Fluent 2 focus reveal).
pub(all) struct ControlStateTokens {
foreground : Color
background : Color
border : Color
border_width : Double
radius : Double
bottom_border_only : Bool
inner_focus_border : Color?
} derive(Eq, Debug, ToJson)
///|
/// Resolved per-state style. Paint layer: `background` is a `Brush`, `border`
/// is `None` when `border_width == 0.0`. This is the type controls actually
/// consume when emitting `DrawCommand`s.
pub(all) struct ControlStateStyle {
foreground : Color
background : Brush
border : BorderStyle?
radius : Double
shadow : ShadowStyle?
bottom_border_only : Bool
inner_focus_border : Color?
} derive(Eq, Debug, ToJson)
///|
/// Convenience constructor for `ControlStateTokens`.
pub fn ControlStateTokens::new(
foreground~ : Color,
background~ : Color,
border~ : Color,
border_width~ : Double,
radius~ : Double,
bottom_border_only? : Bool = false,
inner_focus_border? : Color? = None,
) -> ControlStateTokens {
{
foreground,
background,
border,
border_width,
radius,
bottom_border_only,
inner_focus_border,
}
}
///|
/// Resolve a token set into a paint-ready `ControlStateStyle`. The border is
/// dropped when `border_width <= 0.0`. `shadow` is always `None` here; layered
/// shadows are attached by the component theme that owns the token (e.g.
/// `SurfaceTheme`), not by individual control states.
pub fn ControlStateTokens::resolve(
self : ControlStateTokens,
) -> ControlStateStyle {
{
foreground: self.foreground,
background: Brush::Solid(self.background),
border: control_state_border(self),
radius: self.radius,
shadow: None,
bottom_border_only: self.bottom_border_only,
inner_focus_border: self.inner_focus_border,
}
}
///|
fn control_state_border(state : ControlStateTokens) -> BorderStyle? {
if state.border_width > 0.0 {
Some(
BorderStyle::new(
brush=Brush::Solid(state.border),
width=state.border_width,
),
)
} else {
None
}
}
// ---------------------------------------------------------------------------
// Button
// ---------------------------------------------------------------------------
///|
/// Button variant tokens. Each variant carries its own `ControlStateTokens`
/// for the normal state; hover/press/disabled are derived from the state-layer
/// model (see `ButtonTheme::resolve`). Fluent 2 fills `subtle`/`subtle_brand`
/// distinctly; neutral presets alias them to `ghost`/`tonal`.
pub(all) struct ButtonTheme {
primary : ControlStateTokens
tonal : ControlStateTokens
outline : ControlStateTokens
ghost : ControlStateTokens
subtle : ControlStateTokens
subtle_brand : ControlStateTokens
// State-layer alphas used to derive hovered/pressed/disabled backgrounds.
state_layer : StateLayerTokens
} derive(Eq, Debug, ToJson)
///|
pub(all) enum ButtonVariantToken {
Primary
Tonal
Outline
Ghost
Subtle
SubtleBrand
} derive(Eq, Debug, ToJson)
///|
/// State-layer tokens. Shared by all controls that use the state-layer model
/// (buttons, list rows, menu items): a translucent overlay of `color` is
/// blended onto the base background at `hover_alpha`/`focus_alpha`/... to
/// produce interaction feedback without per-variant color shifts.
pub(all) struct StateLayerTokens {
color : Color
hover_alpha : Double
focus_alpha : Double
pressed_alpha : Double
dragged_alpha : Double
selected_alpha : Double
disabled_alpha : Double
} derive(Eq, Debug, ToJson)
///|
/// Resolve a button variant + interaction state into a paint-ready
/// `ControlStateStyle`. Hover/press/disabled are derived from the variant's
/// normal tokens via the state-layer model so every variant shares a uniform
/// interaction grammar.
pub fn ButtonTheme::resolve(
self : ButtonTheme,
variant : ButtonVariantToken,
state : InteractionState,
) -> ControlStateStyle {
let base = self.variant_tokens(variant)
let normal = base.resolve()
match state {
Normal => normal
Hovered =>
{
..normal,
background: Brush::Solid(
state_layer_background(
base.background,
self.state_layer.color,
self.state_layer.hover_alpha,
),
),
}
Pressed =>
{
..normal,
background: Brush::Solid(
state_layer_background(
base.background,
self.state_layer.color,
self.state_layer.pressed_alpha,
),
),
}
Focused =>
{
..normal,
background: Brush::Solid(
state_layer_background(
base.background,
self.state_layer.color,
self.state_layer.focus_alpha,
),
),
}
Disabled =>
{
..normal,
foreground: normal.foreground.multiply_alpha(
self.state_layer.disabled_alpha,
),
background: normal.background.multiply_alpha(
self.state_layer.disabled_alpha,
),
border: control_state_disabled_border(normal.border, self.state_layer),
shadow: None,
}
Selected | Invalid => normal
}
}
///|
fn ButtonTheme::variant_tokens(
self : ButtonTheme,
variant : ButtonVariantToken,
) -> ControlStateTokens {
match variant {
Primary => self.primary
Tonal => self.tonal
Outline => self.outline
Ghost => self.ghost
Subtle => self.subtle
SubtleBrand => self.subtle_brand
}
}
///|
fn control_state_disabled_border(
border : BorderStyle?,
state_layer : StateLayerTokens,
) -> BorderStyle? {
match border {
Some(value) =>
Some({
..value,
brush: value.brush.multiply_alpha(state_layer.disabled_alpha),
})
None => None
}
}
///|
/// Blend a translucent state-layer color onto a base background. When the base
/// is fully transparent, the layer is emitted at the requested alpha; otherwise
/// the layer is composited over the base keeping the base alpha.
fn state_layer_background(
background : Color,
layer : Color,
alpha : Double,
) -> Color {
if background.a <= 0.0 {
layer.with_alpha(alpha)
} else {
let amount = clamp_unit(alpha)
Color::rgba(
r=background.r * (1.0 - amount) + layer.r * amount,
g=background.g * (1.0 - amount) + layer.g * amount,
b=background.b * (1.0 - amount) + layer.b * amount,
a=background.a,
)
}
}
///|
fn clamp_unit(value : Double) -> Double {
if value < 0.0 {
0.0
} else if value > 1.0 {
1.0
} else {
value
}
}
// ---------------------------------------------------------------------------
// TextField
// ---------------------------------------------------------------------------
///|
/// Text-field appearance tier. Fluent 2 ships an underline variant; the neutral
/// default is `Outline` (full border).
pub(all) enum TextFieldAppearance {
Filled
Outline
Underline
} derive(Eq, Debug, ToJson)
///|
pub(all) struct TextFieldTheme {
normal : ControlStateTokens
focused : ControlStateTokens
disabled : ControlStateTokens
placeholder : Color
cursor : Color
font : FontSpec
appearance : TextFieldAppearance
} derive(Eq, Debug, ToJson)
///|
pub fn TextFieldTheme::resolve(
self : TextFieldTheme,
state : InteractionState,
) -> ControlStateStyle {
match state {
Focused => self.focused.resolve()
Disabled => self.disabled.resolve()
_ => self.normal.resolve()
}
}
// ---------------------------------------------------------------------------
// Surface
// ---------------------------------------------------------------------------
///|
/// Per-tier surface component tokens. Fluent 2 cards carry distinct shadows
/// per layer (shadow2/4/28); neutral presets leave them `None` and rely on the
/// flat surface idiom.
pub(all) struct SurfaceTheme {
base_padding : Double
raised_padding : Double
overlay_padding : Double
base_radius : Double
raised_radius : Double
overlay_radius : Double
base_shadow : ShadowStyle?
raised_shadow : ShadowStyle?
overlay_shadow : ShadowStyle?
} derive(Eq, Debug, ToJson)
// ---------------------------------------------------------------------------
// Choice / progress / slider / picker / feedback / badge / form validation
// ---------------------------------------------------------------------------
///|
/// Shape of a choice-control box (checkbox/radio). `Square` draws a rounded
/// rectangle; `Circle` draws a full circle. Fluent 2 radios use `Circle`,
/// checkboxes use `Square`.
pub(all) enum ChoiceControlShape {
Square
Circle
} derive(Eq, Debug, ToJson)
///|
/// Short label for revision/identity strings.
pub fn ChoiceControlShape::label(self : ChoiceControlShape) -> String {
match self {
Square => "square"
Circle => "circle"
}
}
///|
/// How a selected choice control marks itself. `Checkmark` draws a "✓" glyph
/// on the filled box; `Dot` draws a small filled inner circle (radio style).
pub(all) enum CheckMarkStyle {
Checkmark
Dot
} derive(Eq, Debug, ToJson)
///|
pub fn CheckMarkStyle::label(self : CheckMarkStyle) -> String {
match self {
Checkmark => "checkmark"
Dot => "dot"
}
}
///|
/// Shape of a slider thumb. `Rounded` uses the theme radius; `Circle` forces a
/// full circle (thumb radius = thumb_size / 2). Fluent 2 sliders use `Circle`.
pub(all) enum ThumbShape {
Rounded
Circle
} derive(Eq, Debug, ToJson)
///|
pub fn ThumbShape::label(self : ThumbShape) -> String {
match self {
Rounded => "rounded"
Circle => "circle"
}
}
///|
pub(all) struct ChoiceControlTheme {
foreground : Color
selected_color : Color
disabled_foreground : Color
disabled_selected_color : Color
control_size : Double
// Shape of the indicator box. Square for checkboxes, Circle for radios.
box_shape : ChoiceControlShape
// How a selected indicator renders its mark. Checkmark for checkboxes, Dot
// for radios.
check_style : CheckMarkStyle
} derive(Eq, Debug, ToJson)
///|
pub(all) struct ProgressTheme {
foreground : Color
background : Color
radius : Double
} derive(Eq, Debug, ToJson)
///|
pub(all) struct SliderTheme {
foreground : Color
background : Color
thumb_color : Color
track_height : Double
thumb_size : Double
radius : Double
// Thumb shape. Circle forces a round thumb regardless of `radius`.
thumb_shape : ThumbShape
} derive(Eq, Debug, ToJson)
///|
pub(all) struct PickerTheme {
font : FontSpec
foreground : Color
background : Brush
radius : Double
popup_max_height : Double
} derive(Eq, Debug, ToJson)
///|
pub(all) struct ToneTokens {
foreground : Color
background : Color
border : Color
} derive(Eq, Debug, ToJson)
///|
pub(all) struct FeedbackTheme {
neutral : ToneTokens
info : ToneTokens
success : ToneTokens
warning : ToneTokens
danger : ToneTokens
radius : Double
padding : Double
} derive(Eq, Debug, ToJson)
///|
pub(all) struct BadgeTheme {
neutral : ToneTokens
accent : ToneTokens
success : ToneTokens
warning : ToneTokens
danger : ToneTokens
radius : Double
padding : Double
} derive(Eq, Debug, ToJson)
///|
pub(all) struct FormValidationTheme {
foreground : Color
background : Color
border : Color
radius : Double
padding : Double
} derive(Eq, Debug, ToJson)
// ---------------------------------------------------------------------------
// ComponentThemes aggregate
// ---------------------------------------------------------------------------
///|
/// First-class component theme aggregate carried by `Theme`. Each field is a
/// per-control token set; controls resolve variant/state at paint time via the
/// `resolve` methods above. An app that wants a branded design system sets
/// this once on the root theme and every control inherits it.
pub(all) struct ComponentThemes {
button : ButtonTheme
text_field : TextFieldTheme
surface : SurfaceTheme
choice_control : ChoiceControlTheme
progress : ProgressTheme
slider : SliderTheme
picker : PickerTheme
feedback : FeedbackTheme
badge : BadgeTheme
form_validation : FormValidationTheme
} derive(Eq, Debug, ToJson)
///|
pub fn Theme::with_components(
self : Theme,
components : ComponentThemes,
) -> Theme {
{ ..self, components, }
}