use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ViewTag(pub isize);
impl ViewTag {
pub fn new(tag: isize) -> Self {
Self(tag)
}
pub fn as_isize(&self) -> isize {
self.0
}
}
impl From<isize> for ViewTag {
fn from(tag: isize) -> Self {
Self(tag)
}
}
impl From<ViewTag> for isize {
fn from(tag: ViewTag) -> isize {
tag.0
}
}
#[derive(Debug)]
#[must_use = "Views must be used in a view tree or passed to App::root()"]
pub struct View {
pub(crate) kind: ViewKind,
pub(crate) children: Vec<View>,
pub(crate) style: ViewStyle,
pub(crate) event: Option<EventBinding>,
}
#[derive(Debug, Clone)]
pub enum ViewKind {
VStack {
spacing: f64,
},
HStack {
spacing: f64,
},
ZStack,
Spacer {
min_length: f64,
},
Text(String),
Label(String),
Button {
title: String,
},
TextField {
placeholder: String,
value: String,
},
SecureField {
placeholder: String,
},
Checkbox {
title: String,
checked: bool,
},
Radio {
title: String,
selected: bool,
},
Slider {
min: f64,
max: f64,
value: f64,
},
Toggle {
title: String,
on: bool,
},
Dropdown {
title: String,
options: Vec<String>,
selected: usize,
},
ScrollView {
direction: ScrollDirection,
},
TabView {
tabs: Vec<String>,
selected: usize,
},
SplitView {
orientation: Orientation,
ratio: f64,
},
GroupBox {
title: String,
},
ProgressBar {
value: f64,
max: f64,
},
Image {
name: String,
},
TextArea {
placeholder: String,
value: String,
},
DatePicker {
date: Option<String>,
},
ColorPicker {
color: Option<String>,
},
WebView {
url: String,
},
TableView {
columns: Vec<String>,
rows: Vec<Vec<String>>,
},
SegmentedControl {
segments: Vec<String>,
selected: usize,
},
ComboBox {
items: Vec<String>,
value: String,
},
SearchField {
placeholder: String,
value: String,
},
Stepper {
value: f64,
min: f64,
max: f64,
step: f64,
},
LevelIndicator {
value: f64,
min: f64,
max: f64,
style: LevelIndicatorStyle,
},
PathControl {
path: String,
},
Custom {
type_name: String,
properties: Vec<(String, String)>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum ScrollDirection {
Vertical,
Horizontal,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Orientation {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Alignment {
Leading,
Center,
Trailing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum LevelIndicatorStyle {
Relevancy = 0,
ContinuousCapacity = 1,
DiscreteCapacity = 2,
Rating = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct KeyModifiers {
pub command: bool,
pub option: bool,
pub shift: bool,
pub control: bool,
}
impl KeyModifiers {
pub fn command() -> Self {
Self {
command: true,
..Default::default()
}
}
pub fn option() -> Self {
Self {
option: true,
..Default::default()
}
}
pub fn shift() -> Self {
Self {
shift: true,
..Default::default()
}
}
pub fn control() -> Self {
Self {
control: true,
..Default::default()
}
}
pub fn command_shift() -> Self {
Self {
command: true,
shift: true,
..Default::default()
}
}
pub fn command_option() -> Self {
Self {
command: true,
option: true,
..Default::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ViewStyle {
pub width: Option<f64>,
pub height: Option<f64>,
pub padding: Option<f64>,
pub margin: Option<f64>,
pub background: Option<String>,
pub foreground: Option<String>,
pub font_size: Option<f64>,
pub font_bold: bool,
pub font_italic: bool,
pub corner_radius: Option<f64>,
pub border_width: Option<f64>,
pub border_color: Option<String>,
pub opacity: Option<f64>,
pub hidden: bool,
pub enabled: bool,
pub accessibility_label: Option<String>,
pub tag: Option<isize>,
pub alignment: Option<Alignment>,
pub uses_auto_layout: bool,
pub constraints: Vec<crate::layout::ConstraintDescriptor>,
pub shadow_radius: Option<f64>,
pub shadow_opacity: Option<f32>,
pub shadow_offset_x: Option<f64>,
pub shadow_offset_y: Option<f64>,
pub wants_layer: bool,
pub clips_to_bounds: bool,
pub accepts_first_responder: bool,
pub can_become_key_view: bool,
pub shortcut_key: Option<String>,
pub shortcut_modifiers: KeyModifiers,
}
impl Default for View {
fn default() -> Self {
Self {
kind: ViewKind::Spacer { min_length: 0.0 },
children: Vec::new(),
style: ViewStyle::default(),
event: None,
}
}
}
impl ViewStyle {
fn new() -> Self {
Self {
enabled: true,
..Default::default()
}
}
}
#[derive(Debug, Clone)]
#[must_use = "EventBinding must be attached to a View"]
pub struct EventBinding {
pub action: String,
pub callback_id: usize,
}
impl View {
pub fn vstack() -> Self {
Self {
kind: ViewKind::VStack { spacing: 8.0 },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn hstack() -> Self {
Self {
kind: ViewKind::HStack { spacing: 8.0 },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn zstack() -> Self {
Self {
kind: ViewKind::ZStack,
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn spacer() -> Self {
Self {
kind: ViewKind::Spacer { min_length: 0.0 },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn text(content: impl Into<String>) -> Self {
Self {
kind: ViewKind::Text(content.into()),
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn label(content: impl Into<String>) -> Self {
Self {
kind: ViewKind::Label(content.into()),
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn button(title: impl Into<String>) -> Self {
Self {
kind: ViewKind::Button {
title: title.into(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn text_field(placeholder: impl Into<String>) -> Self {
Self {
kind: ViewKind::TextField {
placeholder: placeholder.into(),
value: String::new(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn secure_field(placeholder: impl Into<String>) -> Self {
Self {
kind: ViewKind::SecureField {
placeholder: placeholder.into(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn checkbox(title: impl Into<String>) -> Self {
Self {
kind: ViewKind::Checkbox {
title: title.into(),
checked: false,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn radio(title: impl Into<String>) -> Self {
Self {
kind: ViewKind::Radio {
title: title.into(),
selected: false,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn slider(min: f64, max: f64, value: f64) -> Self {
Self {
kind: ViewKind::Slider { min, max, value },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn toggle(title: impl Into<String>) -> Self {
Self {
kind: ViewKind::Toggle {
title: title.into(),
on: false,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn dropdown(title: impl Into<String>, options: Vec<String>) -> Self {
Self {
kind: ViewKind::Dropdown {
title: title.into(),
options,
selected: 0,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn scroll_view() -> Self {
Self {
kind: ViewKind::ScrollView {
direction: ScrollDirection::Vertical,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn tab_view(tabs: Vec<impl Into<String>>) -> Self {
Self {
kind: ViewKind::TabView {
tabs: tabs.into_iter().map(|t| t.into()).collect(),
selected: 0,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn split_view() -> Self {
Self {
kind: ViewKind::SplitView {
orientation: Orientation::Vertical,
ratio: 0.5,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn group_box(title: impl Into<String>) -> Self {
Self {
kind: ViewKind::GroupBox {
title: title.into(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn progress_bar(value: f64, max: f64) -> Self {
Self {
kind: ViewKind::ProgressBar { value, max },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn image(name: impl Into<String>) -> Self {
Self {
kind: ViewKind::Image { name: name.into() },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn custom(type_name: impl Into<String>, properties: Vec<(String, String)>) -> Self {
Self {
kind: ViewKind::Custom {
type_name: type_name.into(),
properties,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn text_area(placeholder: impl Into<String>) -> Self {
Self {
kind: ViewKind::TextArea {
placeholder: placeholder.into(),
value: String::new(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn date_picker() -> Self {
Self {
kind: ViewKind::DatePicker { date: None },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn color_picker() -> Self {
Self {
kind: ViewKind::ColorPicker { color: None },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn web_view(url: impl Into<String>) -> Self {
Self {
kind: ViewKind::WebView { url: url.into() },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn table_view(columns: Vec<impl Into<String>>, rows: Vec<Vec<String>>) -> Self {
Self {
kind: ViewKind::TableView {
columns: columns.into_iter().map(|c| c.into()).collect(),
rows,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn segmented_control(segments: Vec<impl Into<String>>) -> Self {
Self {
kind: ViewKind::SegmentedControl {
segments: segments.into_iter().map(|s| s.into()).collect(),
selected: 0,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn combo_box(items: Vec<impl Into<String>>) -> Self {
Self {
kind: ViewKind::ComboBox {
items: items.into_iter().map(|i| i.into()).collect(),
value: String::new(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn search_field(placeholder: impl Into<String>) -> Self {
Self {
kind: ViewKind::SearchField {
placeholder: placeholder.into(),
value: String::new(),
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn stepper(min: f64, max: f64, value: f64) -> Self {
Self {
kind: ViewKind::Stepper {
value,
min,
max,
step: 1.0,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn level_indicator(min: f64, max: f64, value: f64) -> Self {
Self {
kind: ViewKind::LevelIndicator {
value,
min,
max,
style: LevelIndicatorStyle::ContinuousCapacity,
},
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
pub fn path_control(path: impl Into<String>) -> Self {
Self {
kind: ViewKind::PathControl { path: path.into() },
children: Vec::new(),
style: ViewStyle::new(),
event: None,
}
}
}
impl View {
pub fn child(mut self, child: View) -> Self {
self.children.push(child);
self
}
pub fn children(mut self, children: Vec<View>) -> Self {
self.children.extend(children);
self
}
pub fn add_child(&mut self, child: View) {
self.children.push(child);
}
}
impl View {
pub fn width(mut self, w: f64) -> Self {
self.style.width = Some(w);
self
}
pub fn height(mut self, h: f64) -> Self {
self.style.height = Some(h);
self
}
pub fn size(mut self, w: f64, h: f64) -> Self {
self.style.width = Some(w);
self.style.height = Some(h);
self
}
pub fn width_full(self) -> Self {
self.width(f64::MAX)
}
pub fn height_full(self) -> Self {
self.height(f64::MAX)
}
pub fn fill(self) -> Self {
self.width_full().height_full()
}
pub fn padding(mut self, p: f64) -> Self {
self.style.padding = Some(p);
self
}
pub fn margin(mut self, m: f64) -> Self {
self.style.margin = Some(m);
self
}
pub fn background(mut self, color: impl Into<String>) -> Self {
self.style.background = Some(color.into());
self
}
pub fn foreground(mut self, color: impl Into<String>) -> Self {
self.style.foreground = Some(color.into());
self
}
pub fn font_size(mut self, size: f64) -> Self {
self.style.font_size = Some(size);
self
}
pub fn bold(mut self) -> Self {
self.style.font_bold = true;
self
}
pub fn italic(mut self) -> Self {
self.style.font_italic = true;
self
}
pub fn corner_radius(mut self, r: f64) -> Self {
self.style.corner_radius = Some(r);
self
}
pub fn border(mut self, width: f64, color: impl Into<String>) -> Self {
self.style.border_width = Some(width);
self.style.border_color = Some(color.into());
self
}
pub fn opacity(mut self, o: f64) -> Self {
self.style.opacity = Some(o);
self
}
pub fn hidden(mut self, h: bool) -> Self {
self.style.hidden = h;
self
}
pub fn enabled(mut self, e: bool) -> Self {
self.style.enabled = e;
self
}
pub fn alignment(mut self, a: Alignment) -> Self {
self.style.alignment = Some(a);
self
}
pub fn accessibility(mut self, label: impl Into<String>) -> Self {
self.style.accessibility_label = Some(label.into());
self
}
pub fn tag(mut self, t: isize) -> Self {
self.style.tag = Some(t);
self
}
pub fn uses_auto_layout(mut self, uses: bool) -> Self {
self.style.uses_auto_layout = uses;
self
}
pub fn constraint(mut self, constraint: crate::layout::ConstraintDescriptor) -> Self {
self.style.uses_auto_layout = true;
self.style.constraints.push(constraint);
self
}
pub fn constraints(mut self, constraints: Vec<crate::layout::ConstraintDescriptor>) -> Self {
self.style.uses_auto_layout = true;
self.style.constraints.extend(constraints);
self
}
pub fn shadow(mut self, radius: f64, opacity: f32, offset_x: f64, offset_y: f64) -> Self {
self.style.shadow_radius = Some(radius);
self.style.shadow_opacity = Some(opacity);
self.style.shadow_offset_x = Some(offset_x);
self.style.shadow_offset_y = Some(offset_y);
self.style.wants_layer = true;
self
}
pub fn wants_layer(mut self, wants: bool) -> Self {
self.style.wants_layer = wants;
self
}
pub fn clips_to_bounds(mut self, clips: bool) -> Self {
self.style.clips_to_bounds = clips;
self.style.wants_layer = true;
self
}
pub fn accepts_first_responder(mut self, accepts: bool) -> Self {
self.style.accepts_first_responder = accepts;
self
}
pub fn can_become_key_view(mut self, can: bool) -> Self {
self.style.can_become_key_view = can;
self
}
pub fn shortcut(mut self, key: impl Into<String>) -> Self {
self.style.shortcut_key = Some(key.into());
self.style.shortcut_modifiers = KeyModifiers::command();
self
}
pub fn shortcut_with_modifiers(
mut self,
key: impl Into<String>,
modifiers: KeyModifiers,
) -> Self {
self.style.shortcut_key = Some(key.into());
self.style.shortcut_modifiers = modifiers;
self
}
}
impl View {
pub fn on_click(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "click".to_string(),
callback_id,
});
self
}
pub fn on_click_fn(mut self, f: impl Fn() + Send + 'static) -> Self {
let id = crate::event::register_with_auto_id(f);
self.event = Some(EventBinding {
action: "click".to_string(),
callback_id: id,
});
self
}
pub fn on_change(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "change".to_string(),
callback_id,
});
self
}
pub fn on_change_fn(mut self, f: impl Fn(String) + Send + 'static) -> Self {
let id = crate::event::register_value_with_auto_id(f);
self.event = Some(EventBinding {
action: "change_value".to_string(),
callback_id: id,
});
self
}
pub fn on_double_click(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "double_click".to_string(),
callback_id,
});
self
}
pub fn on_double_click_fn(mut self, f: impl Fn() + Send + 'static) -> Self {
let id = crate::event::register_with_auto_id(f);
self.event = Some(EventBinding {
action: "double_click".to_string(),
callback_id: id,
});
self
}
pub fn on_right_click(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "right_click".to_string(),
callback_id,
});
self
}
pub fn on_right_click_fn(mut self, f: impl Fn() + Send + 'static) -> Self {
let id = crate::event::register_with_auto_id(f);
self.event = Some(EventBinding {
action: "right_click".to_string(),
callback_id: id,
});
self
}
pub fn on_hover(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "hover".to_string(),
callback_id,
});
self
}
pub fn on_hover_fn(mut self, f: impl Fn(bool) + Send + 'static) -> Self {
let id = crate::event::register_bool_with_auto_id(f);
self.event = Some(EventBinding {
action: "hover_change".to_string(),
callback_id: id,
});
self
}
pub fn on_focus(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "focus".to_string(),
callback_id,
});
self
}
pub fn on_focus_fn(mut self, f: impl Fn(bool) + Send + 'static) -> Self {
let id = crate::event::register_bool_with_auto_id(f);
self.event = Some(EventBinding {
action: "focus_change".to_string(),
callback_id: id,
});
self
}
pub fn on_key(mut self, callback_id: usize) -> Self {
self.event = Some(EventBinding {
action: "key".to_string(),
callback_id,
});
self
}
pub fn on_key_fn(mut self, f: impl Fn(String) + Send + 'static) -> Self {
let id = crate::event::register_value_with_auto_id(f);
self.event = Some(EventBinding {
action: "key_press".to_string(),
callback_id: id,
});
self
}
}
#[derive(Serialize, Debug)]
pub struct ViewDesc {
#[serde(rename = "type")]
pub view_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checked: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spacing: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orientation: Option<Orientation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ratio: Option<f64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<ViewDesc>,
}
impl View {
pub fn to_desc(&self) -> ViewDesc {
let mut desc = ViewDesc {
view_type: self.view_type().to_string(),
text: None,
title: None,
placeholder: None,
value: None,
min: None,
max: None,
checked: None,
options: None,
selected: None,
spacing: None,
orientation: None,
ratio: None,
children: self.children.iter().map(|c| c.to_desc()).collect(),
};
match &self.kind {
ViewKind::VStack { spacing } | ViewKind::HStack { spacing } => {
desc.spacing = Some(*spacing)
}
ViewKind::Text(s) | ViewKind::Label(s) => desc.text = Some(s.clone()),
ViewKind::Button { title } => desc.title = Some(title.clone()),
ViewKind::TextField { placeholder, value } => {
desc.placeholder = Some(placeholder.clone());
desc.text = Some(value.clone());
}
ViewKind::SecureField { placeholder } => desc.placeholder = Some(placeholder.clone()),
ViewKind::Checkbox { title, checked } => {
desc.title = Some(title.clone());
desc.checked = Some(*checked);
}
ViewKind::Radio { title, selected } => {
desc.title = Some(title.clone());
desc.checked = Some(*selected);
}
ViewKind::Slider { min, max, value } => {
desc.min = Some(*min);
desc.max = Some(*max);
desc.value = Some(*value);
}
ViewKind::Toggle { title, on } => {
desc.title = Some(title.clone());
desc.checked = Some(*on);
}
ViewKind::Dropdown {
title,
options,
selected,
} => {
desc.title = Some(title.clone());
desc.options = Some(options.clone());
desc.selected = Some(*selected);
}
ViewKind::TabView { tabs, selected } => {
desc.options = Some(tabs.clone());
desc.selected = Some(*selected);
}
ViewKind::SplitView { orientation, ratio } => {
desc.orientation = Some(*orientation);
desc.ratio = Some(*ratio);
}
ViewKind::GroupBox { title } => desc.title = Some(title.clone()),
ViewKind::ProgressBar { value, max } => {
desc.value = Some(*value);
desc.max = Some(*max);
}
ViewKind::Image { name } => desc.text = Some(name.clone()),
ViewKind::TextArea { placeholder, value } => {
desc.placeholder = Some(placeholder.clone());
desc.text = Some(value.clone());
}
ViewKind::DatePicker { date: Some(d) } => desc.text = Some(d.clone()),
ViewKind::DatePicker { date: None } => {}
ViewKind::ColorPicker { color: Some(c) } => desc.text = Some(c.clone()),
ViewKind::ColorPicker { color: None } => {}
ViewKind::WebView { url } => desc.text = Some(url.clone()),
ViewKind::TableView { columns, rows: _ } => desc.options = Some(columns.clone()),
ViewKind::Custom { type_name, .. } => desc.title = Some(type_name.clone()),
_ => {}
}
desc
}
pub fn view_type(&self) -> &'static str {
match &self.kind {
ViewKind::VStack { .. } => "VStack",
ViewKind::HStack { .. } => "HStack",
ViewKind::ZStack => "ZStack",
ViewKind::Spacer { .. } => "Spacer",
ViewKind::Text(_) => "Text",
ViewKind::Label(_) => "Label",
ViewKind::Button { .. } => "Button",
ViewKind::TextField { .. } => "TextField",
ViewKind::SecureField { .. } => "SecureField",
ViewKind::Checkbox { .. } => "Checkbox",
ViewKind::Radio { .. } => "Radio",
ViewKind::Slider { .. } => "Slider",
ViewKind::Toggle { .. } => "Toggle",
ViewKind::Dropdown { .. } => "Dropdown",
ViewKind::ScrollView { .. } => "ScrollView",
ViewKind::TabView { .. } => "TabView",
ViewKind::SplitView { .. } => "SplitView",
ViewKind::GroupBox { .. } => "GroupBox",
ViewKind::ProgressBar { .. } => "ProgressBar",
ViewKind::Image { .. } => "Image",
ViewKind::TextArea { .. } => "TextArea",
ViewKind::DatePicker { .. } => "DatePicker",
ViewKind::ColorPicker { .. } => "ColorPicker",
ViewKind::WebView { .. } => "WebView",
ViewKind::TableView { .. } => "TableView",
ViewKind::SegmentedControl { .. } => "SegmentedControl",
ViewKind::ComboBox { .. } => "ComboBox",
ViewKind::SearchField { .. } => "SearchField",
ViewKind::Stepper { .. } => "Stepper",
ViewKind::LevelIndicator { .. } => "LevelIndicator",
ViewKind::PathControl { .. } => "PathControl",
ViewKind::Custom { .. } => "Custom",
}
}
pub fn to_json(&self) -> crate::error::Result<String> {
let desc = self.to_desc();
Ok(serde_json::to_string_pretty(&desc)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_view() {
let v = View::text("Hello");
assert_eq!(v.view_type(), "Text");
assert!(matches!(&v.kind, ViewKind::Text(s) if s == "Hello"));
}
#[test]
fn test_button_view() {
let v = View::button("Click");
assert_eq!(v.view_type(), "Button");
assert!(matches!(&v.kind, ViewKind::Button { title } if title == "Click"));
}
#[test]
fn test_vstack_with_children() {
let v = View::vstack().child(View::text("A")).child(View::text("B"));
assert_eq!(v.children.len(), 2);
}
#[test]
fn test_style_chaining() {
let v = View::button("OK")
.width(100.0)
.height(40.0)
.background("blue")
.bold();
assert_eq!(v.style.width, Some(100.0));
assert_eq!(v.style.height, Some(40.0));
assert_eq!(v.style.background.as_deref(), Some("blue"));
assert!(v.style.font_bold);
}
#[test]
fn test_to_desc_text() {
let v = View::text("hello");
let desc = v.to_desc();
assert_eq!(desc.view_type, "Text");
assert_eq!(desc.text.as_deref(), Some("hello"));
}
#[test]
fn test_to_desc_nested() {
let v = View::vstack().child(View::text("A")).child(View::spacer());
let desc = v.to_desc();
assert_eq!(desc.view_type, "VStack");
assert_eq!(desc.children.len(), 2);
assert_eq!(desc.children[0].view_type, "Text");
assert_eq!(desc.children[1].view_type, "Spacer");
}
#[test]
fn test_to_json() {
let v = View::text("hello");
let json = v.to_json().unwrap();
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["type"], "Text");
assert_eq!(val["text"], "hello");
}
#[test]
fn test_slider() {
let v = View::slider(0.0, 100.0, 50.0);
assert_eq!(v.view_type(), "Slider");
let desc = v.to_desc();
assert_eq!(desc.min, Some(0.0));
assert_eq!(desc.max, Some(100.0));
assert_eq!(desc.value, Some(50.0));
}
#[test]
fn test_dropdown() {
let v = View::dropdown("Pick", vec!["A".into(), "B".into()]);
assert_eq!(v.view_type(), "Dropdown");
let desc = v.to_desc();
assert_eq!(desc.options.as_ref().unwrap().len(), 2);
}
#[test]
fn test_all_view_types() {
assert_eq!(View::vstack().view_type(), "VStack");
assert_eq!(View::hstack().view_type(), "HStack");
assert_eq!(View::zstack().view_type(), "ZStack");
assert_eq!(View::spacer().view_type(), "Spacer");
assert_eq!(View::text("x").view_type(), "Text");
assert_eq!(View::label("x").view_type(), "Label");
assert_eq!(View::button("x").view_type(), "Button");
assert_eq!(View::text_field("x").view_type(), "TextField");
assert_eq!(View::secure_field("x").view_type(), "SecureField");
assert_eq!(View::checkbox("x").view_type(), "Checkbox");
assert_eq!(View::radio("x").view_type(), "Radio");
assert_eq!(View::slider(0.0, 1.0, 0.5).view_type(), "Slider");
assert_eq!(View::toggle("x").view_type(), "Toggle");
assert_eq!(View::dropdown("x", vec![]).view_type(), "Dropdown");
assert_eq!(View::scroll_view().view_type(), "ScrollView");
assert_eq!(View::tab_view(vec!["a", "b"]).view_type(), "TabView");
assert_eq!(View::split_view().view_type(), "SplitView");
assert_eq!(View::group_box("x").view_type(), "GroupBox");
assert_eq!(View::progress_bar(0.5, 1.0).view_type(), "ProgressBar");
assert_eq!(View::image("x").view_type(), "Image");
assert_eq!(View::text_area("x").view_type(), "TextArea");
assert_eq!(View::date_picker().view_type(), "DatePicker");
assert_eq!(View::color_picker().view_type(), "ColorPicker");
assert_eq!(View::web_view("https://example.com").view_type(), "WebView");
assert_eq!(
View::table_view(vec!["A", "B"], vec![]).view_type(),
"TableView"
);
assert_eq!(View::custom("x", vec![]).view_type(), "Custom");
}
#[test]
fn test_event_binding() {
let v = View::button("OK").on_click(42);
assert!(v.event.is_some());
assert_eq!(v.event.as_ref().unwrap().callback_id, 42);
assert_eq!(v.event.as_ref().unwrap().action, "click");
}
#[test]
fn test_add_child_mutable() {
let mut v = View::vstack();
v.add_child(View::text("A"));
v.add_child(View::text("B"));
assert_eq!(v.children.len(), 2);
}
#[test]
fn view_tag_roundtrip() {
let t = ViewTag::new(42);
assert_eq!(t.as_isize(), 42);
let i: isize = t.into();
assert_eq!(i, 42);
assert_eq!(ViewTag::default().as_isize(), 0);
}
#[test]
fn key_modifiers_helpers() {
assert!(KeyModifiers::command().command);
assert!(KeyModifiers::option().option);
assert!(KeyModifiers::shift().shift);
assert!(KeyModifiers::control().control);
let m = KeyModifiers::command_shift();
assert!(m.command && m.shift);
let m2 = KeyModifiers::command_option();
assert!(m2.command && m2.option);
}
#[test]
fn extended_view_types_in_all_view_types() {
assert_eq!(
View::segmented_control(vec!["a".to_string(), "b".to_string()]).view_type(),
"SegmentedControl"
);
assert_eq!(View::combo_box(vec![String::from("x")]).view_type(), "ComboBox");
assert_eq!(
View::search_field("s").view_type(),
"SearchField"
);
assert_eq!(View::stepper(0.0, 10.0, 1.0).view_type(), "Stepper");
assert_eq!(View::level_indicator(0.0, 1.0, 0.5).view_type(), "LevelIndicator");
assert_eq!(View::path_control("/tmp").view_type(), "PathControl");
}
#[test]
fn to_desc_segmented_combo_search_stepper() {
let seg = View::segmented_control(vec!["One".to_string(), "Two".to_string()]);
assert_eq!(seg.to_desc().view_type, "SegmentedControl");
let cb = View::combo_box(vec![String::from("a")]);
assert_eq!(cb.to_desc().view_type, "ComboBox");
let sf = View::search_field("q");
assert_eq!(sf.to_desc().view_type, "SearchField");
let st = View::stepper(0.0, 5.0, 2.0);
assert_eq!(st.to_desc().view_type, "Stepper");
}
#[test]
fn scroll_direction_serialize() {
let v = serde_json::to_string(&ScrollDirection::Vertical).unwrap();
assert!(v.contains("Vertical"));
}
}