Skip to content
Merged
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
6 changes: 1 addition & 5 deletions crates/rustmotion-components/src/chart/funnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ impl Chart {
h: f32,
progress: f32,
) -> Result<()> {
let horizontal = self
.direction
.as_deref()
.map(|d| d == "horizontal")
.unwrap_or(false);
let horizontal = self.direction == Some(super::ChartDirection::Horizontal);

if horizontal {
self.render_funnel_horizontal(canvas, w, h, progress)
Expand Down
15 changes: 13 additions & 2 deletions crates/rustmotion-components/src/chart/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,21 @@ mod radial;
mod scatter;
mod waterfall;

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

/// Funnel flow direction. Closed set (painter matches both variants); JSON
/// values unchanged ("vertical"/"horizontal").
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ChartDirection {
Vertical,
Horizontal,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ChartType {
Expand Down Expand Up @@ -123,7 +134,7 @@ pub struct Chart {
// Funnel direction
/// Direction for funnel chart: "vertical" (default) or "horizontal".
#[serde(default)]
pub direction: Option<String>,
pub direction: Option<ChartDirection>,

// Axes, grid, labels
#[serde(default)]
Expand Down
37 changes: 24 additions & 13 deletions crates/rustmotion-components/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ pub struct Cursor {
pub click_duration: f32,
/// Visual cursor style: "default" (arrow) or "pointer" (hand).
/// Currently both render as a bar; this is metadata for future SVG cursors.
#[serde(default = "default_cursor_style")]
pub cursor_style: String,
#[serde(default)]
pub cursor_style: CursorStyle,
/// Easing for movement between waypoints: "ease_in_out" (default), "linear", "ease_out".
#[serde(default = "default_path_easing")]
pub path_easing: String,
#[serde(default)]
pub path_easing: CursorPathEasing,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
Expand Down Expand Up @@ -86,12 +86,24 @@ fn default_click_duration() -> f32 {
0.3
}

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

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

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

// Apply easing
let t = match self.path_easing.as_str() {
"linear" => raw_t,
"ease_out" => 1.0 - (1.0 - raw_t).powi(3),
_ => {
// ease_in_out
let t = match self.path_easing {
CursorPathEasing::Linear => raw_t,
CursorPathEasing::EaseOut => 1.0 - (1.0 - raw_t).powi(3),
CursorPathEasing::EaseInOut => {
if raw_t < 0.5 {
4.0 * raw_t * raw_t * raw_t
} else {
Expand Down
18 changes: 13 additions & 5 deletions crates/rustmotion-components/src/stepper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ fn default_transition_duration() -> f64 {
0.5
}

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

fn default_active_color() -> String {
Expand Down Expand Up @@ -64,8 +72,8 @@ pub struct Stepper {
#[serde(default = "default_transition_duration")]
pub transition_duration: f64,
/// Layout direction: "horizontal" or "vertical".
#[serde(default = "default_orientation")]
pub orientation: String,
#[serde(default)]
pub orientation: StepperOrientation,
/// Color of the active step node.
#[serde(default = "default_active_color")]
pub active_color: String,
Expand Down Expand Up @@ -146,7 +154,7 @@ impl Stepper {
let emoji_desc_font =
emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, desc_font_size));

let is_horizontal = self.orientation == "horizontal";
let is_horizontal = self.orientation == StepperOrientation::Horizontal;

if is_horizontal {
let padding = r + 8.0;
Expand Down
63 changes: 63 additions & 0 deletions crates/rustmotion-studio/src/editor/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,69 @@ fn ffmpeg_available() -> bool {
.unwrap_or(false)
}

/// Floating export-status toast, bottom-right of the canvas area (absolute in
/// the canvas container — no measurement). Running → live progression;
/// success → auto-dismissed after ~6 s; failure → persistent with a close
/// button. Replaces the old topbar status spans, which overlapped the
/// centered scenario title.
#[component]
pub fn ExportToast() -> Element {
let export = use_hook(export_slot);
let status = use_signal(|| ExportStatus::Idle);
use_export_poll(export.clone(), status);
let mut dismissed = use_signal(|| None::<ExportStatus>);

// Auto-dismiss success ~6 s after it appears (a NEW export produces a
// different status value, so the toast reappears naturally).
use_effect(move || {
let s = status();
if matches!(s, ExportStatus::Done(_)) && dismissed.peek().as_ref() != Some(&s) {
spawn(async move {
tokio::time::sleep(Duration::from_secs(6)).await;
if *status.peek() == s {
dismissed.set(Some(s));
}
});
}
});

let s = status();
if matches!(s, ExportStatus::Idle) || dismissed() == Some(s.clone()) {
return rsx! {};
}

rsx! {
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);",
match &s {
ExportStatus::Running { .. } => rsx! {
span { style: "color:var(--rm-text); white-space:nowrap;", "{export_label(&s)}" }
},
ExportStatus::Done(path) => rsx! {
span {
title: "{path.display()}",
style: "color:var(--rm-text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;",
"Exported: {path.display()}"
}
},
ExportStatus::Failed(reason) => rsx! {
span {
title: "{reason}",
style: "color:var(--rm-error); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;",
"Export failed: {reason}"
}
button {
style: "background:none; border:none; color:var(--rm-text-muted); cursor:pointer; font-size:13px; padding:0 2px;",
title: "Dismiss",
onclick: move |_| dismissed.set(Some(s.clone())),
"✕"
}
},
ExportStatus::Idle => rsx! {},
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading