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}