slint/tools/lsp/preview/ui.rs
Tobias Hunger 77f676d69d
live-preview: Update the live-preview data model (#7741)
... on reloads. That is needed to make the UI aware
of the new data.

I tried to be clever and not update widgets on model
changes, but that is not a problem anymore as all
the property values are no longer passed through
the model. So the only reason to update the model now
is because the code was loaded fresh and we *need*
to refresh the entire UI in that case.
2025-02-26 11:00:06 +13:00

2177 lines
77 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
use std::path::PathBuf;
use std::{collections::HashMap, iter::once, rc::Rc};
use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, TextRange};
use i_slint_compiler::{expression_tree, langtype, literals};
use itertools::Itertools;
use lsp_types::Url;
use slint::{Model, SharedString, 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::*;
slint::include_modules!();
pub type PropertyDeclarations = HashMap<SmolStr, PropertyDeclaration>;
pub fn create_ui(style: String, experimental: bool) -> Result<PreviewUi, PlatformError> {
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::<Vec<_>>();
let style = if known_styles.contains(&style.as_str()) {
style
} 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>();
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);
api.on_show_document(|file, line, column| {
use lsp_types::{Position, Range};
let pos = Position::new((line as u32).saturating_sub(1), (column as u32).saturating_sub(1));
super::ask_editor_to_show_document(&file, Range::new(pos, pos), false)
});
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_can_drop(super::can_drop_component);
api.on_drop(super::drop_component);
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_property_declaration_ranges(super::property_declaration_ranges);
api.on_get_property_value(get_property_value);
api.on_set_json_preview_data(set_json_preview_data);
api.on_string_to_code(string_to_code);
api.on_string_to_color(|s| string_to_color(s.as_ref()).unwrap_or_default());
api.on_string_is_color(|s| string_to_color(s.as_ref()).is_some());
api.on_color_to_data(|c| {
let encoded = c.as_argb_encoded();
let a = ((encoded & 0xff000000) >> 24) as u8;
let r = ((encoded & 0x00ff0000) >> 16) as u8;
let g = ((encoded & 0x0000ff00) >> 8) as u8;
let b = (encoded & 0x000000ff) as u8;
ColorData {
a: a as i32,
r: r as i32,
g: g as i32,
b: b as i32,
text: format!(
"#{:08x}",
((r as u32) << 24) + ((g as u32) << 16) + ((b as u32) << 8) + (a as u32)
)
.into(),
}
});
api.on_rgba_to_color(|r, g, b, a| {
if (0..256).contains(&r)
&& (0..256).contains(&g)
&& (0..256).contains(&b)
&& (0..256).contains(&a)
{
slint::Color::from_argb_u8(a as u8, r as u8, g as u8, b as u8)
} else {
slint::Color::default()
}
});
#[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>();
api.set_uses_widgets(uses_widgets);
}
pub fn set_diagnostics(ui: &PreviewUi, diagnostics: &[slint_interpreter::Diagnostic]) {
let summary = diagnostics.iter().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>();
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<String, Vec<ComponentItem>> = Default::default();
let mut std_widgets_map: HashMap<String, Vec<ComponentItem>> = Default::default();
let mut path_map: HashMap<PathBuf, (SharedString, Vec<ComponentItem>)> = Default::default();
let mut library_map: HashMap<String, Vec<ComponentItem>> = 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);
}
}
fn sort_subset(mut input: HashMap<String, Vec<ComponentItem>>) -> Vec<ComponentListItem> {
let mut output = input
.drain()
.map(|(k, mut v)| {
v.sort_by_key(|i| i.name.clone());
let model = Rc::new(VecModel::from(v));
ComponentListItem {
category: k.into(),
file_url: SharedString::new(),
components: model.into(),
}
})
.collect::<Vec<_>>();
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(VecModel::from(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::<Vec<_>>();
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(VecModel::from(all_components));
let api = ui.global::<Api>();
api.set_known_components(result.into());
}
fn to_ui_range(r: TextRange) -> Option<Range> {
Some(Range {
start: i32::try_from(u32::from(r.start())).ok()?,
end: i32::try_from(u32::from(r.end())).ok()?,
})
}
fn map_property_declaration(
document_cache: &common::DocumentCache,
declared_at: &Option<properties::DeclarationInformation>,
defined_at: PropertyDefinition,
) -> Option<PropertyDeclaration> {
let da = declared_at.as_ref()?;
let source_version = document_cache.document_version_by_path(&da.path).unwrap_or(-1);
let pos = TextRange::new(da.start_position, da.start_position);
Some(PropertyDeclaration {
defined_at,
source_path: da.path.to_string_lossy().to_string().into(),
source_version,
range: to_ui_range(pos)?,
})
}
fn extract_tr_data(tr_node: &syntax_nodes::AtTr, value: &mut PropertyValue) {
let Some(text) = tr_node
.child_text(SyntaxKind::StringLiteral)
.and_then(|s| i_slint_compiler::literals::unescape_string(&s))
else {
return;
};
let context = tr_node
.TrContext()
.and_then(|n| n.child_text(SyntaxKind::StringLiteral))
.and_then(|s| i_slint_compiler::literals::unescape_string(&s))
.unwrap_or_default();
let plural = tr_node
.TrPlural()
.and_then(|n| n.child_text(SyntaxKind::StringLiteral))
.and_then(|s| i_slint_compiler::literals::unescape_string(&s))
.unwrap_or_default();
let plural_expression = tr_node
.TrPlural()
.and_then(|n| n.child_node(SyntaxKind::Expression))
.and_then(|e| e.child_node(SyntaxKind::QualifiedName))
.map(|n| i_slint_compiler::object_tree::QualifiedTypeName::from_node(n.into()))
.map(|qtn| qtn.to_string());
// We have expressions -> Edit as code
if tr_node.Expression().next().is_none() && (plural.is_empty() || plural_expression.is_some()) {
value.kind = PropertyValueKind::String;
value.is_translatable = true;
value.tr_context = context.as_str().into();
value.tr_plural = plural.as_str().into();
value.tr_plural_expression = plural_expression.unwrap_or_default().into();
value.value_string = text.as_str().into();
}
}
fn convert_number_literal(
node: &syntax_nodes::Expression,
) -> Option<(f64, i_slint_compiler::expression_tree::Unit)> {
if let Some(unary) = &node.UnaryOpExpression() {
let factor = match unary.first_token().unwrap().text() {
"-" => -1.0,
"+" => 1.0,
_ => return None,
};
convert_number_literal(&unary.Expression()).map(|(v, u)| (factor * v, u))
} else {
let literal = node.child_text(SyntaxKind::NumberLiteral)?;
let expr = literals::parse_number_literal(literal).ok()?;
match expr {
i_slint_compiler::expression_tree::Expression::NumberLiteral(value, unit) => {
Some((value, unit))
}
_ => None,
}
}
}
fn extract_value_with_unit_impl(
expression: &Option<syntax_nodes::Expression>,
def_val: Option<&expression_tree::Expression>,
code: &str,
units: &[i_slint_compiler::expression_tree::Unit],
) -> Option<(PropertyValueKind, f32, i32)> {
if let Some(expression) = expression {
if let Some((value, unit)) = convert_number_literal(expression) {
let index = units.iter().position(|u| u == &unit).or_else(|| {
(units.is_empty() && unit == i_slint_compiler::expression_tree::Unit::None)
.then_some(0_usize)
})?;
return Some((PropertyValueKind::Float, value as f32, index as i32));
}
} else if code.is_empty() {
if let Some(expression_tree::Expression::NumberLiteral(value, unit)) = def_val {
let index = units.iter().position(|u| u == unit).unwrap_or(0);
return Some((PropertyValueKind::Float, *value as f32, index as i32));
} else {
// FIXME: if def_vale is Some but not a NumberLiteral, we should not show "0"
return Some((PropertyValueKind::Float, 0.0, 0));
}
}
None
}
fn convert_simple_string(input: slint::SharedString) -> String {
format!("\"{}\"", str::escape_debug(input.as_ref()))
}
fn string_to_code(
input: slint::SharedString,
is_translatable: bool,
tr_context: slint::SharedString,
tr_plural: slint::SharedString,
tr_plural_expression: slint::SharedString,
) -> slint::SharedString {
let input = convert_simple_string(input);
if !is_translatable {
input
} else {
let context = if tr_context.is_empty() {
String::new()
} else {
format!("{} => ", convert_simple_string(tr_context))
};
let plural = if tr_plural.is_empty() {
String::new()
} else {
format!(" | {} % {}", convert_simple_string(tr_plural), tr_plural_expression)
};
format!("@tr({context}{input}{plural})")
}
.into()
}
fn string_to_color(text: &str) -> Option<slint::Color> {
literals::parse_color_literal(text).map(slint::Color::from_argb_encoded)
}
fn unit_model(units: &[expression_tree::Unit]) -> slint::ModelRc<slint::SharedString> {
Rc::new(VecModel::from(
units.iter().map(|u| u.to_string().into()).collect::<Vec<slint::SharedString>>(),
))
.into()
}
fn extract_value_with_unit(
expression: &Option<syntax_nodes::Expression>,
def_val: Option<&expression_tree::Expression>,
units: &[expression_tree::Unit],
value: &mut PropertyValue,
) {
let Some((kind, v, index)) =
extract_value_with_unit_impl(expression, def_val, &value.code, units)
else {
return;
};
value.kind = kind;
value.value_float = v;
value.visual_items = unit_model(units);
value.value_int = index
}
fn extract_color(
expression: &syntax_nodes::Expression,
kind: PropertyValueKind,
value: &mut PropertyValue,
) -> bool {
if let Some(text) = expression.child_text(SyntaxKind::ColorLiteral) {
if let Some(color) = string_to_color(&text) {
value.kind = kind;
value.value_brush = slint::Brush::SolidColor(color);
value.value_string = text.as_str().into();
return true;
}
}
false
}
fn set_default_brush(
kind: PropertyValueKind,
def_val: Option<&expression_tree::Expression>,
value: &mut PropertyValue,
) {
use expression_tree::Expression;
value.kind = kind;
if let Some(mut def_val) = def_val {
if let Expression::Cast { from, .. } = def_val {
def_val = from;
}
if let Expression::NumberLiteral(v, _) = def_val {
value.value_brush = slint::Brush::SolidColor(slint::Color::from_argb_encoded(*v as _));
return;
}
}
let text = "#00000000";
let color = literals::parse_color_literal(text).unwrap();
value.value_string = text.into();
value.value_brush = slint::Brush::SolidColor(slint::Color::from_argb_encoded(color));
}
fn simplify_value(prop_info: &super::properties::PropertyInformation) -> PropertyValue {
use i_slint_compiler::expression_tree::Unit;
use langtype::Type;
let code_block_or_expression =
prop_info.defined_at.as_ref().map(|da| da.code_block_or_expression.clone());
let expression = code_block_or_expression.as_ref().and_then(|cbe| cbe.expression());
let mut value = PropertyValue {
code: code_block_or_expression
.as_ref()
.map(|cbe| cbe.text().to_string())
.unwrap_or_default()
.into(),
kind: PropertyValueKind::Code,
..Default::default()
};
let def_val = prop_info.default_value.as_ref();
match &prop_info.ty {
Type::Float32 => extract_value_with_unit(&expression, def_val, &[], &mut value),
Type::Duration => {
extract_value_with_unit(&expression, def_val, &[Unit::S, Unit::Ms], &mut value)
}
Type::PhysicalLength | Type::LogicalLength | Type::Rem => extract_value_with_unit(
&expression,
def_val,
&[Unit::Px, Unit::Cm, Unit::Mm, Unit::In, Unit::Pt, Unit::Phx, Unit::Rem],
&mut value,
),
Type::Angle => extract_value_with_unit(
&expression,
def_val,
&[Unit::Deg, Unit::Grad, Unit::Turn, Unit::Rad],
&mut value,
),
Type::Percent => {
extract_value_with_unit(&expression, def_val, &[Unit::Percent], &mut value)
}
Type::Int32 => {
if let Some(expression) = expression {
if let Some((v, unit)) = convert_number_literal(&expression) {
if unit == i_slint_compiler::expression_tree::Unit::None {
value.kind = PropertyValueKind::Integer;
value.value_int = v as i32;
}
}
} else if value.code.is_empty() {
value.kind = PropertyValueKind::Integer;
}
}
Type::Color => {
if let Some(expression) = expression {
extract_color(&expression, PropertyValueKind::Color, &mut value);
// TODO: Extract `Foo.bar` as Palette `Foo`, entry `bar`.
// This makes no sense right now, as we have no way to get any
// information on the palettes.
} else if value.code.is_empty() {
set_default_brush(PropertyValueKind::Color, def_val, &mut value);
}
}
Type::Brush => {
if let Some(expression) = expression {
extract_color(&expression, PropertyValueKind::Brush, &mut value);
// TODO: Handle gradients...
} else if value.code.is_empty() {
set_default_brush(PropertyValueKind::Brush, def_val, &mut value);
}
}
Type::Bool => {
if let Some(expression) = expression {
let qualified_name =
expression.QualifiedName().map(|qn| qn.text().to_string()).unwrap_or_default();
if ["true", "false"].contains(&qualified_name.as_str()) {
value.kind = PropertyValueKind::Boolean;
value.value_bool = &qualified_name == "true";
}
} else if value.code.is_empty() {
if let Some(expression_tree::Expression::BoolLiteral(v)) = def_val {
value.value_bool = *v;
}
value.kind = PropertyValueKind::Boolean;
}
}
Type::String => {
if let Some(expression) = &expression {
if let Some(text) = expression
.child_text(SyntaxKind::StringLiteral)
.and_then(|s| i_slint_compiler::literals::unescape_string(&s))
{
value.kind = PropertyValueKind::String;
value.value_string = text.as_str().into();
} else if let Some(tr_node) = &expression.AtTr() {
extract_tr_data(tr_node, &mut value)
}
} else if value.code.is_empty() {
if let Some(expression_tree::Expression::StringLiteral(v)) = def_val {
value.value_string = v.as_str().into();
}
value.kind = PropertyValueKind::String;
}
}
Type::Enumeration(enumeration) => {
value.kind = PropertyValueKind::Enum;
value.value_string = enumeration.name.as_str().into();
value.default_selection = i32::try_from(enumeration.default_value).unwrap_or_default();
value.visual_items = Rc::new(VecModel::from(
enumeration
.values
.iter()
.map(|s| SharedString::from(s.as_str()))
.collect::<Vec<_>>(),
))
.into();
if let Some(expression) = expression {
if let Some(text) = expression
.child_node(SyntaxKind::QualifiedName)
.map(|n| i_slint_compiler::object_tree::QualifiedTypeName::from_node(n.into()))
.map(|n| {
let n_str = n.to_string();
n_str
.strip_prefix(&format!("{}.", enumeration.name))
.map(|s| s.to_string())
.unwrap_or(n_str)
})
.map(|s| s.to_string())
{
value.value_int = enumeration
.values
.iter()
.position(|v| v == &text)
.and_then(|v| i32::try_from(v).ok())
.unwrap_or_default();
}
} else if let Some(expression_tree::Expression::EnumerationValue(v)) = def_val {
value.value_int = v.value as i32
}
}
_ => {}
}
value
}
fn map_property_definition(
defined_at: &Option<properties::DefinitionInformation>,
) -> Option<PropertyDefinition> {
let da = defined_at.as_ref()?;
Some(PropertyDefinition {
definition_range: to_ui_range(da.property_definition_range)?,
selection_range: to_ui_range(da.selection_range)?,
expression_range: to_ui_range(da.code_block_or_expression.text_range())?,
expression_value: da.code_block_or_expression.text().to_string().into(),
})
}
fn map_properties_to_ui(
document_cache: &common::DocumentCache,
properties: Option<properties::QueryPropertyResponse>,
) -> Option<(ElementInformation, HashMap<SmolStr, PropertyDeclaration>, PropertyGroupModel)> {
use std::cmp::Ordering;
let properties = &properties?;
let element = properties.element.as_ref()?;
let raw_source_uri = Url::parse(&properties.source_uri).ok()?;
let source_uri: SharedString = raw_source_uri.to_string().into();
let source_version = properties.source_version;
let mut property_groups: HashMap<(SmolStr, u32), Vec<PropertyInformation>> = HashMap::new();
let mut declarations = HashMap::new();
fn property_group_from(
groups: &mut HashMap<(SmolStr, u32), Vec<PropertyInformation>>,
name: SmolStr,
group_priority: u32,
property: PropertyInformation,
) {
let entry = groups.entry((name.clone(), group_priority));
entry.and_modify(|e| e.push(property.clone())).or_insert(vec![property]);
}
for pi in &properties.properties {
let defined_at = map_property_definition(&pi.defined_at).unwrap_or(PropertyDefinition {
definition_range: Range { start: 0, end: 0 },
selection_range: Range { start: 0, end: 0 },
expression_range: Range { start: 0, end: 0 },
expression_value: String::new().into(),
});
let declared_at =
map_property_declaration(document_cache, &pi.declared_at, defined_at.clone())
.unwrap_or(PropertyDeclaration {
defined_at,
source_path: String::new().into(),
source_version: -1,
range: Range { start: 0, end: 0 },
});
declarations.insert(pi.name.clone(), declared_at);
let value = simplify_value(pi);
property_group_from(
&mut property_groups,
pi.group.clone(),
pi.group_priority,
PropertyInformation {
name: pi.name.as_str().into(),
type_name: pi.ty.to_string().into(),
value,
display_priority: i32::try_from(pi.priority).unwrap(),
},
);
}
let keys = property_groups
.keys()
.sorted_by(|a, b| match a.1.cmp(&b.1) {
Ordering::Less => Ordering::Less,
Ordering::Equal => a.0.cmp(&b.0),
Ordering::Greater => Ordering::Greater,
})
.cloned()
.collect::<Vec<_>>();
Some((
ElementInformation {
id: element.id.as_str().into(),
type_name: element.type_name.as_str().into(),
source_uri,
source_version,
range: to_ui_range(element.range)?,
},
declarations,
Rc::new(VecModel::from(
keys.iter()
.map(|k| PropertyGroup {
group_name: k.0.as_str().into(),
properties: Rc::new(VecModel::from({
let mut v = property_groups.remove(k).unwrap();
v.sort_by(|a, b| match a.display_priority.cmp(&b.display_priority) {
Ordering::Less => Ordering::Less,
Ordering::Equal => a.name.cmp(&b.name),
Ordering::Greater => Ordering::Greater,
});
v
}))
.into(),
})
.collect::<Vec<_>>(),
))
.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.range.start == n.range.start
}
pub type PropertyGroupModel = slint::ModelRc<PropertyGroup>;
fn update_grouped_properties(
cvg: &VecModel<PropertyInformation>,
nvg: &VecModel<PropertyInformation>,
) {
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<T: Sized + std::convert::TryFrom<slint_interpreter::Value> + std::default::Default>(
v: &Option<slint_interpreter::Value>,
) -> T {
v.clone().and_then(|v| v.try_into().ok()).unwrap_or_default()
}
fn get_code(v: &Option<slint_interpreter::Value>) -> 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: String,
is_too_complex: bool,
is_array: bool,
header: Vec<String>,
current_values: Vec<PropertyValue>,
array_values: Vec<Vec<PropertyValue>>,
}
fn map_value_and_type(
ty: &langtype::Type,
value: &Option<slint_interpreter::Value>,
mapping: &mut ValueMapping,
) {
use i_slint_compiler::expression_tree::Unit;
use langtype::Type;
match ty {
Type::Float32 => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: get_value::<f32>(value).to_string().into(),
code: get_code(value),
..Default::default()
});
}
Type::Int32 => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Integer,
value_int: get_value::<i32>(value),
value_string: get_value::<i32>(value).to_string().into(),
code: get_code(value),
..Default::default()
});
}
Type::Duration => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: format!("{}{}", get_value::<f32>(value), Unit::Ms).into(),
visual_items: unit_model(&[Unit::Ms]),
value_int: 0,
code: get_code(value),
default_selection: 1,
..Default::default()
});
}
Type::PhysicalLength => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: format!("{}{}", get_value::<f32>(value), Unit::Phx).into(),
visual_items: unit_model(&[Unit::Phx]),
value_int: 0,
code: get_code(value),
default_selection: 0,
..Default::default()
});
}
Type::LogicalLength => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: format!("{}{}", get_value::<f32>(value), Unit::Px).into(),
visual_items: unit_model(&[Unit::Px]),
value_int: 0,
code: get_code(value),
default_selection: 0,
..Default::default()
});
}
Type::Rem => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: format!("{}{}", get_value::<f32>(value), Unit::Rem).into(),
visual_items: unit_model(&[Unit::Rem]),
value_int: 0,
code: get_code(value),
default_selection: 0,
..Default::default()
});
}
Type::Angle => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: format!("{}{}", get_value::<f32>(value), Unit::Deg).into(),
visual_items: unit_model(&[Unit::Deg]),
value_int: 0,
code: get_code(value),
default_selection: 0,
..Default::default()
});
}
Type::Percent => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Float,
value_float: get_value::<f32>(value),
value_string: format!("{}{}", get_value::<f32>(value), Unit::Percent).into(),
visual_items: unit_model(&[Unit::Percent]),
value_int: 0,
code: get_code(value),
default_selection: 0,
..Default::default()
});
}
Type::String => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::String,
value_string: get_value::<slint::SharedString>(value),
code: get_code(value),
..Default::default()
});
}
Type::Color => {
let color = get_value::<slint::Color>(value);
let color_string = {
let a = color.alpha();
let r = color.red();
let g = color.green();
let b = color.blue();
format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
};
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Color,
value_brush: slint::Brush::SolidColor(color),
value_string: color_string.into(),
code: get_code(value),
..Default::default()
});
}
Type::Bool => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Boolean,
value_bool: get_value::<bool>(value),
value_string: if get_value::<bool>(value) { "true".into() } else { "false".into() },
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.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
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::<Vec<_>>(),
))
.into(),
..Default::default()
});
}
Type::Array(_)
| Type::Image
| Type::Model
| Type::PathData
| Type::Easing
| Type::Brush
| Type::Struct(_)
| Type::UnitProduct(_) => {
mapping.is_too_complex = true;
}
_ => {
mapping.header.push(mapping.name_prefix.clone());
mapping.current_values.push(PropertyValue {
kind: PropertyValueKind::Code,
value_string: "???".into(),
code: get_code(value),
..Default::default()
});
}
}
if mapping.array_values.is_empty() {
mapping.array_values = vec![std::mem::take(&mut mapping.current_values)];
}
// Back out when this got too complex and just put the JSON value in:
if mapping.is_too_complex {
mapping.is_array = false;
mapping.header = vec![String::new()];
mapping.array_values = vec![vec![PropertyValue {
kind: PropertyValueKind::Code,
code: get_code(value),
..Default::default()
}]]
}
}
fn map_preview_data_to_property_value(
preview_data: &preview_data::PreviewData,
) -> Option<PropertyValue> {
let mut mapping = ValueMapping::default();
map_value_and_type(&preview_data.ty, &preview_data.value, &mut mapping);
mapping.array_values.first().and_then(|av| av.first()).cloned()
}
fn map_preview_data_property(preview_data: &preview_data::PreviewData) -> Option<PreviewData> {
if !preview_data.is_property() {
return None;
};
let has_getter = preview_data.has_getter();
let has_setter = preview_data.has_setter();
let mut mapping = ValueMapping::default();
map_value_and_type(&preview_data.ty, &preview_data.value, &mut mapping);
Some(PreviewData {
name: preview_data.name.clone().into(),
has_getter,
has_setter,
kind: if mapping.is_too_complex { PreviewDataKind::Json } else { PreviewDataKind::Value },
})
}
pub fn ui_set_preview_data(
ui: &PreviewUi,
preview_data: HashMap<preview_data::PropertyContainer, Vec<preview_data::PreviewData>>,
) {
fn fill_container(
container_name: String,
container_id: String,
properties: &[preview_data::PreviewData],
) -> Option<PropertyContainer> {
let properties =
properties.iter().filter_map(map_preview_data_property).collect::<Vec<_>>();
(!properties.is_empty()).then(|| PropertyContainer {
container_name: container_name.into(),
container_id: container_id.into(),
properties: Rc::new(slint::VecModel::from(properties)).into(),
})
}
let mut result: Vec<PropertyContainer> = vec![];
if let Some(main) = preview_data.get(&preview_data::PropertyContainer::Main) {
if let Some(c) = fill_container("<MAIN>".to_string(), String::new(), main) {
result.push(c)
}
}
for component_key in
preview_data.keys().filter(|k| **k != preview_data::PropertyContainer::Main)
{
if let Some(component) = preview_data.get(component_key) {
let component_key = component_key.to_string();
if let Some(c) = fill_container(component_key.clone(), component_key, component) {
result.push(c);
}
}
}
let api = ui.global::<Api>();
api.set_preview_data(Rc::new(VecModel::from(result)).into());
}
fn to_property_container(container: slint::SharedString) -> preview_data::PropertyContainer {
if container.is_empty() {
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.to_string(),
)
})
.and_then(|pd| map_preview_data_to_property_value(&pd))
.unwrap_or_else(Default::default)
}
fn set_json_preview_data(
container: SharedString,
property_name: SharedString,
json_string: SharedString,
) -> bool {
let property_name = (!property_name.is_empty()).then_some(property_name.to_string());
let Ok(json) = serde_json::from_str::<serde_json::Value>(json_string.as_ref()) else {
return false;
};
if property_name.is_none() && !json.is_object() {
return false;
}
preview::component_instance()
.and_then(|component_instance| {
preview_data::set_json_preview_data(
&component_instance,
to_property_container(container),
property_name,
json,
)
.ok()
})
.is_some()
}
fn update_properties(
current_model: PropertyGroupModel,
next_model: PropertyGroupModel,
) -> PropertyGroupModel {
debug_assert_eq!(current_model.row_count(), next_model.row_count());
for (c, n) in std::iter::zip(current_model.iter(), next_model.iter()) {
debug_assert_eq!(c.group_name, n.group_name);
let cvg = c.properties.as_any().downcast_ref::<VecModel<PropertyInformation>>().unwrap();
let nvg = n.properties.as_any().downcast_ref::<VecModel<PropertyInformation>>().unwrap();
update_grouped_properties(cvg, nvg);
}
current_model
}
pub fn ui_set_properties(
ui: &PreviewUi,
document_cache: &common::DocumentCache,
properties: Option<properties::QueryPropertyResponse>,
) -> PropertyDeclarations {
let (next_element, declarations, next_model) = map_properties_to_ui(document_cache, properties)
.unwrap_or((
ElementInformation {
id: "".into(),
type_name: "".into(),
source_uri: "".into(),
source_version: 0,
range: Range { start: 0, end: 0 },
},
HashMap::new(),
Rc::new(VecModel::from(Vec::<PropertyGroup>::new())).into(),
));
let api = ui.global::<Api>();
let current_model = api.get_properties();
let element = api.get_current_element();
if !is_equal_element(&element, &next_element) {
api.set_properties(next_model);
} else if current_model.row_count() > 0 {
update_properties(current_model, next_model);
} else {
api.set_properties(next_model);
}
api.set_current_element(next_element);
declarations
}
#[cfg(test)]
mod tests {
use crate::{language::test::loaded_document_cache, preview::preview_data};
use crate::common;
use crate::preview::properties;
use i_slint_core::model::Model;
use super::{PropertyInformation, PropertyValue, PropertyValueKind};
fn properties_at_position(
source: &str,
line: u32,
character: u32,
) -> Option<(
common::ElementRcNode,
Vec<properties::PropertyInformation>,
common::DocumentCache,
lsp_types::Url,
)> {
let (dc, url, _) = loaded_document_cache(source.to_string());
if let Some((e, p)) =
properties::tests::properties_at_position_in_cache(line, character, &dc, &url)
{
Some((e, p, dc, url))
} else {
None
}
}
fn property_conversion_test(contents: &str, property_line: u32) -> PropertyValue {
let (_, pi, _, _) = properties_at_position(contents, property_line, 30).unwrap();
let test1 = pi.iter().find(|pi| pi.name == "test1").unwrap();
super::simplify_value(test1)
}
#[test]
fn test_property_bool() {
let result =
property_conversion_test(r#"export component Test { in property <bool> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::Boolean);
assert!(!result.value_bool);
assert!(result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <bool> test1: true; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Boolean);
assert!(result.value_bool);
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <bool> test1: false; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Boolean);
assert!(!result.value_bool);
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <bool> test1: 1.1.round() == 1.1.floor(); }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert!(!result.value_bool);
assert!(!result.code.is_empty());
}
#[test]
fn test_property_string() {
let result =
property_conversion_test(r#"export component Test { in property <string> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::String);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert!(!result.value_bool);
assert!(result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <string> test1: ""; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::String);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert!(!result.value_bool);
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <string> test1: "string"; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::String);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert!(!result.value_bool);
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <string> test1: "" + "test"); }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert!(!result.value_bool);
assert!(!result.code.is_empty());
}
#[test]
fn test_property_tr_string() {
let result = property_conversion_test(
r#"export component Test { in property <string> test1: @tr("Context" => "test"); }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::String);
assert_eq!(result.value_string, "test");
assert!(result.is_translatable);
assert_eq!(result.tr_context, "Context");
assert_eq!(result.tr_plural, "");
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test {
property <int> test: 42;
in property <string> test1: @tr("{n} string" | "{n} strings" % test);
}"#,
2,
);
assert_eq!(result.kind, PropertyValueKind::String);
assert!(result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "{n} strings");
assert_eq!(result.tr_plural_expression, "test");
assert_eq!(result.value_string, "{n} string");
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test {
property <int> test: 42;
in property <string> test1: @tr("{n} string" | "{n} strings" % self.test);
}"#,
2,
);
assert_eq!(result.kind, PropertyValueKind::String);
assert!(result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "{n} strings");
assert_eq!(result.tr_plural_expression, "self.test");
assert_eq!(result.value_string, "{n} string");
assert!(!result.code.is_empty());
// `15` is not a qualified name
let result = property_conversion_test(
r#"export component Test { in property <string> test1: @tr("{n} string" | "{n} strings" % 15); }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert_eq!(result.value_string, "");
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <string> test1: @tr("" + "test"); }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert_eq!(result.value_string, "");
assert!(!result.code.is_empty());
let result = property_conversion_test(
r#"export component Test { in property <string> test1: @tr("width {}", self.width()); }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert!(!result.is_translatable);
assert_eq!(result.tr_context, "");
assert_eq!(result.tr_plural, "");
assert_eq!(result.value_string, "");
assert!(!result.code.is_empty());
}
#[test]
fn test_property_enum() {
let result = property_conversion_test(
r#"export component Test { in property <ImageFit> test1: ImageFit.preserve; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Enum);
assert_eq!(result.value_string, "ImageFit");
assert_eq!(result.value_int, 3);
assert_eq!(result.default_selection, 0);
assert!(!result.is_translatable);
assert_eq!(result.visual_items.row_count(), 4);
let result = property_conversion_test(
r#"export component Test { in property <ImageFit> test1: ImageFit . /* abc */ preserve; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Enum);
assert_eq!(result.value_string, "ImageFit");
assert_eq!(result.value_int, 3);
assert_eq!(result.default_selection, 0);
assert!(!result.is_translatable);
assert_eq!(result.visual_items.row_count(), 4);
let result = property_conversion_test(
r#"export component Test { in property <ImageFit> test1: /* abc */ preserve; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Enum);
assert_eq!(result.value_string, "ImageFit");
assert_eq!(result.value_int, 3);
assert_eq!(result.default_selection, 0);
assert!(!result.is_translatable);
assert_eq!(result.visual_items.row_count(), 4);
let result = property_conversion_test(
r#"enum Foobar { foo, bar }
export component Test { in property <Foobar> test1: Foobar.bar; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Enum);
assert_eq!(result.value_string, "Foobar");
assert_eq!(result.value_int, 1);
assert_eq!(result.default_selection, 0);
assert!(!result.is_translatable);
assert_eq!(result.visual_items.row_count(), 2);
assert_eq!(result.visual_items.row_data(0), Some(slint::SharedString::from("foo")));
assert_eq!(result.visual_items.row_data(1), Some(slint::SharedString::from("bar")));
let result = property_conversion_test(
r#"enum Foobar { foo, bar }
export component Test { in property <Foobar> test1; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Enum);
assert_eq!(result.value_string, "Foobar");
assert_eq!(result.value_int, 0); // default
assert_eq!(result.default_selection, 0);
assert!(!result.is_translatable);
assert_eq!(result.visual_items.row_count(), 2);
assert_eq!(result.visual_items.row_data(0), Some(slint::SharedString::from("foo")));
assert_eq!(result.visual_items.row_data(1), Some(slint::SharedString::from("bar")));
}
#[test]
fn test_property_float() {
let result =
property_conversion_test(r#"export component Test { in property <float> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, 0.0);
let result = property_conversion_test(
r#"export component Test { in property <float> test1: 42.0; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, 42.0);
let result = property_conversion_test(
r#"export component Test { in property <float> test1: +42.0; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, 42.0);
let result = property_conversion_test(
r#"export component Test { in property <float> test1: -42.0; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, -42.0);
let result = property_conversion_test(
r#"export component Test { in property <float> test1: 42.0 * 23.0; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert_eq!(result.value_float, 0.0);
}
#[test]
fn test_property_integer() {
let result =
property_conversion_test(r#"export component Test { in property <int> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::Integer);
assert_eq!(result.value_int, 0);
let result = property_conversion_test(
r#"export component Test { in property <int> test1: 42; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Integer);
assert_eq!(result.value_int, 42);
let result = property_conversion_test(
r#"export component Test { in property <int> test1: +42; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Integer);
assert_eq!(result.value_int, 42);
let result = property_conversion_test(
r#"export component Test { in property <int> test1: -42; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Integer);
assert_eq!(result.value_int, -42);
let result = property_conversion_test(
r#"export component Test { in property <int> test1: 42 * 23; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
assert_eq!(result.value_int, 0);
}
#[test]
fn test_property_color() {
let result =
property_conversion_test(r#"export component Test { in property <color> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::Color);
assert!(matches!(result.value_brush, slint::Brush::SolidColor(_)));
assert_eq!(result.value_brush.color().red(), 0);
assert_eq!(result.value_brush.color().green(), 0);
assert_eq!(result.value_brush.color().blue(), 0);
assert_eq!(result.value_brush.color().alpha(), 0);
let result = property_conversion_test(
r#"export component Test { in property <color> test1: #10203040; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Color);
assert!(matches!(result.value_brush, slint::Brush::SolidColor(_)));
assert_eq!(result.value_brush.color().red(), 0x10);
assert_eq!(result.value_brush.color().green(), 0x20);
assert_eq!(result.value_brush.color().blue(), 0x30);
assert_eq!(result.value_brush.color().alpha(), 0x40);
let result = property_conversion_test(
r#"export component Test { in property <color> test1: #10203040.darker(0.5); }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Code);
let result = property_conversion_test(
r#"export component Test { in property <color> test1: Colors.red; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
}
#[test]
fn test_property_brush() {
let result =
property_conversion_test(r#"export component Test { in property <brush> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::Brush);
assert!(matches!(result.value_brush, slint::Brush::SolidColor(_)));
assert_eq!(result.value_brush.color().red(), 0);
assert_eq!(result.value_brush.color().green(), 0);
assert_eq!(result.value_brush.color().blue(), 0);
assert_eq!(result.value_brush.color().alpha(), 0);
let result = property_conversion_test(
r#"export component Test { in property <brush> test1: #10203040; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Brush);
assert!(matches!(result.value_brush, slint::Brush::SolidColor(_)));
assert_eq!(result.value_brush.color().red(), 0x10);
assert_eq!(result.value_brush.color().green(), 0x20);
assert_eq!(result.value_brush.color().blue(), 0x30);
assert_eq!(result.value_brush.color().alpha(), 0x40);
let result = property_conversion_test(
r#"export component Test { in property <brush> test1: #10203040.darker(0.5); }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Code);
let result = property_conversion_test(
r#"export component Test { in property <brush> test1: Colors.red; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
let result = property_conversion_test(
r#"export component Test { in property <brush> test1: @linear-gradient(90deg, #3f87a6 0%, #ebf8e1 50%, #f69d3c 100%); }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Code);
let result = property_conversion_test(
r#"export component Test { in property <brush> test1: @radial-gradient(circle, #f00 0%, #0f0 50%, #00f 100%)
@linear-gradient(90deg, #3f87a6 0%, #ebf8e1 50%, #f69d3c 100%); }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Code);
}
#[test]
fn test_property_units() {
let result =
property_conversion_test(r#"export component Test { in property <length> test1; }"#, 0);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.default_selection, 0);
assert_eq!(result.value_int, 0);
assert_eq!(result.visual_items.row_data(result.value_int as usize), Some("px".into()));
let length_row_count = result.visual_items.row_count();
assert!(length_row_count > 2);
let result = property_conversion_test(
r#"export component Test { in property <duration> test1: 25s; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, 25.0);
assert_eq!(result.default_selection, 0);
assert_eq!(result.visual_items.row_data(result.value_int as usize), Some("s".into()));
assert_eq!(result.visual_items.row_count(), 2); // ms, s
let result = property_conversion_test(
r#"export component Test { in property <physical-length> test1: 1.5phx; }"#,
1,
);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, 1.5);
assert_eq!(result.default_selection, 0);
assert_eq!(result.visual_items.row_data(result.value_int as usize), Some("phx".into()));
assert!(result.visual_items.row_count() > 1); // More than just physical length
let result = property_conversion_test(
r#"export component Test { in property <angle> test1: 1.5turns + 1.3deg; }"#,
0,
);
assert_eq!(result.kind, PropertyValueKind::Code);
}
#[test]
fn test_property_with_default_values() {
let source = r#"
import { Button } from "std-widgets.slint";
component MyButton inherits Button {
text: "Ok";
in property <color> color: red;
in property alias <=> self.xxx;
property <length> xxx: 45cm;
}
export component X {
MyButton {
/*CURSOR*/
}
}
"#;
let (dc, url, _diag) = loaded_document_cache(source.to_string());
let element = dc
.element_at_offset(&url, (source.find("/*CURSOR*/").expect("cursor") as u32).into())
.unwrap();
let pi = super::properties::get_properties(&element, super::properties::LayoutKind::None);
let prop = pi.iter().find(|pi| pi.name == "visible").unwrap();
let result = super::simplify_value(prop);
assert_eq!(result.kind, PropertyValueKind::Boolean);
assert!(result.value_bool);
let prop = pi.iter().find(|pi| pi.name == "enabled").unwrap();
let result = super::simplify_value(prop);
assert_eq!(result.kind, PropertyValueKind::Boolean);
assert!(result.value_bool);
let prop = pi.iter().find(|pi| pi.name == "text").unwrap();
let result = super::simplify_value(prop);
assert_eq!(result.kind, PropertyValueKind::String);
assert_eq!(result.value_string, "Ok");
let prop = pi.iter().find(|pi| pi.name == "alias").unwrap();
let result = super::simplify_value(prop);
assert_eq!(result.kind, PropertyValueKind::Float);
assert_eq!(result.value_float, 45.);
assert_eq!(result.visual_items.row_data(result.value_int as usize).unwrap(), "cm");
let prop = pi.iter().find(|pi| pi.name == "color").unwrap();
let result = super::simplify_value(prop);
assert_eq!(result.kind, PropertyValueKind::Color);
assert_eq!(
result.value_brush,
slint::Brush::SolidColor(slint::Color::from_rgb_u8(255, 0, 0))
);
}
#[test]
fn test_property_with_default_values_loop() {
let source = r#"
component Abc {
// This should be an error, not a infinite loop/hang
in property <length> some_loop <=> r.border-width;
r:= Rectangle {
property <length> some_loop <=> root.some_loop;
border-width <=> some_loop;
}
}
export component X {
Abc {
/*CURSOR*/
}
}
"#;
let (dc, url, _diag) = loaded_document_cache(source.to_string());
let element = dc
.element_at_offset(&url, (source.find("/*CURSOR*/").expect("cursor") as u32).into())
.unwrap();
let pi = super::properties::get_properties(&element, super::properties::LayoutKind::None);
let prop = pi.iter().find(|pi| pi.name == "visible").unwrap();
let result = super::simplify_value(prop);
assert_eq!(result.kind, PropertyValueKind::Boolean);
assert!(result.value_bool);
}
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 = slint::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 = slint::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(&current, &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::PreviewData {
let component_instance = crate::preview::test::interpret_test(
"fluent",
&format!(
r#"
{type_def}
export component Tester {{
{visibility} property <{type_name}> test: {code};
}}
"#
),
);
let preview_data =
preview_data::query_preview_data_properties_and_callbacks(&component_instance);
return preview_data.get(&preview_data::PropertyContainer::Main).unwrap()[0].clone();
}
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(
visibility: &str,
type_def: &str,
type_name: &str,
code: &str,
expected_data: super::PreviewData,
expected_value: super::PropertyValue,
) {
let raw_data = generate_preview_data(visibility, type_def, type_name, code);
let rp = super::map_preview_data_property(&raw_data).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);
let pv = super::map_preview_data_to_property_value(&raw_data).unwrap();
compare_pv(&pv, &expected_value);
}
#[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 {
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 {
code: "100".into(),
kind: super::PropertyValueKind::Float,
value_float: 100.0,
value_string: "100px".into(),
visual_items: std::rc::Rc::new(slint::VecModel::from(vec!["px".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 {
code: "378".into(),
kind: super::PropertyValueKind::Float,
value_float: 378.0,
value_string: "378px".into(),
visual_items: std::rc::Rc::new(slint::VecModel::from(vec!["px".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 {
code: "100000".into(),
kind: super::PropertyValueKind::Float,
value_float: 100000.0,
value_string: "100000ms".into(),
visual_items: std::rc::Rc::new(slint::VecModel::from(vec!["ms".into()])).into(),
default_selection: 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 {
code: "36000".into(),
kind: super::PropertyValueKind::Float,
value_float: 36000.0,
value_string: "36000deg".into(),
visual_items: std::rc::Rc::new(slint::VecModel::from(vec!["deg".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 {
code: "10".into(),
kind: super::PropertyValueKind::Float,
value_float: 10.0,
value_string: "10%".into(),
visual_items: std::rc::Rc::new(slint::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 {
code: "\"#aabbccff\"".into(),
kind: super::PropertyValueKind::Color,
value_string: "#aabbccff".into(),
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 {
code: "12".into(),
kind: super::PropertyValueKind::Integer,
value_string: "12".into(),
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 {
code: "true".into(),
kind: super::PropertyValueKind::Boolean,
value_string: "true".into(),
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,
..Default::default()
},
super::PropertyValue {
code: "false".into(),
kind: super::PropertyValueKind::Boolean,
value_string: "false".into(),
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,
..Default::default()
},
super::PropertyValue {
kind: super::PropertyValueKind::Code,
code:
"{\n \"first\": [\n \"first of a kind\",\n \"second of a kind\"\n ]\n}"
.into(),
..Default::default()
},
);
}
}