venus_core/
widgets.rs

1//! Interactive widgets for Venus notebooks.
2//!
3//! Widgets allow notebook users to create interactive inputs like sliders,
4//! text boxes, and dropdowns that trigger cell re-execution when values change.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use venus::prelude::*;
10//!
11//! #[venus::cell]
12//! pub fn interactive() -> String {
13//!     let speed = venus::input_slider("speed", 0.0, 100.0, 50.0);
14//!     let name = venus::input_text("name", "Enter your name");
15//!     let mode = venus::input_select("mode", &["Fast", "Slow", "Auto"], 0);
16//!
17//!     format!("Speed: {}, Name: {}, Mode: {}", speed, name, mode)
18//! }
19//! ```
20
21use serde::{Deserialize, Serialize};
22use std::cell::RefCell;
23use std::collections::HashMap;
24
25/// Widget definition sent to the frontend.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum WidgetDef {
29    /// Numeric slider widget.
30    Slider {
31        /// Unique widget ID within the cell.
32        id: String,
33        /// Human-readable label.
34        label: String,
35        /// Minimum value.
36        min: f64,
37        /// Maximum value.
38        max: f64,
39        /// Step increment.
40        step: f64,
41        /// Current value.
42        value: f64,
43    },
44    /// Text input widget.
45    TextInput {
46        /// Unique widget ID within the cell.
47        id: String,
48        /// Human-readable label.
49        label: String,
50        /// Placeholder text.
51        placeholder: String,
52        /// Current value.
53        value: String,
54    },
55    /// Dropdown select widget.
56    Select {
57        /// Unique widget ID within the cell.
58        id: String,
59        /// Human-readable label.
60        label: String,
61        /// Available options.
62        options: Vec<String>,
63        /// Currently selected index.
64        selected: usize,
65    },
66    /// Checkbox widget.
67    Checkbox {
68        /// Unique widget ID within the cell.
69        id: String,
70        /// Human-readable label.
71        label: String,
72        /// Current value.
73        value: bool,
74    },
75}
76
77impl WidgetDef {
78    /// Get the widget ID.
79    pub fn id(&self) -> &str {
80        match self {
81            WidgetDef::Slider { id, .. } => id,
82            WidgetDef::TextInput { id, .. } => id,
83            WidgetDef::Select { id, .. } => id,
84            WidgetDef::Checkbox { id, .. } => id,
85        }
86    }
87}
88
89/// Widget value that can be stored in state.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(untagged)]
92pub enum WidgetValue {
93    /// Numeric value (for sliders).
94    Number(f64),
95    /// String value (for text inputs).
96    Text(String),
97    /// Index value (for selects).
98    Index(usize),
99    /// Boolean value (for checkboxes).
100    Bool(bool),
101}
102
103impl WidgetValue {
104    /// Get as f64 if it's a number.
105    pub fn as_f64(&self) -> Option<f64> {
106        match self {
107            WidgetValue::Number(n) => Some(*n),
108            _ => None,
109        }
110    }
111
112    /// Get as String if it's text.
113    pub fn as_string(&self) -> Option<&str> {
114        match self {
115            WidgetValue::Text(s) => Some(s),
116            _ => None,
117        }
118    }
119
120    /// Get as usize if it's an index.
121    pub fn as_index(&self) -> Option<usize> {
122        match self {
123            WidgetValue::Index(i) => Some(*i),
124            _ => None,
125        }
126    }
127
128    /// Get as bool if it's a boolean.
129    pub fn as_bool(&self) -> Option<bool> {
130        match self {
131            WidgetValue::Bool(b) => Some(*b),
132            _ => None,
133        }
134    }
135}
136
137/// Thread-local widget context for the current cell execution.
138///
139/// This is set by the executor before calling the cell function,
140/// and allows widgets to:
141/// 1. Register themselves (so the frontend knows about them)
142/// 2. Read their current value (set by user interaction)
143#[derive(Debug, Default)]
144pub struct WidgetContext {
145    /// Registered widgets during this execution.
146    pub widgets: Vec<WidgetDef>,
147    /// Current widget values (set by user interaction).
148    pub values: HashMap<String, WidgetValue>,
149}
150
151impl WidgetContext {
152    /// Create a new empty widget context.
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    /// Create a widget context with pre-set values.
158    pub fn with_values(values: HashMap<String, WidgetValue>) -> Self {
159        Self {
160            widgets: Vec::new(),
161            values,
162        }
163    }
164
165    /// Register a widget and return its current value.
166    fn register_slider(&mut self, id: &str, label: &str, min: f64, max: f64, step: f64, default: f64) -> f64 {
167        let value = self
168            .values
169            .get(id)
170            .and_then(|v| v.as_f64())
171            .unwrap_or(default)
172            .clamp(min, max);
173
174        self.widgets.push(WidgetDef::Slider {
175            id: id.to_string(),
176            label: label.to_string(),
177            min,
178            max,
179            step,
180            value,
181        });
182
183        value
184    }
185
186    /// Register a text input and return its current value.
187    fn register_text_input(&mut self, id: &str, label: &str, placeholder: &str, default: &str) -> String {
188        let value = self
189            .values
190            .get(id)
191            .and_then(|v| v.as_string())
192            .map(|s| s.to_string())
193            .unwrap_or_else(|| default.to_string());
194
195        self.widgets.push(WidgetDef::TextInput {
196            id: id.to_string(),
197            label: label.to_string(),
198            placeholder: placeholder.to_string(),
199            value: value.clone(),
200        });
201
202        value
203    }
204
205    /// Register a select widget and return the currently selected option.
206    fn register_select(&mut self, id: &str, label: &str, options: &[&str], default: usize) -> String {
207        let selected = self
208            .values
209            .get(id)
210            .and_then(|v| v.as_index())
211            .unwrap_or(default)
212            .min(options.len().saturating_sub(1));
213
214        self.widgets.push(WidgetDef::Select {
215            id: id.to_string(),
216            label: label.to_string(),
217            options: options.iter().map(|s| s.to_string()).collect(),
218            selected,
219        });
220
221        options.get(selected).map(|s| s.to_string()).unwrap_or_default()
222    }
223
224    /// Register a checkbox and return its current value.
225    fn register_checkbox(&mut self, id: &str, label: &str, default: bool) -> bool {
226        let value = self
227            .values
228            .get(id)
229            .and_then(|v| v.as_bool())
230            .unwrap_or(default);
231
232        self.widgets.push(WidgetDef::Checkbox {
233            id: id.to_string(),
234            label: label.to_string(),
235            value,
236        });
237
238        value
239    }
240
241    /// Get all registered widgets.
242    pub fn take_widgets(&mut self) -> Vec<WidgetDef> {
243        std::mem::take(&mut self.widgets)
244    }
245}
246
247thread_local! {
248    /// Thread-local widget context for the current cell execution.
249    static WIDGET_CONTEXT: RefCell<Option<WidgetContext>> = const { RefCell::new(None) };
250}
251
252/// Set the widget context for the current cell execution.
253///
254/// This is called by the executor before calling the cell function.
255pub fn set_widget_context(ctx: WidgetContext) {
256    WIDGET_CONTEXT.with(|c| {
257        *c.borrow_mut() = Some(ctx);
258    });
259}
260
261/// Take the widget context after cell execution.
262///
263/// This is called by the executor after the cell function returns.
264pub fn take_widget_context() -> Option<WidgetContext> {
265    WIDGET_CONTEXT.with(|c| c.borrow_mut().take())
266}
267
268/// Access the widget context, returning a default if not set.
269fn with_context<F, R>(f: F) -> R
270where
271    F: FnOnce(&mut WidgetContext) -> R,
272{
273    WIDGET_CONTEXT.with(|c| {
274        let mut ctx = c.borrow_mut();
275        if ctx.is_none() {
276            *ctx = Some(WidgetContext::new());
277        }
278        f(ctx.as_mut().unwrap())
279    })
280}
281
282// =============================================================================
283// Public Widget API
284// =============================================================================
285
286/// Create a numeric slider widget.
287///
288/// Returns the current slider value (set by user or default).
289/// When the user moves the slider, the cell automatically re-executes.
290///
291/// # Arguments
292///
293/// * `id` - Unique identifier for this widget within the cell
294/// * `min` - Minimum slider value
295/// * `max` - Maximum slider value
296/// * `default` - Default value when first rendered
297///
298/// # Example
299///
300/// ```rust,ignore
301/// let speed = venus::input_slider("speed", 0.0, 100.0, 50.0);
302/// println!("Current speed: {}", speed);
303/// ```
304pub fn input_slider(id: &str, min: f64, max: f64, default: f64) -> f64 {
305    input_slider_with_step(id, min, max, 1.0, default)
306}
307
308/// Create a numeric slider widget with custom step.
309///
310/// Like `input_slider` but allows specifying the step increment.
311///
312/// # Arguments
313///
314/// * `id` - Unique identifier for this widget within the cell
315/// * `min` - Minimum slider value
316/// * `max` - Maximum slider value
317/// * `step` - Step increment for the slider
318/// * `default` - Default value when first rendered
319pub fn input_slider_with_step(id: &str, min: f64, max: f64, step: f64, default: f64) -> f64 {
320    with_context(|ctx| ctx.register_slider(id, id, min, max, step, default))
321}
322
323/// Create a numeric slider widget with custom label.
324///
325/// Like `input_slider` but allows specifying a human-readable label.
326///
327/// # Arguments
328///
329/// * `id` - Unique identifier for this widget within the cell
330/// * `label` - Human-readable label shown in the UI
331/// * `min` - Minimum slider value
332/// * `max` - Maximum slider value
333/// * `step` - Step increment for the slider
334/// * `default` - Default value when first rendered
335pub fn input_slider_labeled(id: &str, label: &str, min: f64, max: f64, step: f64, default: f64) -> f64 {
336    with_context(|ctx| ctx.register_slider(id, label, min, max, step, default))
337}
338
339/// Create a text input widget.
340///
341/// Returns the current text value (set by user or default).
342/// When the user changes the text, the cell automatically re-executes.
343///
344/// # Arguments
345///
346/// * `id` - Unique identifier for this widget within the cell
347/// * `placeholder` - Placeholder text shown when empty
348///
349/// # Example
350///
351/// ```rust,ignore
352/// let name = venus::input_text("name", "Enter your name");
353/// println!("Hello, {}!", name);
354/// ```
355pub fn input_text(id: &str, placeholder: &str) -> String {
356    input_text_with_default(id, placeholder, "")
357}
358
359/// Create a text input widget with a default value.
360///
361/// Like `input_text` but allows specifying an initial value.
362///
363/// # Arguments
364///
365/// * `id` - Unique identifier for this widget within the cell
366/// * `placeholder` - Placeholder text shown when empty
367/// * `default` - Default value when first rendered
368pub fn input_text_with_default(id: &str, placeholder: &str, default: &str) -> String {
369    with_context(|ctx| ctx.register_text_input(id, id, placeholder, default))
370}
371
372/// Create a text input widget with custom label.
373///
374/// # Arguments
375///
376/// * `id` - Unique identifier for this widget within the cell
377/// * `label` - Human-readable label shown in the UI
378/// * `placeholder` - Placeholder text shown when empty
379/// * `default` - Default value when first rendered
380pub fn input_text_labeled(id: &str, label: &str, placeholder: &str, default: &str) -> String {
381    with_context(|ctx| ctx.register_text_input(id, label, placeholder, default))
382}
383
384/// Create a dropdown select widget.
385///
386/// Returns the currently selected option as a string.
387/// When the user selects a different option, the cell automatically re-executes.
388///
389/// # Arguments
390///
391/// * `id` - Unique identifier for this widget within the cell
392/// * `options` - List of available options
393/// * `default` - Index of the default selection (0-based)
394///
395/// # Example
396///
397/// ```rust,ignore
398/// let mode = venus::input_select("mode", &["Fast", "Normal", "Slow"], 1);
399/// println!("Selected mode: {}", mode);
400/// ```
401pub fn input_select(id: &str, options: &[&str], default: usize) -> String {
402    with_context(|ctx| ctx.register_select(id, id, options, default))
403}
404
405/// Create a dropdown select widget with custom label.
406///
407/// # Arguments
408///
409/// * `id` - Unique identifier for this widget within the cell
410/// * `label` - Human-readable label shown in the UI
411/// * `options` - List of available options
412/// * `default` - Index of the default selection (0-based)
413pub fn input_select_labeled(id: &str, label: &str, options: &[&str], default: usize) -> String {
414    with_context(|ctx| ctx.register_select(id, label, options, default))
415}
416
417/// Create a checkbox widget.
418///
419/// Returns the current boolean value.
420/// When the user toggles the checkbox, the cell automatically re-executes.
421///
422/// # Arguments
423///
424/// * `id` - Unique identifier for this widget within the cell
425/// * `default` - Default value when first rendered
426///
427/// # Example
428///
429/// ```rust,ignore
430/// let enabled = venus::input_checkbox("enabled", true);
431/// if enabled {
432///     println!("Feature is enabled!");
433/// }
434/// ```
435pub fn input_checkbox(id: &str, default: bool) -> bool {
436    with_context(|ctx| ctx.register_checkbox(id, id, default))
437}
438
439/// Create a checkbox widget with custom label.
440///
441/// # Arguments
442///
443/// * `id` - Unique identifier for this widget within the cell
444/// * `label` - Human-readable label shown in the UI
445/// * `default` - Default value when first rendered
446pub fn input_checkbox_labeled(id: &str, label: &str, default: bool) -> bool {
447    with_context(|ctx| ctx.register_checkbox(id, label, default))
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_slider_registration() {
456        let ctx = WidgetContext::new();
457        set_widget_context(ctx);
458
459        let value = input_slider("speed", 0.0, 100.0, 50.0);
460        assert_eq!(value, 50.0);
461
462        let ctx = take_widget_context().unwrap();
463        assert_eq!(ctx.widgets.len(), 1);
464        match &ctx.widgets[0] {
465            WidgetDef::Slider { id, min, max, value, .. } => {
466                assert_eq!(id, "speed");
467                assert_eq!(*min, 0.0);
468                assert_eq!(*max, 100.0);
469                assert_eq!(*value, 50.0);
470            }
471            _ => panic!("Expected slider"),
472        }
473    }
474
475    #[test]
476    fn test_slider_with_existing_value() {
477        let mut values = HashMap::new();
478        values.insert("speed".to_string(), WidgetValue::Number(75.0));
479        let ctx = WidgetContext::with_values(values);
480        set_widget_context(ctx);
481
482        let value = input_slider("speed", 0.0, 100.0, 50.0);
483        assert_eq!(value, 75.0);
484
485        let ctx = take_widget_context().unwrap();
486        match &ctx.widgets[0] {
487            WidgetDef::Slider { value, .. } => {
488                assert_eq!(*value, 75.0);
489            }
490            _ => panic!("Expected slider"),
491        }
492    }
493
494    #[test]
495    fn test_text_input_registration() {
496        let ctx = WidgetContext::new();
497        set_widget_context(ctx);
498
499        let value = input_text("name", "Enter name");
500        assert_eq!(value, "");
501
502        let ctx = take_widget_context().unwrap();
503        assert_eq!(ctx.widgets.len(), 1);
504        match &ctx.widgets[0] {
505            WidgetDef::TextInput { id, placeholder, .. } => {
506                assert_eq!(id, "name");
507                assert_eq!(placeholder, "Enter name");
508            }
509            _ => panic!("Expected text input"),
510        }
511    }
512
513    #[test]
514    fn test_select_registration() {
515        let ctx = WidgetContext::new();
516        set_widget_context(ctx);
517
518        let value = input_select("mode", &["Fast", "Normal", "Slow"], 1);
519        assert_eq!(value, "Normal");
520
521        let ctx = take_widget_context().unwrap();
522        assert_eq!(ctx.widgets.len(), 1);
523        match &ctx.widgets[0] {
524            WidgetDef::Select { id, options, selected, .. } => {
525                assert_eq!(id, "mode");
526                assert_eq!(options, &["Fast", "Normal", "Slow"]);
527                assert_eq!(*selected, 1);
528            }
529            _ => panic!("Expected select"),
530        }
531    }
532
533    #[test]
534    fn test_checkbox_registration() {
535        let ctx = WidgetContext::new();
536        set_widget_context(ctx);
537
538        let value = input_checkbox("enabled", true);
539        assert!(value);
540
541        let ctx = take_widget_context().unwrap();
542        assert_eq!(ctx.widgets.len(), 1);
543        match &ctx.widgets[0] {
544            WidgetDef::Checkbox { id, value, .. } => {
545                assert_eq!(id, "enabled");
546                assert!(*value);
547            }
548            _ => panic!("Expected checkbox"),
549        }
550    }
551
552    #[test]
553    fn test_widget_value_clamping() {
554        let mut values = HashMap::new();
555        values.insert("speed".to_string(), WidgetValue::Number(150.0)); // Over max
556        let ctx = WidgetContext::with_values(values);
557        set_widget_context(ctx);
558
559        let value = input_slider("speed", 0.0, 100.0, 50.0);
560        assert_eq!(value, 100.0); // Clamped to max
561
562        take_widget_context();
563    }
564}