// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use std::collections::BTreeMap; use std::path::PathBuf; use std::{collections::HashMap, iter::once, rc::Rc}; use i_slint_compiler::parser::TextRange; use i_slint_compiler::{expression_tree, langtype}; use itertools::Itertools; use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel}; use slint_interpreter::{DiagnosticLevel, PlatformError}; use smol_str::SmolStr; use crate::common::{self, ComponentInformation}; use crate::preview::{self, preview_data, properties, SelectionNotification}; #[cfg(target_arch = "wasm32")] use crate::wasm_prelude::*; mod brushes; pub mod log_messages; pub mod palette; mod property_view; mod recent_colors; pub mod search_model; slint::include_modules!(); pub type PropertyDeclarations = HashMap; pub fn create_ui( to_lsp: &Rc, style: &str, experimental: bool, ) -> Result { #[cfg(all(target_vendor = "apple", not(target_arch = "wasm32")))] crate::preview::connector::native::init_apple_platform()?; let ui = PreviewUi::new()?; // styles: let known_styles = once(&"native") .chain(i_slint_compiler::fileaccess::styles().iter()) .filter(|s| s != &&"qt" || i_slint_backend_selector::HAS_NATIVE_STYLE) .cloned() .sorted() .collect::>(); let style = if known_styles.contains(&style) { style.to_string() } else { known_styles .iter() .find(|x| **x == "native") .or_else(|| known_styles.first()) .map(|s| s.to_string()) .unwrap_or_default() }; let style_model = Rc::new({ let model = VecModel::default(); model.extend(known_styles.iter().map(|s| SharedString::from(*s))); assert!(model.row_count() > 1); model }); let api = ui.global::(); api.set_current_style(style.clone().into()); api.set_experimental(experimental); api.set_known_styles(style_model.into()); api.on_add_new_component(super::add_new_component); api.on_rename_component(super::rename_component); api.on_style_changed(super::change_style); api.on_show_component(super::show_component); let lsp = to_lsp.clone(); api.on_show_document(move |file, line, column| { use lsp_types::{Position, Range}; let pos = Position::new((line as u32).saturating_sub(1), (column as u32).saturating_sub(1)); lsp.ask_editor_to_show_document(&file, Range::new(pos, pos), false).unwrap(); }); api.on_show_document_offset_range(super::show_document_offset_range); api.on_show_preview_for(super::show_preview_for); api.on_reload_preview(super::reload_preview); api.on_unselect(super::element_selection::unselect_element); api.on_reselect(super::element_selection::reselect_element); api.on_select_at(super::element_selection::select_element_at); api.on_selection_stack_at(super::element_selection::selection_stack_at); api.on_filter_sort_selection_stack(super::element_selection::filter_sort_selection_stack); api.on_find_selected_selection_stack_frame(|stack| { stack.iter().find(|frame| frame.is_selected).unwrap_or_default() }); api.on_select_element(|path, offset, x, y| { super::element_selection::select_element_at_source_code_position( PathBuf::from(path.to_string()), preview::TextSize::from(offset as u32), Some(i_slint_core::lengths::LogicalPoint::new(x, y)), SelectionNotification::Now, ); }); api.on_select_behind(super::element_selection::select_element_behind); api.on_highlight_positions(super::element_selection::highlight_positions); let lsp = to_lsp.clone(); api.on_can_drop(super::can_drop_component); api.on_drop(move |component_index: i32, x: f32, y: f32| { lsp.send_telemetry(&mut [( "type".to_string(), serde_json::to_value("component_dropped").unwrap(), )]) .unwrap(); super::drop_component(component_index, x, y) }); api.on_selected_element_resize(super::resize_selected_element); api.on_selected_element_can_move_to(super::can_move_selected_element); api.on_selected_element_move(super::move_selected_element); api.on_selected_element_delete(super::delete_selected_element); api.on_test_code_binding(super::test_code_binding); api.on_set_code_binding(super::set_code_binding); api.on_set_color_binding(super::set_color_binding); api.on_set_element_id(super::set_element_id); api.on_property_declaration_ranges(super::property_declaration_ranges); api.on_get_property_value(get_property_value); api.on_get_property_value_table(get_property_value_table); api.on_set_property_value_table(set_property_value_table); api.on_insert_row_into_value_table(insert_row_into_value_table); api.on_remove_row_from_value_table(remove_row_from_value_table); let lsp = to_lsp.clone(); api.on_set_json_preview_data(move |container, property_name, json_string, send_telemetry| { if send_telemetry { lsp.send_telemetry(&mut [( "type".to_string(), serde_json::to_value("data_json_changed").unwrap(), )]) .unwrap(); } set_json_preview_data(container, property_name, json_string) }); api.on_string_to_code(string_to_code); brushes::setup(&ui); log_messages::setup(&ui); palette::setup(&ui); recent_colors::setup(&ui); super::outline::setup(&ui); super::undo_redo::setup(&ui); #[cfg(target_vendor = "apple")] api.set_control_key_name("command".into()); #[cfg(target_family = "wasm")] if web_sys::window() .and_then(|window| window.navigator().platform().ok()) .map_or(false, |platform| platform.to_ascii_lowercase().contains("mac")) { api.set_control_key_name("command".into()); } Ok(ui) } fn extract_definition_location(ci: &ComponentInformation) -> (SharedString, SharedString) { let Some(url) = ci.defined_at.as_ref().map(|da| da.url()) else { return (Default::default(), Default::default()); }; let path = url.to_file_path().unwrap_or_default(); let file_name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); (url.to_string().into(), file_name.into()) } pub fn ui_set_uses_widgets(ui: &PreviewUi, uses_widgets: bool) { let api = ui.global::(); api.set_uses_widgets(uses_widgets); } pub fn set_diagnostics(ui: &PreviewUi, diagnostics: &[slint_interpreter::Diagnostic]) { let summary = diagnostics .iter() .inspect(|d| { let location = d.source_file().map(|p| { let (line, column) = d.line_column(); (p.to_string_lossy().to_string().into(), line, column) }); let level = match d.level() { DiagnosticLevel::Error => LogMessageLevel::Error, DiagnosticLevel::Warning => LogMessageLevel::Warning, _ => LogMessageLevel::Debug, }; log_messages::append_log_message(ui, level, location, d.message()); }) .fold(DiagnosticSummary::NothingDetected, |acc, d| { match (acc, d.level()) { (_, DiagnosticLevel::Error) => DiagnosticSummary::Errors, (DiagnosticSummary::Errors, DiagnosticLevel::Warning) => DiagnosticSummary::Errors, (_, DiagnosticLevel::Warning) => DiagnosticSummary::Warnings, // DiagnosticLevel is non-exhaustive: (acc, _) => acc, } }); let api = ui.global::(); api.set_diagnostic_summary(summary); } pub fn ui_set_known_components( ui: &PreviewUi, known_components: &[crate::common::ComponentInformation], current_component_index: usize, ) { let mut builtins_map: HashMap> = Default::default(); let mut std_widgets_map: HashMap> = Default::default(); let mut path_map: HashMap)> = Default::default(); let mut library_map: HashMap> = Default::default(); let mut longest_path_prefix = PathBuf::new(); for (idx, ci) in known_components.iter().enumerate() { if ci.is_global { continue; } let (url, pretty_location) = extract_definition_location(ci); let item = ComponentItem { name: ci.name.clone().into(), index: idx.try_into().unwrap(), defined_at: url.clone(), pretty_location, is_user_defined: !(ci.is_builtin || ci.is_std_widget), is_currently_shown: idx == current_component_index, is_exported: ci.is_exported, }; if let Some(position) = &ci.defined_at { if let Some(library) = position.url().path().strip_prefix("/@") { library_map.entry(format!("@{library}")).or_default().push(item); } else { let path = i_slint_compiler::pathutils::clean_path( &(position.url().to_file_path().unwrap_or_default()), ); if path != PathBuf::new() { if longest_path_prefix == PathBuf::new() { longest_path_prefix = path.clone(); } else { longest_path_prefix = std::iter::zip(longest_path_prefix.components(), path.components()) .take_while(|(l, p)| l == p) .map(|(l, _)| l) .collect(); } } path_map.entry(path).or_insert((url, Vec::new())).1.push(item); } } else if ci.is_builtin { builtins_map.entry(ci.category.clone()).or_default().push(item); } else { std_widgets_map.entry(ci.category.clone()).or_default().push(item); } } type ComponentModel = search_model::SearchModel; fn make_component_model(vec: Vec) -> ComponentModel { ComponentModel::new(VecModel::from(vec), |i, search_str| { search_model::contains(i.name.as_str(), search_str) }) } fn sort_subset(mut input: HashMap>) -> Vec { let mut output = input .drain() .map(|(k, mut v)| { v.sort_by_key(|i| i.name.clone()); let model = Rc::new(make_component_model(v)); ComponentListItem { category: k.into(), file_url: SharedString::new(), components: model.into(), } }) .collect::>(); output.sort_by_key(|k| k.category.clone()); output } let builtin_components = sort_subset(builtins_map); let std_widgets_components = sort_subset(std_widgets_map); let library_components = sort_subset(library_map); let mut file_components = path_map .drain() .map(|(p, (file_url, mut v))| { v.sort_by_key(|i| i.name.clone()); let model = Rc::new(make_component_model(v)); let name = if p == longest_path_prefix { p.file_name().unwrap_or_default().to_string_lossy().to_string() } else { p.strip_prefix(&longest_path_prefix).unwrap_or(&p).to_string_lossy().to_string() }; ComponentListItem { category: name.into(), file_url, components: model.into() } }) .collect::>(); file_components.sort_by_key(|k| PathBuf::from(k.category.to_string())); let mut all_components = Vec::with_capacity( builtin_components.len() + library_components.len() + file_components.len(), ); all_components.extend_from_slice(&builtin_components[..]); all_components.extend_from_slice(&std_widgets_components[..]); all_components.extend_from_slice(&library_components[..]); all_components.extend_from_slice(&file_components[..]); let result = Rc::new(search_model::SearchModel::new( VecModel::from(all_components), |category, search_str| { let mut yes = search_str.is_empty(); if let Some(sub_filter) = category.components.as_any().downcast_ref::() { sub_filter.set_search_text(search_str.clone()); yes = yes || sub_filter.row_count() > 0; } yes }, )); let api = ui.global::(); let old_search_text = api .get_known_components() .as_any() .downcast_ref::>() .map(|x| x.search_text()) .filter(|x| !x.is_empty()); if let Some(search_text) = old_search_text { result.set_search_text(search_text.clone()); } api.set_known_components(result.clone().into()); api.on_library_search(move |term| { result.set_search_text(term.into()); }); } fn to_ui_range(r: TextRange) -> Option { Some(Range { start: i32::try_from(u32::from(r.start())).ok()?, end: i32::try_from(u32::from(r.end())).ok()?, }) } fn convert_simple_string(input: SharedString) -> SharedString { slint::format!("\"{}\"", str::escape_debug(input.as_ref())) } fn string_to_code( input: SharedString, is_translatable: bool, tr_context: SharedString, tr_plural: SharedString, tr_plural_expression: SharedString, ) -> SharedString { let input = convert_simple_string(input); if !is_translatable { input } else { let context = if tr_context.is_empty() { SharedString::new() } else { slint::format!("{} => ", convert_simple_string(tr_context)) }; let plural = if tr_plural.is_empty() { SharedString::new() } else { slint::format!(" | {} % {}", convert_simple_string(tr_plural), tr_plural_expression) }; slint::format!("@tr({context}{input}{plural})") } } fn unit_model(units: &[expression_tree::Unit]) -> ModelRc { Rc::new(VecModel::from( units.iter().map(|u| u.to_string().into()).collect::>(), )) .into() } fn is_equal_value(c: &PropertyValue, n: &PropertyValue) -> bool { c.code == n.code } fn is_equal_property(c: &PropertyInformation, n: &PropertyInformation) -> bool { c.name == n.name && c.type_name == n.type_name && is_equal_value(&c.value, &n.value) } fn is_equal_element(c: &ElementInformation, n: &ElementInformation) -> bool { c.id == n.id && c.type_name == n.type_name && c.source_uri == n.source_uri && c.offset == n.offset } pub type PropertyGroupModel = ModelRc; fn update_grouped_properties( cvg: &VecModel, nvg: &VecModel, ) { enum Op { Insert((usize, usize)), Copy((usize, usize)), PushBack(usize), Remove(usize), } let mut to_do = Vec::new(); let mut c_it = cvg.iter(); let mut n_it = nvg.iter(); let mut cp = c_it.next(); let mut np = n_it.next(); let mut c_index = 0_usize; let mut n_index = 0_usize; loop { match (cp.as_ref(), np.as_ref()) { (None, None) => break, (Some(_), None) => { to_do.push(Op::Remove(c_index)); cp = c_it.next(); } (Some(c), Some(n)) => match c.name.cmp(&n.name) { std::cmp::Ordering::Less => { to_do.push(Op::Remove(c_index)); cp = c_it.next(); } std::cmp::Ordering::Equal => { if !is_equal_property(c, n) { to_do.push(Op::Copy((c_index, n_index))); } c_index += 1; n_index += 1; cp = c_it.next(); np = n_it.next(); } std::cmp::Ordering::Greater => { to_do.push(Op::Insert((c_index, n_index))); c_index += 1; n_index += 1; np = n_it.next(); } }, (None, Some(_)) => { to_do.push(Op::PushBack(n_index)); n_index += 1; np = n_it.next(); } } } for op in &to_do { match op { Op::Insert((c, n)) => { cvg.insert(*c, nvg.row_data(*n).unwrap()); } Op::Copy((c, n)) => { cvg.set_row_data(*c, nvg.row_data(*n).unwrap()); } Op::PushBack(n) => { cvg.push(nvg.row_data(*n).unwrap()); } Op::Remove(c) => { cvg.remove(*c); } } } } fn get_value + std::default::Default>( v: &Option, ) -> T { v.clone().and_then(|v| v.try_into().ok()).unwrap_or_default() } fn get_code(v: &Option) -> SharedString { v.as_ref() .and_then(|v| slint_interpreter::json::value_to_json(v).ok()) .and_then(|j| serde_json::to_string_pretty(&j).ok()) .unwrap_or_default() .into() } #[derive(Default, Debug)] struct ValueMapping { name_prefix: SharedString, is_too_complex: bool, is_array: bool, headers: Vec, current_values: Vec, array_values: Vec>, code_value: PropertyValue, } fn map_value_and_type( ty: &langtype::Type, value: &Option, mapping: &mut ValueMapping, ) { fn map_color( mapping: &mut ValueMapping, color: slint::Color, kind: PropertyValueKind, code: SharedString, ) { let color_string = brushes::color_to_string(color); mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { value_kind: kind, kind, display_string: color_string.clone(), brush_kind: BrushKind::Solid, value_brush: slint::Brush::SolidColor(color), gradient_stops: Rc::new(VecModel::from(vec![GradientStop { color, position: 0.5 }])) .into(), code, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } use i_slint_compiler::expression_tree::Unit; use langtype::Type; match ty { Type::Float32 => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: get_value::(value).to_shared_string(), kind: PropertyValueKind::Float, value_float: get_value::(value), code: get_code(value), accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::Int32 => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: get_value::(value).to_shared_string(), kind: PropertyValueKind::Integer, value_kind: PropertyValueKind::Integer, value_int: get_value::(value), code: get_code(value), accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::Duration => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("{}{}", get_value::(value), Unit::Ms), kind: PropertyValueKind::Float, value_kind: PropertyValueKind::Float, value_float: get_value::(value), visual_items: unit_model(&[Unit::S, Unit::Ms]), value_int: 1, code: get_code(value), default_selection: 1, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::PhysicalLength => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("{}{}", get_value::(value), Unit::Phx), kind: PropertyValueKind::Float, value_kind: PropertyValueKind::Float, value_float: get_value::(value), visual_items: unit_model(&[ Unit::Px, Unit::Cm, Unit::Mm, Unit::In, Unit::Pt, Unit::Phx, Unit::Rem, ]), value_int: 5, code: get_code(value), default_selection: 5, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::LogicalLength => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("{}{}", get_value::(value), Unit::Px), kind: PropertyValueKind::Float, value_kind: PropertyValueKind::Float, value_float: get_value::(value), visual_items: unit_model(&[ Unit::Px, Unit::Cm, Unit::Mm, Unit::In, Unit::Pt, Unit::Phx, Unit::Rem, ]), value_int: 0, code: get_code(value), default_selection: 0, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::Rem => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("{}{}", get_value::(value), Unit::Rem), kind: PropertyValueKind::Float, value_kind: PropertyValueKind::Float, value_float: get_value::(value), visual_items: unit_model(&[ Unit::Px, Unit::Cm, Unit::Mm, Unit::In, Unit::Pt, Unit::Phx, Unit::Rem, ]), value_int: 6, code: get_code(value), default_selection: 6, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::Angle => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("{}{}", get_value::(value), Unit::Deg), kind: PropertyValueKind::Float, value_kind: PropertyValueKind::Float, value_float: get_value::(value), visual_items: unit_model(&[Unit::Deg, Unit::Grad, Unit::Turn, Unit::Rad]), value_int: 0, code: get_code(value), default_selection: 0, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::Percent => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("{}{}", get_value::(value), Unit::Percent), kind: PropertyValueKind::Float, value_kind: PropertyValueKind::Float, value_float: get_value::(value), visual_items: unit_model(&[Unit::Percent]), value_int: 0, code: get_code(value), default_selection: 0, accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::String => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!("\"{}\"", get_value::(value)), kind: PropertyValueKind::String, value_kind: PropertyValueKind::String, value_string: get_value::(value), code: get_code(value), accessor_path: mapping.name_prefix.clone(), ..Default::default() }); } Type::Color => { map_color( mapping, get_value::(value), PropertyValueKind::Color, get_code(value), ); } Type::Brush => { let brush = get_value::(value); match brush { slint::Brush::SolidColor(c) => { map_color(mapping, c, PropertyValueKind::Brush, get_code(value)) } slint::Brush::LinearGradient(lg) => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: SharedString::from("Linear Gradient"), kind: PropertyValueKind::Brush, value_kind: PropertyValueKind::Brush, brush_kind: BrushKind::Linear, value_float: lg.angle(), value_brush: slint::Brush::LinearGradient(lg.clone()), gradient_stops: Rc::new(VecModel::from( lg.stops() .map(|gs| GradientStop { color: gs.color, position: gs.position }) .collect::>(), )) .into(), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } slint::Brush::RadialGradient(rg) => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: SharedString::from("Radial Gradient"), kind: PropertyValueKind::Brush, value_kind: PropertyValueKind::Brush, brush_kind: BrushKind::Radial, value_brush: slint::Brush::RadialGradient(rg.clone()), gradient_stops: Rc::new(VecModel::from( rg.stops() .map(|gs| GradientStop { color: gs.color, position: gs.position }) .collect::>(), )) .into(), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } slint::Brush::ConicGradient(cg) => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: SharedString::from("Conic Gradient"), kind: PropertyValueKind::Brush, value_kind: PropertyValueKind::Brush, brush_kind: BrushKind::Conic, value_brush: slint::Brush::ConicGradient(cg.clone()), gradient_stops: Rc::new(VecModel::from( cg.stops() .map(|gs| GradientStop { color: gs.color, position: gs.position }) .collect::>(), )) .into(), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } _ => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: SharedString::from("Unknown Brush"), kind: PropertyValueKind::Code, value_kind: PropertyValueKind::Code, value_string: SharedString::from("???"), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } } } Type::Bool => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: get_value::(value).to_shared_string(), kind: PropertyValueKind::Boolean, value_kind: PropertyValueKind::Boolean, value_bool: get_value::(value), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } Type::Enumeration(enumeration) => { let selected_value = match &value { Some(slint_interpreter::Value::EnumerationValue(_, k)) => enumeration .values .iter() .position(|v| v.as_str() == k) .unwrap_or(enumeration.default_value), _ => enumeration.default_value, }; mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: slint::format!( "{}.{}", enumeration.name, enumeration.values[selected_value] ), kind: PropertyValueKind::Enum, value_kind: PropertyValueKind::Enum, value_string: enumeration.name.as_str().into(), default_selection: i32::try_from(enumeration.default_value).unwrap_or_default(), value_int: i32::try_from(selected_value).unwrap_or_default(), visual_items: Rc::new(VecModel::from( enumeration .values .iter() .map(|s| SharedString::from(s.as_str())) .collect::>(), )) .into(), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } Type::Array(array_ty) => { mapping.is_array = true; let model = get_value::>(value); for (idx, sub_value) in model.iter().enumerate() { let mut sub_mapping = ValueMapping { name_prefix: mapping.name_prefix.clone(), ..Default::default() }; map_value_and_type(array_ty, &Some(sub_value), &mut sub_mapping); let sub_mapping_too_complex = sub_mapping.is_array || sub_mapping.is_too_complex; mapping.is_too_complex = mapping.is_too_complex || sub_mapping_too_complex; if sub_mapping_too_complex { if idx == 0 { mapping.headers.push(mapping.name_prefix.clone()); } mapping.array_values.push(vec![std::mem::take(&mut sub_mapping.code_value)]); } else { if idx == 0 { mapping.headers.extend_from_slice(&sub_mapping.headers); } mapping.array_values.push(std::mem::take(&mut sub_mapping.array_values[0])); } } } Type::Struct(s) => { mapping.is_array = false; let struct_data = get_value::(value); for (field, field_ty) in s.fields.iter() { let field = field.to_string(); let mut sub_mapping = ValueMapping::default(); let header_name = if mapping.name_prefix.is_empty() { field.clone().into() } else { slint::format!("{}.{field}", mapping.name_prefix) }; sub_mapping.name_prefix = header_name.clone(); map_value_and_type( field_ty, &struct_data.get_field(&field).cloned(), &mut sub_mapping, ); let sub_mapping_too_complex = sub_mapping.is_array || sub_mapping.is_too_complex; mapping.is_too_complex = mapping.is_too_complex || sub_mapping_too_complex; if sub_mapping_too_complex { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(std::mem::take(&mut sub_mapping.code_value)); } else { mapping.headers.extend_from_slice(&sub_mapping.headers); mapping.current_values.extend_from_slice(&sub_mapping.array_values[0]); } } } Type::Image | Type::Model | Type::PathData | Type::Easing | Type::UnitProduct(_) => { mapping.headers.push(mapping.name_prefix.clone()); mapping.is_too_complex = true; } _ => { mapping.headers.push(mapping.name_prefix.clone()); mapping.current_values.push(PropertyValue { display_string: "Unsupported type".into(), kind: PropertyValueKind::Code, value_kind: PropertyValueKind::Code, value_string: "???".into(), accessor_path: mapping.name_prefix.clone(), code: get_code(value), ..Default::default() }); } } if mapping.array_values.is_empty() { mapping.array_values = vec![std::mem::take(&mut mapping.current_values)]; } mapping.code_value = PropertyValue { kind: PropertyValueKind::Code, value_kind: PropertyValueKind::Code, code: get_code(value), ..Default::default() }; } pub fn map_value_and_type_to_property_value( ty: &langtype::Type, value: &Option, name_prefix: &str, ) -> PropertyValue { let mut mapping = ValueMapping { name_prefix: SharedString::from(name_prefix), ..Default::default() }; map_value_and_type(ty, value, &mut mapping); if mapping.is_too_complex || mapping.array_values.len() != 1 || mapping.array_values[0].len() != 1 { mapping.code_value } else { mapping .array_values .first() .and_then(|av| av.first()) .cloned() .unwrap_or_else(|| mapping.code_value.clone()) } } fn map_preview_data_to_property_value(preview_data: &preview_data::PreviewData) -> PropertyValue { map_value_and_type_to_property_value(&preview_data.ty, &preview_data.value, "") } fn map_preview_data_property( key: &preview_data::PreviewDataKey, value: &preview_data::PreviewData, ) -> Option { if !value.is_property() { return None; }; let has_getter = value.has_getter(); let has_setter = value.has_setter(); let mut mapping = ValueMapping::default(); map_value_and_type(&value.ty, &value.value, &mut mapping); let is_array = mapping.array_values.len() != 1 || mapping.array_values[0].len() != 1; let is_too_complex = mapping.is_too_complex; Some(PreviewData { name: SharedString::from(&key.property_name), has_getter, has_setter, kind: match (is_array, is_too_complex) { (false, false) => PreviewDataKind::Value, (true, false) => PreviewDataKind::Table, _ => PreviewDataKind::Json, }, }) } pub fn ui_set_preview_data( ui: &PreviewUi, preview_data: preview_data::PreviewDataMap, previewed_component: Option, ) { fn create_container( container_name: String, it: &mut dyn Iterator, ) -> Option { let (id, props) = it.filter_map(|(k, v)| Some((k, map_preview_data_property(k, v)?))).fold( (None, vec![]), move |mut acc, (key, value)| { acc.0 = Some(acc.0.unwrap_or_else(|| key.container.clone())); acc.1.push(value); acc }, ); Some(PropertyContainer { container_name: container_name.into(), container_id: id?.to_string().into(), properties: Rc::new(VecModel::from(props)).into(), }) } let mut result: Vec = vec![]; if let Some(c) = create_container( previewed_component.unwrap_or_else(|| "
".to_string()), &mut preview_data .iter() .filter(|(k, _)| k.container == preview_data::PropertyContainer::Main), ) { result.push(c); } for (k, mut chunk) in &preview_data .iter() .filter(|(k, _)| k.container != preview_data::PropertyContainer::Main) .chunk_by(|(k, _)| k.container.clone()) { if let Some(c) = create_container(k.to_string(), &mut chunk) { result.push(c); } } let api = ui.global::(); api.set_preview_data(Rc::new(VecModel::from(result)).into()); } fn to_property_container(container: SharedString) -> preview_data::PropertyContainer { if container.is_empty() || container == "
" { preview_data::PropertyContainer::Main } else { preview_data::PropertyContainer::Global(container.to_string()) } } fn get_property_value(container: SharedString, property_name: SharedString) -> PropertyValue { preview::component_instance() .and_then(|component_instance| { preview_data::get_preview_data( &component_instance, &to_property_container(container), property_name.as_str(), ) }) .map(|pd| map_preview_data_to_property_value(&pd)) .unwrap_or_default() } fn map_preview_data_to_property_value_table( preview_data: &preview_data::PreviewData, ) -> (bool, Vec, Vec>) { let mut mapping = ValueMapping::default(); map_value_and_type(&preview_data.ty, &preview_data.value, &mut mapping); let is_array = mapping.is_array; let headers = std::mem::take(&mut mapping.headers); let values = std::mem::take(&mut mapping.array_values); (is_array, headers, values) } fn get_property_value_table( container: SharedString, property_name: SharedString, ) -> PropertyValueTable { let (is_array, headers, mut values) = preview::component_instance() .and_then(|component_instance| { preview_data::get_preview_data( &component_instance, &to_property_container(container), property_name.as_str(), ) }) .map(|pd| map_preview_data_to_property_value_table(&pd)) .unwrap_or_else(|| (false, Default::default(), Default::default())); let headers = Rc::new(VecModel::from(headers)).into(); let values = Rc::new(VecModel::from( values.drain(..).map(|cv| Rc::new(VecModel::from(cv)).into()).collect::>(), )) .into(); PropertyValueTable { is_array, headers, values } } fn table_to_array(table: ModelRc>) -> Option { let mut result = "[\n".to_string(); for (row_number, row) in table.iter().enumerate() { if row_number != 0 { result += ",\n"; } result += &table_row_to_struct(row, 1)?; } result += "\n]"; Some(result) } fn table_row_to_struct(row: ModelRc, indent_level: usize) -> Option { enum NodeKind { Leaf(String), Inner(BTreeMap), } if row.row_count() == 1 { if let Some(v) = row.row_data(0) { if v.accessor_path.is_empty() { // bare value! return Some(format!("{}{}", " ".repeat(indent_level), v.code)); } } } fn structurize(row: ModelRc) -> Option> { let mut result = BTreeMap::default(); fn insert( map: &mut BTreeMap, accessor_path: &[&str], value: String, ) -> Option<()> { match accessor_path.len() { 0 => None, 1 => { let prev = map .insert(accessor_path.first().unwrap().to_string(), NodeKind::Leaf(value)); prev.is_none().then_some(()) } _ => { let n = map .entry(accessor_path.first().unwrap().to_string()) .or_insert_with(|| NodeKind::Inner(BTreeMap::default())); match n { NodeKind::Leaf(_) => None, NodeKind::Inner(m) => insert(m, &accessor_path[1..], value), } } } } for col in row.iter() { let ap = col.accessor_path.split('.').collect::>(); let value = if col.was_edited { col.edited_value.clone() } else { col.code.clone() }; insert(&mut result, &ap[..], value.to_string())?; } Some(result) } let structure = structurize(row)?; fn structure_to_string( structure: &BTreeMap, indent_level: usize, prefix: &str, ) -> Option { let indent_step = " "; let mut result = format!("{}{prefix}{{\n", indent_step.repeat(indent_level)); let last_index = structure.len() - 1; for (index, (k, v)) in structure.iter().enumerate() { let comma = if index == last_index { "" } else { "," }; match v { NodeKind::Leaf(v) => { result += &format!("{}\"{k}\": {v}{comma}\n", indent_step.repeat(indent_level + 1)) } NodeKind::Inner(m) => { result += &structure_to_string(m, indent_level + 1, &format!("\"{k}\": "))?; result += &format!("{comma}\n"); } } } result += &format!("{}}}", indent_step.repeat(indent_level)); Some(result) } structure_to_string(&structure, indent_level, "") } fn set_property_value_table( container: SharedString, property_name: SharedString, table: ModelRc>, is_array: bool, ) -> SharedString { let json_string = if is_array { table_to_array(table) } else { if table.row_count() != 1 { // A struct must have exactly one row! return "Malformed table".into(); } table_row_to_struct(table.row_data(0).unwrap(), 0) }; let Some(json_string) = json_string else { return "Could not process input values".into(); }; set_json_preview_data(container, property_name, json_string.into()) } fn default_property_value(source: &PropertyValue) -> PropertyValue { let mut pv = PropertyValue { kind: source.kind, accessor_path: source.accessor_path.clone(), ..Default::default() }; match source.kind { PropertyValueKind::Boolean => { pv.display_string = "false".into(); pv.code = "false".into(); } PropertyValueKind::Brush => { pv.display_string = "#00000000".into(); pv.brush_kind = BrushKind::Solid; pv.value_brush = slint::Color::default().into(); pv.code = "#00000000".into(); } PropertyValueKind::Code => { pv.display_string = "Code".into(); } PropertyValueKind::Color => { pv.display_string = "#00000000".into(); pv.brush_kind = BrushKind::Solid; pv.value_brush = slint::Color::default().into(); pv.code = "#00000000".into(); } PropertyValueKind::Enum => { let enum_selection: SharedString = format!( "{}.{}", source.value_string, source .visual_items .row_data(source.default_selection.try_into().unwrap_or_default()) .unwrap_or_default(), ) .into(); pv.display_string = enum_selection.clone(); pv.value_int = source.default_selection; pv.value_string = source.value_string.clone(); pv.default_selection = source.default_selection; pv.visual_items = source.visual_items.clone(); pv.code = enum_selection; } PropertyValueKind::Float => { pv.display_string = "0.0".into(); pv.code = "0.0".into(); } PropertyValueKind::Integer => { pv.display_string = "0".into(); pv.code = "0".into(); } PropertyValueKind::String => { pv.display_string = "".into(); pv.code = "\"\"".into(); } } pv } fn insert_row_into_value_table(table: PropertyValueTable, insert_before: i32) { if !table.is_array { return; } let model = table.values.clone(); let insert_before = (insert_before as usize).clamp(0, model.row_count()); let Some(vec_model) = model.as_any().downcast_ref::>>() else { return; }; let row_data = { let mut result = vec![]; if let Some(row) = vec_model.row_data(0) { result = row.iter().map(|pv| default_property_value(&pv)).collect::>(); } result }; let row_model = Rc::new(VecModel::from(row_data)); if vec_model.row_count() == insert_before { vec_model.push(row_model.into()); } else { vec_model.insert(insert_before, row_model.into()); } } fn remove_row_from_value_table(table: PropertyValueTable, to_remove: i32) { if to_remove < 0 || !table.is_array { return; } let to_remove = to_remove as usize; let model = table.values.clone(); let Some(vec_model) = model.as_any().downcast_ref::>>() else { return; }; if to_remove < vec_model.row_count() { vec_model.remove(to_remove); } } fn set_json_preview_data( container: SharedString, property_name: SharedString, json_string: SharedString, ) -> SharedString { let property_name = (!property_name.is_empty()).then_some(property_name.to_string()); let json = match serde_json::from_str::(json_string.as_ref()) { Ok(j) => j, Err(e) => { return SharedString::from(format!("Input is not valid JSON: {e}")); } }; if property_name.is_none() && !json.is_object() { return SharedString::from("Input for Slint Element is not a JSON object"); } if let Some(ci) = preview::component_instance() { match preview_data::set_json_preview_data( &ci, to_property_container(container), property_name, json, ) { Ok(_) => SharedString::new(), Err(v) => v.first().cloned().unwrap_or_default().into(), } } else { SharedString::from("No preview loaded") } } fn update_properties( current_model: PropertyGroupModel, next_model: PropertyGroupModel, ) -> PropertyGroupModel { if current_model.row_count() != next_model.row_count() { return next_model; } for (c, n) in std::iter::zip(current_model.iter(), next_model.iter()) { debug_assert_eq!(c.group_name, n.group_name); fn extract_inner_model<'a>(m: &'a PropertyGroup) -> &'a VecModel { m.properties .as_any() .downcast_ref::>() .unwrap() .source_model() .as_any() .downcast_ref::>() .unwrap() } let cvg = extract_inner_model(&c); let nvg = extract_inner_model(&n); update_grouped_properties(cvg, nvg); } current_model } pub fn ui_set_properties( ui: &PreviewUi, document_cache: &common::DocumentCache, properties: Option, ) -> PropertyDeclarations { let win = i_slint_core::window::WindowInner::from_pub(ui.window()).window_adapter(); let Some((next_element, declarations, next_model)) = property_view::map_properties_to_ui(document_cache, properties, &win) else { let api = ui.global::(); api.set_properties(ModelRc::default()); api.set_current_element(ElementInformation::default()); return Default::default(); }; let api = ui.global::(); let current_model = api.get_properties(); let element = api.get_current_element(); if !is_equal_element(&element, &next_element) { let old_search_text = current_model .as_any() .downcast_ref::>() .map(|x| x.search_text()) .filter(|x| !x.is_empty()); if let Some(search_text) = old_search_text { next_model.set_search_text(search_text.clone()); } api.set_properties(next_model.clone().into()); api.on_properties_search(move |search_text| { next_model.set_search_text(search_text); }); } else { update_properties(current_model, next_model.into()); } api.set_current_element(next_element); declarations } #[cfg(test)] mod tests { use crate::preview::preview_data; use slint::{Model, SharedString, ToSharedString, VecModel}; use super::{PropertyInformation, PropertyValue, PropertyValueKind}; fn create_test_property(name: &str, value: &str) -> PropertyInformation { PropertyInformation { name: name.into(), display_priority: 1000, type_name: "Sometype".into(), value: PropertyValue { kind: PropertyValueKind::String, value_string: value.into(), code: value.into(), ..Default::default() }, } } #[test] fn test_property_date_update() { let current = VecModel::from(vec![ create_test_property("aaa", "AAA"), create_test_property("bbb", "BBB"), create_test_property("ccc", "CCC"), create_test_property("ddd", "DDD"), create_test_property("eee", "EEE"), ]); let next = VecModel::from(vec![ create_test_property("aaa", "AAA"), create_test_property("aab", "AAB"), create_test_property("abb", "ABB"), create_test_property("bbb", "BBBX"), create_test_property("ddd", "DDD"), ]); super::update_grouped_properties(¤t, &next); let mut it = current.iter(); let t = it.next().unwrap(); assert_eq!(t.name.as_str(), "aaa"); assert_eq!(t.value.code.as_str(), "AAA"); let t = it.next().unwrap(); assert_eq!(t.name.as_str(), "aab"); assert_eq!(t.value.code.as_str(), "AAB"); let t = it.next().unwrap(); assert_eq!(t.name.as_str(), "abb"); assert_eq!(t.value.code.as_str(), "ABB"); let t = it.next().unwrap(); assert_eq!(t.name.as_str(), "bbb"); assert_eq!(t.value.code.as_str(), "BBBX"); let t = it.next().unwrap(); assert_eq!(t.name.as_str(), "ddd"); assert_eq!(t.value.code.as_str(), "DDD"); assert!(it.next().is_none()); } fn generate_preview_data( visibility: &str, type_def: &str, type_name: &str, code: &str, ) -> (crate::preview::preview_data::PreviewDataKey, crate::preview::preview_data::PreviewData) { let component_instance = crate::preview::test::interpret_test( "fluent", &format!( r#" {type_def} export component Tester {{ {visibility} property <{type_name}> test: {code}; }} "# ), ); let mut data = preview_data::query_preview_data_properties_and_callbacks(&component_instance); assert_eq!(data.len(), 1); data.pop_first().unwrap() } fn compare_pv(r: &super::PropertyValue, e: &PropertyValue) { eprintln!("Received: {r:?}"); eprintln!("Expected: {e:?}"); assert_eq!(r.value_bool, e.value_bool); assert_eq!(r.is_translatable, e.is_translatable); assert_eq!(r.value_brush, e.value_brush); assert_eq!(r.value_float, e.value_float); assert_eq!(r.value_int, e.value_int); assert_eq!(r.default_selection, e.default_selection); assert_eq!(r.value_string, e.value_string); assert_eq!(r.tr_context, e.tr_context); assert_eq!(r.tr_plural, e.tr_plural); assert_eq!(r.tr_plural_expression, e.tr_plural_expression); assert_eq!(r.code, e.code); assert_eq!(r.visual_items.row_count(), e.visual_items.row_count()); for (r, e) in r.visual_items.iter().zip(e.visual_items.iter()) { assert_eq!(r, e); } } fn validate_rp_impl( visibility: &str, type_def: &str, type_name: &str, code: &str, expected_data: super::PreviewData, ) -> (preview_data::PreviewDataKey, preview_data::PreviewData) { let (key, value) = generate_preview_data(visibility, type_def, type_name, code); let rp = super::map_preview_data_property(&key, &value).unwrap(); eprintln!("*** Validating PreviewData: Received: {rp:?}"); eprintln!("*** Validating PreviewData: Expected: {expected_data:?}"); assert_eq!(rp.name, expected_data.name); assert_eq!(rp.has_getter, expected_data.has_getter); assert_eq!(rp.has_setter, expected_data.has_setter); assert_eq!(rp.kind, expected_data.kind); eprintln!("*** PreviewData is as expected..."); (key, value) } fn validate_rp( visibility: &str, type_def: &str, type_name: &str, code: &str, expected_data: super::PreviewData, expected_value: super::PropertyValue, ) { let (_, value) = validate_rp_impl(visibility, type_def, type_name, code, expected_data); let pv = super::map_preview_data_to_property_value(&value); compare_pv(&pv, &expected_value); let (is_array, headers, values) = super::map_preview_data_to_property_value_table(&value); assert!(!is_array); assert!(headers.len() == 1); assert!(headers[0].is_empty()); assert_eq!(values.len(), 1); assert_eq!(values.first().unwrap().len(), 1); } fn validate_table_rp( visibility: &str, type_def: &str, type_name: &str, code: &str, expected_data: super::PreviewData, expected_code: &str, expected_is_array: bool, expected_headers: Vec, expected_table: Vec>, ) { let (_, value) = validate_rp_impl(visibility, type_def, type_name, code, expected_data); let pv = super::map_preview_data_to_property_value(&value); compare_pv( &pv, &super::PropertyValue { kind: super::PropertyValueKind::Code, code: expected_code.into(), ..Default::default() }, ); let (is_array, headers, values) = super::map_preview_data_to_property_value_table(&value); assert_eq!(is_array, expected_is_array); for (idx, h) in headers.iter().enumerate() { eprintln!("Header {idx}: \"{h}\""); } assert_eq!(headers.len(), expected_headers.len()); assert!(headers.iter().zip(expected_headers.iter()).all(|(rh, eh)| rh == eh)); assert_eq!(values.len(), expected_table.len()); for (rr, er) in values.iter().zip(expected_table.iter()) { assert!(!rr.is_empty()); assert_eq!(rr.len(), er.len()); rr.iter().zip(er.iter()).for_each(|(rv, ev)| compare_pv(rv, ev)); } } #[test] fn test_map_preview_data_string() { validate_rp( "in", "", "string", "\"Test\"", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "\"Test\"".into(), code: "\"Test\"".into(), kind: super::PropertyValueKind::String, value_string: "Test".into(), ..Default::default() }, ); } #[test] fn test_map_preview_data_length_px() { validate_rp( "in", "", "length", "100px", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "100".into(), code: "100".into(), kind: super::PropertyValueKind::Float, value_float: 100.0, visual_items: std::rc::Rc::new(VecModel::from(vec![ "px".into(), "cm".into(), "mm".into(), "in".into(), "pt".into(), "phx".into(), "rem".into(), ])) .into(), ..Default::default() }, ); } #[test] fn test_map_preview_data_length_cm() { validate_rp( "in", "", "length", "10cm", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "378px".into(), code: "378".into(), kind: super::PropertyValueKind::Float, value_float: 378.0, visual_items: std::rc::Rc::new(VecModel::from(vec![ "px".into(), "cm".into(), "mm".into(), "in".into(), "pt".into(), "phx".into(), "rem".into(), ])) .into(), ..Default::default() }, ); } #[test] fn test_map_preview_data_duration() { validate_rp( "in", "", "duration", "100s", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "100000ms".into(), code: "100000".into(), kind: super::PropertyValueKind::Float, value_float: 100000.0, visual_items: std::rc::Rc::new(VecModel::from(vec!["s".into(), "ms".into()])) .into(), default_selection: 1, value_int: 1, ..Default::default() }, ); } #[test] fn test_map_preview_data_angle() { validate_rp( "in", "", "angle", "100turn", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "36000deg".into(), code: "36000".into(), kind: super::PropertyValueKind::Float, value_float: 36000.0, visual_items: std::rc::Rc::new(VecModel::from(vec![ "deg".into(), "grad".into(), "turn".into(), "rad".into(), ])) .into(), ..Default::default() }, ); } #[test] fn test_map_preview_data_percent() { validate_rp( "in", "", "percent", "10%", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "10%".into(), code: "10".into(), kind: super::PropertyValueKind::Float, value_float: 10.0, visual_items: std::rc::Rc::new(VecModel::from(vec!["%".into()])).into(), ..Default::default() }, ); } #[test] fn test_map_preview_data_color() { validate_rp( "in", "", "color", "#aabbcc", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "#aabbcc".into(), code: "\"#aabbcc\"".into(), kind: super::PropertyValueKind::Color, value_brush: slint::Brush::SolidColor(slint::Color::from_argb_u8( 0xff, 0xaa, 0xbb, 0xcc, )), ..Default::default() }, ); } #[test] fn test_map_preview_data_int() { validate_rp( "in", "", "int", "12", super::PreviewData { name: "test".into(), has_setter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "12".into(), code: "12".into(), kind: super::PropertyValueKind::Integer, value_int: 12, ..Default::default() }, ); } #[test] fn test_map_preview_data_bool_true() { validate_rp( "out", "", "bool", "true", super::PreviewData { name: "test".into(), has_getter: true, kind: super::PreviewDataKind::Value, ..Default::default() }, super::PropertyValue { display_string: "true".into(), code: "true".into(), kind: super::PropertyValueKind::Boolean, value_bool: true, ..Default::default() }, ); } #[test] fn test_map_preview_data_bool_false() { validate_rp( "in-out", "", "bool", "false", super::PreviewData { name: "test".into(), has_getter: true, has_setter: true, kind: super::PreviewDataKind::Value, }, super::PropertyValue { display_string: "false".into(), code: "false".into(), kind: super::PropertyValueKind::Boolean, value_bool: false, ..Default::default() }, ); } #[test] fn test_map_preview_data_struct_with_array() { validate_rp( "in-out", r#" struct FooStruct { first: [string] } "#, "FooStruct", "{ first: [ \"first of a kind\", \"second of a kind\"] }", super::PreviewData { name: "test".into(), has_getter: true, has_setter: true, kind: super::PreviewDataKind::Json, }, super::PropertyValue { kind: super::PropertyValueKind::Code, code: "{\n \"first\": [\n \"first of a kind\",\n \"second of a kind\"\n ]\n}" .into(), ..Default::default() }, ); } #[test] fn test_map_preview_data_struct() { validate_table_rp( "in-out", "struct FooStruct { bar: bool, count: int }", "FooStruct", "{ bar: true, count: 23 }", super::PreviewData { name: "test".into(), has_getter: true, has_setter: true, kind: super::PreviewDataKind::Table, }, "{\n \"bar\": true,\n \"count\": 23\n}", false, vec!["bar".into(), "count".into()], vec![vec![ super::PropertyValue { display_string: "true".into(), code: "true".into(), kind: super::PropertyValueKind::Boolean, value_bool: true, ..Default::default() }, super::PropertyValue { display_string: "23".into(), code: "23".into(), kind: super::PropertyValueKind::Integer, value_int: 23, ..Default::default() }, ]], ); } #[test] fn test_map_preview_data_struct_of_structs() { validate_table_rp( "in-out", r#" struct C1 { c1_1: string, c1_2: int } struct C2 { c2_1: string, c2_2: int } struct FooStruct { first: C1, second: C2 } "#, "FooStruct", "{ first: { c1_1: \"first of a kind\", c1_2: 23 }, second: { c2_1: \"second of a kind\", c2_2: 42 } }", super::PreviewData { name: "test".into(), has_getter: true, has_setter: true, kind: super::PreviewDataKind::Table, }, "{\n \"first\": {\n \"c1-1\": \"first of a kind\",\n \"c1-2\": 23\n },\n \"second\": {\n \"c2-1\": \"second of a kind\",\n \"c2-2\": 42\n }\n}", false, vec![ "first.c1-1".into(), "first.c1-2".into(), "second.c2-1".into(), "second.c2-2".into(), ], vec![ vec![super::PropertyValue { display_string: "first of a kind".into(), code: "\"first of a kind\"".into(), kind: super::PropertyValueKind::String, value_string: "first of a kind".into(), ..Default::default() }, super::PropertyValue { display_string: "23".into(), code: "23".into(), kind: super::PropertyValueKind::Integer, value_int: 23, ..Default::default() }, super::PropertyValue { display_string: "second of a kind".into(), code: "\"second of a kind\"".into(), kind: super::PropertyValueKind::String, value_string: "second of a kind".into(), ..Default::default() }, super::PropertyValue { display_string: "42".into(), code: "42".into(), kind: super::PropertyValueKind::Integer, value_int: 42, ..Default::default() }, ], ] ); } #[test] fn test_map_preview_data_array_of_struct_of_structs() { validate_table_rp( "in-out", r#" struct C1 { c1_1: string, c1_2: int } struct C2 { c2_1: string, c2_2: int } struct FooStruct { first: C1, second: C2 } "#, "[FooStruct]", "[{ first: { c1_1: \"first of a kind\", c1_2: 23 }, second: { c2_1: \"second of a kind\", c2_2: 42 } }, { first: { c1_1: \"row 2, 1\", c1_2: 3 }, second: { c2_1: \"row 2, 2\", c2_2: 2 } }]", super::PreviewData { name: "test".into(), has_getter: true, has_setter: true, kind: super::PreviewDataKind::Table, }, "[\n {\n \"first\": {\n \"c1-1\": \"first of a kind\",\n \"c1-2\": 23\n },\n \"second\": {\n \"c2-1\": \"second of a kind\",\n \"c2-2\": 42\n }\n },\n {\n \"first\": {\n \"c1-1\": \"row 2, 1\",\n \"c1-2\": 3\n },\n \"second\": {\n \"c2-1\": \"row 2, 2\",\n \"c2-2\": 2\n }\n }\n]", true, vec![ "first.c1-1".into(), "first.c1-2".into(), "second.c2-1".into(), "second.c2-2".into(), ], vec![ vec![super::PropertyValue { display_string: "first of a kind".into(), code: "\"first of a kind\"".into(), kind: super::PropertyValueKind::String, value_string: "first of a kind".into(), ..Default::default() }, super::PropertyValue { display_string: "23".into(), code: "23".into(), kind: super::PropertyValueKind::Integer, value_int: 23, ..Default::default() }, super::PropertyValue { display_string: "second of a kind".into(), code: "\"second of a kind\"".into(), kind: super::PropertyValueKind::String, value_string: "second of a kind".into(), ..Default::default() }, super::PropertyValue { display_string: "42".into(), code: "42".into(), kind: super::PropertyValueKind::Integer, value_int: 42, ..Default::default() }, ], vec![super::PropertyValue { display_string: "row 2, 1".into(), code: "\"row 2, 1\"".into(), kind: super::PropertyValueKind::String, value_string: "row 2, 1".into(), ..Default::default() }, super::PropertyValue { display_string: "3".into(), code: "3".into(), kind: super::PropertyValueKind::Integer, value_int: 3, ..Default::default() }, super::PropertyValue { display_string: "row 2, 2".into(), code: "\"row 2, 2\"".into(), kind: super::PropertyValueKind::String, value_string: "row 2, 2".into(), ..Default::default() }, super::PropertyValue { display_string: "2".into(), code: "2".into(), kind: super::PropertyValueKind::Integer, value_int: 2, ..Default::default() }, ], ] ); } #[test] fn test_map_preview_data_bool_array() { validate_table_rp( "in-out", "", "[bool]", "[ true, false ]", super::PreviewData { name: "test".into(), has_getter: true, has_setter: true, kind: super::PreviewDataKind::Table, }, "[\n true,\n false\n]", true, vec!["".into()], vec![ vec![super::PropertyValue { display_string: "true".into(), code: "true".into(), kind: super::PropertyValueKind::Boolean, value_bool: true, ..Default::default() }], vec![super::PropertyValue { display_string: "false".into(), code: "false".into(), kind: super::PropertyValueKind::Boolean, value_bool: false, ..Default::default() }], ], ); } #[track_caller] fn validate_array_row_to_struct(indent_level: usize, row: Vec, expected: &str) { let model = std::rc::Rc::new(VecModel::from(row)).into(); let received = super::table_row_to_struct(model, indent_level).unwrap(); assert_eq!(received, expected); } #[test] fn test_table_row_to_stuct() { fn bool_pv(value: bool, accessor_path: &str) -> PropertyValue { PropertyValue { accessor_path: SharedString::from(accessor_path), display_string: value.to_shared_string(), value_bool: value, code: value.to_shared_string(), ..Default::default() } } validate_array_row_to_struct(0, vec![bool_pv(true, "")], "true"); validate_array_row_to_struct(1, vec![bool_pv(true, "")], " true"); validate_array_row_to_struct(2, vec![bool_pv(true, "")], " true"); validate_array_row_to_struct(3, vec![bool_pv(true, "")], " true"); validate_array_row_to_struct( 1, vec![bool_pv(true, "test")], " {\n \"test\": true\n }", ); validate_array_row_to_struct( 0, vec![bool_pv(true, "l1.l2.l3")], "{\n \"l1\": {\n \"l2\": {\n \"l3\": true\n }\n }\n}", ); validate_array_row_to_struct( 0, vec![bool_pv(true, "l1.l2.l3"), bool_pv(false, "l1.test")], "{\n \"l1\": {\n \"l2\": {\n \"l3\": true\n },\n \"test\": false\n }\n}", ); } }