Skip to content

Commit bf1ae4f

Browse files
authored
feat(studio+components): inspector rounds 6-7 — typed enums, palette colors, lists, toast, CSS defaults (#85)
Closes #84, #78-follow-ups. Real enum typing for closed-set fields, palette-prefilled chart colors with pixel-proven write path, generic ListRows (colors/numbers/strings), radar_data exclusion, export toast replacing the overlapping topbar spans, effective CSS defaults on every control.
1 parent 5ba9975 commit bf1ae4f

11 files changed

Lines changed: 770 additions & 157 deletions

File tree

crates/rustmotion-components/src/chart/funnel.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ impl Chart {
1616
h: f32,
1717
progress: f32,
1818
) -> Result<()> {
19-
let horizontal = self
20-
.direction
21-
.as_deref()
22-
.map(|d| d == "horizontal")
23-
.unwrap_or(false);
19+
let horizontal = self.direction == Some(super::ChartDirection::Horizontal);
2420

2521
if horizontal {
2622
self.render_funnel_horizontal(canvas, w, h, progress)

crates/rustmotion-components/src/chart/mod.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,21 @@ mod radial;
2020
mod scatter;
2121
mod waterfall;
2222

23-
pub(crate) const DEFAULT_PALETTE: &[&str] = &[
23+
/// The engine's default series palette. `pub` so the studio can prefill an
24+
/// empty `colors` list with what the canvas actually renders.
25+
pub const DEFAULT_PALETTE: &[&str] = &[
2426
"#3B82F6", "#EF4444", "#22C55E", "#F59E0B", "#8B5CF6", "#EC4899", "#06B6D4", "#F97316",
2527
];
2628

29+
/// Funnel flow direction. Closed set (painter matches both variants); JSON
30+
/// values unchanged ("vertical"/"horizontal").
31+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
32+
#[serde(rename_all = "snake_case")]
33+
pub enum ChartDirection {
34+
Vertical,
35+
Horizontal,
36+
}
37+
2738
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2839
#[serde(rename_all = "snake_case")]
2940
pub enum ChartType {
@@ -123,7 +134,7 @@ pub struct Chart {
123134
// Funnel direction
124135
/// Direction for funnel chart: "vertical" (default) or "horizontal".
125136
#[serde(default)]
126-
pub direction: Option<String>,
137+
pub direction: Option<ChartDirection>,
127138

128139
// Axes, grid, labels
129140
#[serde(default)]

crates/rustmotion-components/src/cursor.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ pub struct Cursor {
4747
pub click_duration: f32,
4848
/// Visual cursor style: "default" (arrow) or "pointer" (hand).
4949
/// Currently both render as a bar; this is metadata for future SVG cursors.
50-
#[serde(default = "default_cursor_style")]
51-
pub cursor_style: String,
50+
#[serde(default)]
51+
pub cursor_style: CursorStyle,
5252
/// Easing for movement between waypoints: "ease_in_out" (default), "linear", "ease_out".
53-
#[serde(default = "default_path_easing")]
54-
pub path_easing: String,
53+
#[serde(default)]
54+
pub path_easing: CursorPathEasing,
5555
#[serde(flatten)]
5656
pub timing: TimingConfig,
5757
#[serde(default)]
@@ -86,12 +86,24 @@ fn default_click_duration() -> f32 {
8686
0.3
8787
}
8888

89-
fn default_cursor_style() -> String {
90-
"default".to_string()
89+
/// Visual cursor style. Closed documented set; JSON values unchanged.
90+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
91+
#[serde(rename_all = "snake_case")]
92+
pub enum CursorStyle {
93+
#[default]
94+
Default,
95+
Pointer,
9196
}
9297

93-
fn default_path_easing() -> String {
94-
"ease_in_out".to_string()
98+
/// Easing of the cursor's waypoint path. Closed set, now matched
99+
/// exhaustively; previously-silent unknown values fail the typed parse.
100+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
101+
#[serde(rename_all = "snake_case")]
102+
pub enum CursorPathEasing {
103+
Linear,
104+
EaseOut,
105+
#[default]
106+
EaseInOut,
95107
}
96108

97109
rustmotion_core::impl_traits!(Cursor {
@@ -161,11 +173,10 @@ impl Cursor {
161173
let raw_t = ((time - move_start) / move_duration).clamp(0.0, 1.0);
162174

163175
// Apply easing
164-
let t = match self.path_easing.as_str() {
165-
"linear" => raw_t,
166-
"ease_out" => 1.0 - (1.0 - raw_t).powi(3),
167-
_ => {
168-
// ease_in_out
176+
let t = match self.path_easing {
177+
CursorPathEasing::Linear => raw_t,
178+
CursorPathEasing::EaseOut => 1.0 - (1.0 - raw_t).powi(3),
179+
CursorPathEasing::EaseInOut => {
169180
if raw_t < 0.5 {
170181
4.0 * raw_t * raw_t * raw_t
171182
} else {

crates/rustmotion-components/src/stepper.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ fn default_transition_duration() -> f64 {
2020
0.5
2121
}
2222

23-
fn default_orientation() -> String {
24-
"horizontal".to_string()
23+
/// Layout axis of the stepper. Closed set, matched exhaustively by the
24+
/// painter; serde snake_case keeps the JSON values identical
25+
/// ("horizontal"/"vertical") — an unknown value now fails the typed parse
26+
/// (blocking validate error, by design).
27+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
28+
#[serde(rename_all = "snake_case")]
29+
pub enum StepperOrientation {
30+
#[default]
31+
Horizontal,
32+
Vertical,
2533
}
2634

2735
fn default_active_color() -> String {
@@ -64,8 +72,8 @@ pub struct Stepper {
6472
#[serde(default = "default_transition_duration")]
6573
pub transition_duration: f64,
6674
/// Layout direction: "horizontal" or "vertical".
67-
#[serde(default = "default_orientation")]
68-
pub orientation: String,
75+
#[serde(default)]
76+
pub orientation: StepperOrientation,
6977
/// Color of the active step node.
7078
#[serde(default = "default_active_color")]
7179
pub active_color: String,
@@ -146,7 +154,7 @@ impl Stepper {
146154
let emoji_desc_font =
147155
emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, desc_font_size));
148156

149-
let is_horizontal = self.orientation == "horizontal";
157+
let is_horizontal = self.orientation == StepperOrientation::Horizontal;
150158

151159
if is_horizontal {
152160
let padding = r + 8.0;

crates/rustmotion-studio/src/editor/export.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,69 @@ fn ffmpeg_available() -> bool {
210210
.unwrap_or(false)
211211
}
212212

213+
/// Floating export-status toast, bottom-right of the canvas area (absolute in
214+
/// the canvas container — no measurement). Running → live progression;
215+
/// success → auto-dismissed after ~6 s; failure → persistent with a close
216+
/// button. Replaces the old topbar status spans, which overlapped the
217+
/// centered scenario title.
218+
#[component]
219+
pub fn ExportToast() -> Element {
220+
let export = use_hook(export_slot);
221+
let status = use_signal(|| ExportStatus::Idle);
222+
use_export_poll(export.clone(), status);
223+
let mut dismissed = use_signal(|| None::<ExportStatus>);
224+
225+
// Auto-dismiss success ~6 s after it appears (a NEW export produces a
226+
// different status value, so the toast reappears naturally).
227+
use_effect(move || {
228+
let s = status();
229+
if matches!(s, ExportStatus::Done(_)) && dismissed.peek().as_ref() != Some(&s) {
230+
spawn(async move {
231+
tokio::time::sleep(Duration::from_secs(6)).await;
232+
if *status.peek() == s {
233+
dismissed.set(Some(s));
234+
}
235+
});
236+
}
237+
});
238+
239+
let s = status();
240+
if matches!(s, ExportStatus::Idle) || dismissed() == Some(s.clone()) {
241+
return rsx! {};
242+
}
243+
244+
rsx! {
245+
div { style: "position:absolute; right:16px; bottom:16px; z-index:900; display:flex; align-items:center; gap:8px; max-width:60%; padding:8px 12px; font-size:12px; background:var(--rm-surface-2); border:1px solid var(--rm-border); border-radius:8px; box-shadow:0 6px 20px rgba(0,0,0,0.35);",
246+
match &s {
247+
ExportStatus::Running { .. } => rsx! {
248+
span { style: "color:var(--rm-text); white-space:nowrap;", "{export_label(&s)}" }
249+
},
250+
ExportStatus::Done(path) => rsx! {
251+
span {
252+
title: "{path.display()}",
253+
style: "color:var(--rm-text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;",
254+
"Exported: {path.display()}"
255+
}
256+
},
257+
ExportStatus::Failed(reason) => rsx! {
258+
span {
259+
title: "{reason}",
260+
style: "color:var(--rm-error); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;",
261+
"Export failed: {reason}"
262+
}
263+
button {
264+
style: "background:none; border:none; color:var(--rm-text-muted); cursor:pointer; font-size:13px; padding:0 2px;",
265+
title: "Dismiss",
266+
onclick: move |_| dismissed.set(Some(s.clone())),
267+
"✕"
268+
}
269+
},
270+
ExportStatus::Idle => rsx! {},
271+
}
272+
}
273+
}
274+
}
275+
213276
#[cfg(test)]
214277
mod tests {
215278
use super::*;

0 commit comments

Comments
 (0)