Skip to main content

dioxuscut_player/
controls.rs

1//! Playback controls UI for the `<Player>` component.
2
3use dioxus::prelude::*;
4
5/// Props for the `<Controls>` bar.
6#[derive(Props, Clone, PartialEq)]
7pub struct ControlsProps {
8    /// Current frame number.
9    pub frame: u32,
10    /// Total frames.
11    pub duration: u32,
12    /// Whether the player is currently playing.
13    pub playing: bool,
14    /// Called when play/pause is clicked.
15    pub on_play_pause: EventHandler<MouseEvent>,
16    /// Called when the scrubber is moved, with the new frame number.
17    pub on_seek: EventHandler<u32>,
18}
19
20/// Playback controls — play/pause button + scrubber timeline.
21#[component]
22pub fn Controls(props: ControlsProps) -> Element {
23    let play_icon = if props.playing { "⏸" } else { "▶" };
24    let progress_pct = if props.duration > 1 {
25        props.frame as f64 / (props.duration - 1) as f64 * 100.0
26    } else {
27        0.0
28    };
29
30    let duration = props.duration;
31    let on_seek = props.on_seek;
32
33    rsx! {
34        div {
35            class: "dioxuscut-controls",
36            style: "
37                display: flex;
38                align-items: center;
39                gap: 10px;
40                width: 100%;
41                padding: 8px 12px;
42                background: rgba(20, 20, 30, 0.85);
43                backdrop-filter: blur(8px);
44                border-radius: 8px;
45                box-sizing: border-box;
46            ",
47
48            // Play / Pause button
49            button {
50                onclick: props.on_play_pause,
51                style: "
52                    background: none;
53                    border: none;
54                    color: white;
55                    font-size: 18px;
56                    cursor: pointer;
57                    padding: 4px 8px;
58                    border-radius: 4px;
59                    transition: background 0.15s;
60                ",
61                onmouseenter: |e: MouseEvent| {
62                    // Hover handled via CSS in production
63                    let _ = e;
64                },
65                "{play_icon}"
66            }
67
68            // Scrubber
69            div {
70                style: "
71                    flex: 1;
72                    position: relative;
73                    height: 20px;
74                    display: flex;
75                    align-items: center;
76                    cursor: pointer;
77                ",
78                // Track background
79                div {
80                    style: "
81                        position: absolute;
82                        left: 0; right: 0;
83                        height: 4px;
84                        background: rgba(255,255,255,0.2);
85                        border-radius: 2px;
86                    ",
87                }
88                // Progress fill
89                div {
90                    style: "
91                        position: absolute;
92                        left: 0;
93                        width: {progress_pct:.1}%;
94                        height: 4px;
95                        background: #6c63ff;
96                        border-radius: 2px;
97                        pointer-events: none;
98                    ",
99                }
100                // Invisible range input for interaction
101                input {
102                    r#type: "range",
103                    min: "0",
104                    max: "{duration.saturating_sub(1)}",
105                    value: "{props.frame}",
106                    style: "
107                        position: absolute;
108                        width: 100%;
109                        opacity: 0;
110                        cursor: pointer;
111                        height: 20px;
112                        margin: 0;
113                    ",
114                    oninput: move |e| {
115                        if let Ok(v) = e.value().parse::<u32>() {
116                            on_seek.call(v);
117                        }
118                    },
119                }
120            }
121
122            // Frame counter
123            span {
124                style: "color: rgba(255,255,255,0.6); font-size: 12px; font-family: monospace; min-width: 60px; text-align: right;",
125                "{props.frame}/{duration.saturating_sub(1)}"
126            }
127        }
128    }
129}