From 769d4390638e9ec4aeba727c5b84eb44a9f3c12d Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 13 Nov 2025 14:00:53 +0100 Subject: [PATCH 01/20] Add markdown macro and parse/escape functions --- internal/compiler/builtin_macros.rs | 25 +++++++++++++++++++ internal/compiler/expression_tree.rs | 10 +++++++- .../llr/optim_passes/inline_expressions.rs | 1 + 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index 03f6fa3115e..2e88243afe2 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -87,6 +87,7 @@ pub fn lower_macro( } BuiltinMacroFunction::Rgb => rgb_macro(n, sub_expr.collect(), diag), BuiltinMacroFunction::Hsv => hsv_macro(n, sub_expr.collect(), diag), + BuiltinMacroFunction::Markdown => markdown_macro(n, sub_expr.collect()), } } @@ -319,6 +320,30 @@ fn debug_macro( } } +fn markdown_macro(node: &dyn Spanned, args: Vec<(Expression, Option)>) -> Expression { + let mut string = None; + for (expr, node) in args { + let escaped = Expression::FunctionCall { + function: BuiltinFunction::EscapeMarkdown.into(), + arguments: vec![expr], + source_location: Some(node.to_source_location()), + }; + string = Some(match string { + None => escaped, + Some(string) => Expression::BinaryExpression { + lhs: Box::new(string), + op: '+', + rhs: Box::new(escaped), + }, + }); + } + Expression::FunctionCall { + function: BuiltinFunction::ParseMarkdown.into(), + arguments: vec![string.unwrap_or_else(|| Expression::default_value_for_type(&Type::String))], + source_location: Some(node.to_source_location()), + } +} + fn to_debug_string( expr: Expression, node: &dyn Spanned, diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 3cda7bd4bae..b88e6b264e1 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -112,6 +112,8 @@ pub enum BuiltinFunction { StopTimer, RestartTimer, OpenUrl, + ParseMarkdown, + EscapeMarkdown, } #[derive(Debug, Clone)] @@ -140,6 +142,8 @@ pub enum BuiltinMacroFunction { Hsv, /// transform `debug(a, b, c)` into debug `a + " " + b + " " + c` Debug, + /// Markdown + Markdown, } macro_rules! declare_builtin_function_types { @@ -277,7 +281,9 @@ declare_builtin_function_types!( StartTimer: (Type::ElementReference) -> Type::Void, StopTimer: (Type::ElementReference) -> Type::Void, RestartTimer: (Type::ElementReference) -> Type::Void, - OpenUrl: (Type::String) -> Type::Void + OpenUrl: (Type::String) -> Type::Void, + EscapeMarkdown: (Type::String) -> Type::String, + ParseMarkdown: (Type::String) -> Type::Void ); impl BuiltinFunction { @@ -374,6 +380,7 @@ impl BuiltinFunction { BuiltinFunction::StopTimer => false, BuiltinFunction::RestartTimer => false, BuiltinFunction::OpenUrl => false, + BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => true, } } @@ -452,6 +459,7 @@ impl BuiltinFunction { BuiltinFunction::StopTimer => false, BuiltinFunction::RestartTimer => false, BuiltinFunction::OpenUrl => false, + BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => true, } } } diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index 2e0c6343737..01aae39e56c 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -156,6 +156,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::StopTimer => 10, BuiltinFunction::RestartTimer => 10, BuiltinFunction::OpenUrl => isize::MAX, + BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => isize::MAX, } } From 866dcf11a7d75021e70892cd11e03a3596c5489e Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 13 Nov 2025 14:43:19 +0100 Subject: [PATCH 02/20] Markdown stuff --- internal/compiler/builtin_macros.rs | 25 ----- internal/compiler/expression_tree.rs | 4 +- internal/compiler/generator/cpp.rs | 8 ++ internal/compiler/generator/rust.rs | 8 ++ internal/compiler/parser.rs | 1 + internal/compiler/parser/expressions.rs | 34 +++++++ internal/compiler/passes/resolving.rs | 121 ++++++++++++++++++++++++ internal/core/lib.rs | 10 ++ internal/interpreter/eval.rs | 10 ++ 9 files changed, 193 insertions(+), 28 deletions(-) diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index 2e88243afe2..03f6fa3115e 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -87,7 +87,6 @@ pub fn lower_macro( } BuiltinMacroFunction::Rgb => rgb_macro(n, sub_expr.collect(), diag), BuiltinMacroFunction::Hsv => hsv_macro(n, sub_expr.collect(), diag), - BuiltinMacroFunction::Markdown => markdown_macro(n, sub_expr.collect()), } } @@ -320,30 +319,6 @@ fn debug_macro( } } -fn markdown_macro(node: &dyn Spanned, args: Vec<(Expression, Option)>) -> Expression { - let mut string = None; - for (expr, node) in args { - let escaped = Expression::FunctionCall { - function: BuiltinFunction::EscapeMarkdown.into(), - arguments: vec![expr], - source_location: Some(node.to_source_location()), - }; - string = Some(match string { - None => escaped, - Some(string) => Expression::BinaryExpression { - lhs: Box::new(string), - op: '+', - rhs: Box::new(escaped), - }, - }); - } - Expression::FunctionCall { - function: BuiltinFunction::ParseMarkdown.into(), - arguments: vec![string.unwrap_or_else(|| Expression::default_value_for_type(&Type::String))], - source_location: Some(node.to_source_location()), - } -} - fn to_debug_string( expr: Expression, node: &dyn Spanned, diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index b88e6b264e1..fb6235f4c0f 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -142,8 +142,6 @@ pub enum BuiltinMacroFunction { Hsv, /// transform `debug(a, b, c)` into debug `a + " " + b + " " + c` Debug, - /// Markdown - Markdown, } macro_rules! declare_builtin_function_types { @@ -283,7 +281,7 @@ declare_builtin_function_types!( RestartTimer: (Type::ElementReference) -> Type::Void, OpenUrl: (Type::String) -> Type::Void, EscapeMarkdown: (Type::String) -> Type::String, - ParseMarkdown: (Type::String) -> Type::Void + ParseMarkdown: (Type::String) -> Type::String ); impl BuiltinFunction { diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 5dff6551d89..4543085dc02 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -4211,6 +4211,14 @@ fn compile_builtin_function_call( let url = a.next().unwrap(); format!("slint::cbindgen_private::open_url({})", url) } + BuiltinFunction::EscapeMarkdown => { + let text = a.next().unwrap(); + format!("slint::cbindgen_private::escape_markdown({})", text) + } + BuiltinFunction::ParseMarkdown => { + let text = a.next().unwrap(); + format!("slint::cbindgen_private::parse_markdown({})", text) + } } } diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index dd73a026c79..bbd326fffe6 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -3397,6 +3397,14 @@ fn compile_builtin_function_call( let url = a.next().unwrap(); quote!(sp::open_url(&#url)) } + BuiltinFunction::EscapeMarkdown => { + let text = a.next().unwrap(); + quote!(sp::escape_markdown(&#text)) + } + BuiltinFunction::ParseMarkdown => { + let text = a.next().unwrap(); + quote!(sp::parse_markdown(&#text)) + } } } diff --git a/internal/compiler/parser.rs b/internal/compiler/parser.rs index 42138b6058f..3cb3a9a37e8 100644 --- a/internal/compiler/parser.rs +++ b/internal/compiler/parser.rs @@ -383,6 +383,7 @@ declare_syntax! { AtGradient -> [*Expression], /// `@tr("foo", ...)` // the string is a StringLiteral AtTr -> [?TrContext, ?TrPlural, *Expression], + AtMarkdown -> [*Expression], /// `"foo" =>` in a `AtTr` node TrContext -> [], /// `| "foo" % n` in a `AtTr` node diff --git a/internal/compiler/parser/expressions.rs b/internal/compiler/parser/expressions.rs index 0d15afdebfc..3f96331631c 100644 --- a/internal/compiler/parser/expressions.rs +++ b/internal/compiler/parser/expressions.rs @@ -255,6 +255,9 @@ fn parse_at_keyword(p: &mut impl Parser) { "tr" => { parse_tr(p); } + "markdown" => { + parse_markdown(p); + }, _ => { p.consume(); p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works @@ -438,6 +441,37 @@ fn parse_tr(p: &mut impl Parser) { p.expect(SyntaxKind::RParent); } +fn parse_markdown(p: &mut impl Parser) { + let mut p = p.start_node(SyntaxKind::AtMarkdown); + p.expect(SyntaxKind::At); + debug_assert!(p.peek().as_str().ends_with("markdown")); + p.expect(SyntaxKind::Identifier); //eg "markdown" + p.expect(SyntaxKind::LParent); + + fn consume_literal(p: &mut impl Parser) -> bool { + let peek = p.peek(); + if peek.kind() != SyntaxKind::StringLiteral + || !peek.as_str().starts_with('"') + || !peek.as_str().ends_with('"') + { + p.error("Expected plain string literal"); + return false; + } + p.expect(SyntaxKind::StringLiteral) + } + + if !consume_literal(&mut *p) { + return; + } + + while p.test(SyntaxKind::Comma) { + if !parse_expression(&mut *p) { + break; + } + } + p.expect(SyntaxKind::RParent); +} + #[cfg_attr(test, parser_test)] /// ```test,AtImageUrl /// @image-url("foo.png") diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 6bd20804c61..ffc6d75d04b 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -359,6 +359,7 @@ impl Expression { SyntaxKind::AtImageUrl => Some(Self::from_at_image_url_node(node.into(), ctx)), SyntaxKind::AtGradient => Some(Self::from_at_gradient(node.into(), ctx)), SyntaxKind::AtTr => Some(Self::from_at_tr(node.into(), ctx)), + SyntaxKind::AtMarkdown => Some(Self::from_at_markdown(node.into(), ctx)), SyntaxKind::QualifiedName => Some(Self::from_qualified_name_node( node.clone().into(), ctx, @@ -744,6 +745,126 @@ impl Expression { } } } + + fn from_at_markdown(node: syntax_nodes::AtMarkdown, ctx: &mut LookupCtx) -> Expression { + let Some(string) = std::dbg!(node + .child_text(SyntaxKind::StringLiteral)) + .and_then(|s| crate::literals::unescape_string(&s)) + else { + ctx.diag.push_error("Cannot parse string literal".into(), &node); + return Expression::Invalid; + }; + + let subs = node.Expression().map(|n| { + Expression::from_expression_node(n.clone(), ctx).maybe_convert_to( + Type::String, + &n, + ctx.diag, + ) + }); + let values = subs.collect::>(); + std::dbg!(&values); + + let mut expr = None; + + // check format string + { + let mut arg_idx = 0; + let mut pos_max = 0; + let mut pos = 0; + let mut literal_start_pos = 0; + while let Some(mut p) = string[pos..].find(['{', '}']) { + if string.len() - pos < p + 1 { + ctx.diag.push_error( + "Unescaped trailing '{' in format string. Escape '{' with '{{'".into(), + &node, + ); + break; + } + p += pos; + + // Skip escaped } + if string.get(p..=p) == Some("}") { + if string.get(p + 1..=p + 1) == Some("}") { + pos = p + 2; + continue; + } else { + ctx.diag.push_error( + "Unescaped '}' in format string. Escape '}' with '}}'".into(), + &node, + ); + break; + } + } + + // Skip escaped { + if string.get(p + 1..=p + 1) == Some("{") { + pos = p + 2; + continue; + } + + // Find the argument + let end = if let Some(end) = string[p..].find('}') { + end + p + } else { + ctx.diag.push_error( + "Unterminated placeholder in format string. '{' must be escaped with '{{'" + .into(), + &node, + ); + break; + }; + let argument = &string[p + 1..end]; + if argument.is_empty() { + arg_idx += 1; + } else if let Ok(n) = argument.parse::() { + pos_max = pos_max.max(n as usize + 1); + } else { + ctx.diag + .push_error("Invalid '{...}' placeholder in format string. The placeholder must be a number, or braces must be escaped with '{{' and '}}'".into(), &node); + break; + }; + let add = Expression::BinaryExpression { + lhs: Box::new(Expression::StringLiteral((&string[literal_start_pos..p]).into())), + op: '+', + rhs: Box::new(values[arg_idx-1].clone()) + }; + expr = Some(match expr { + None => add, + Some(expr) => Expression::BinaryExpression { + lhs: Box::new(expr), + op: '+', + rhs: Box::new(add), + }, + }); + pos = end + 1; + literal_start_pos = pos; + } + if arg_idx > 0 && pos_max > 0 { + ctx.diag.push_error( + "Cannot mix positional and non-positional placeholder in format string".into(), + &node, + ); + } else if arg_idx > values.len() || pos_max > values.len() { + let num = arg_idx.max(pos_max); + ctx.diag.push_error( + format!("Format string contains {num} placeholders, but only {} extra arguments were given", values.len()), + &node, + ); + } + } + + std::dbg!(&expr); + + Expression::FunctionCall { + function: BuiltinFunction::ParseMarkdown.into(), + arguments: vec![ + expr.unwrap()//Expression::StringLiteral(string), + + ], + source_location: Some(node.to_source_location()), + } + } fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression { let Some(string) = node diff --git a/internal/core/lib.rs b/internal/core/lib.rs index f6039c0ab5a..0c86b7f43ca 100644 --- a/internal/core/lib.rs +++ b/internal/core/lib.rs @@ -184,3 +184,13 @@ pub fn open_url(url: &str) { debug_log!("Error opening url {}: {}", url, err); } } + +pub fn escape_markdown(text: &str) -> std::string::String { + std::dbg!(text); + text.into() +} + +pub fn parse_markdown(text: &str) -> std::string::String { + std::dbg!(text); + text.into() +} diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 74e5496d702..92fff6f9b1a 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -1400,6 +1400,16 @@ fn call_builtin_function( corelib::open_url(&url); Value::Void } + BuiltinFunction::EscapeMarkdown => { + let text: SharedString = + eval_expression(&arguments[0], local_context).try_into().unwrap(); + Value::String(corelib::escape_markdown(&text).into()) + } + BuiltinFunction::ParseMarkdown => { + let text: SharedString = + eval_expression(&arguments[0], local_context).try_into().unwrap(); + Value::String(corelib::parse_markdown(&text).into()) + } } } From 374e2b690a452d6489a45312cb4f75bccd38533f Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 13 Nov 2025 14:43:19 +0100 Subject: [PATCH 03/20] Implement escape markdown, handle trailing literal sections --- internal/compiler/parser/expressions.rs | 4 +-- internal/compiler/passes/resolving.rs | 44 +++++++++++++++++-------- internal/core/lib.rs | 21 +++++++++--- internal/interpreter/eval.rs | 4 +-- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/internal/compiler/parser/expressions.rs b/internal/compiler/parser/expressions.rs index 3f96331631c..350c4f3e295 100644 --- a/internal/compiler/parser/expressions.rs +++ b/internal/compiler/parser/expressions.rs @@ -257,7 +257,7 @@ fn parse_at_keyword(p: &mut impl Parser) { } "markdown" => { parse_markdown(p); - }, + } _ => { p.consume(); p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works @@ -447,7 +447,7 @@ fn parse_markdown(p: &mut impl Parser) { debug_assert!(p.peek().as_str().ends_with("markdown")); p.expect(SyntaxKind::Identifier); //eg "markdown" p.expect(SyntaxKind::LParent); - + fn consume_literal(p: &mut impl Parser) -> bool { let peek = p.peek(); if peek.kind() != SyntaxKind::StringLiteral diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index ffc6d75d04b..864d6539695 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -745,16 +745,16 @@ impl Expression { } } } - + fn from_at_markdown(node: syntax_nodes::AtMarkdown, ctx: &mut LookupCtx) -> Expression { - let Some(string) = std::dbg!(node - .child_text(SyntaxKind::StringLiteral)) + let Some(string) = node + .child_text(SyntaxKind::StringLiteral) .and_then(|s| crate::literals::unescape_string(&s)) else { ctx.diag.push_error("Cannot parse string literal".into(), &node); return Expression::Invalid; }; - + let subs = node.Expression().map(|n| { Expression::from_expression_node(n.clone(), ctx).maybe_convert_to( Type::String, @@ -763,10 +763,9 @@ impl Expression { ) }); let values = subs.collect::>(); - std::dbg!(&values); let mut expr = None; - + // check format string { let mut arg_idx = 0; @@ -815,19 +814,28 @@ impl Expression { break; }; let argument = &string[p + 1..end]; - if argument.is_empty() { + let argument_index = if argument.is_empty() { + let argument_index = arg_idx; arg_idx += 1; + argument_index } else if let Ok(n) = argument.parse::() { pos_max = pos_max.max(n as usize + 1); + n as usize } else { ctx.diag .push_error("Invalid '{...}' placeholder in format string. The placeholder must be a number, or braces must be escaped with '{{' and '}}'".into(), &node); break; }; let add = Expression::BinaryExpression { - lhs: Box::new(Expression::StringLiteral((&string[literal_start_pos..p]).into())), + lhs: Box::new(Expression::StringLiteral( + (&string[literal_start_pos..p]).into(), + )), op: '+', - rhs: Box::new(values[arg_idx-1].clone()) + rhs: Box::new(Expression::FunctionCall { + function: BuiltinFunction::EscapeMarkdown.into(), + arguments: vec![values[argument_index].clone()], + source_location: Some(node.to_source_location()), + }), }; expr = Some(match expr { None => add, @@ -840,6 +848,17 @@ impl Expression { pos = end + 1; literal_start_pos = pos; } + { + let trailing = Expression::StringLiteral((&string[literal_start_pos..]).into()); + expr = Some(match expr { + None => trailing, + Some(expr) => Expression::BinaryExpression { + lhs: Box::new(expr), + op: '+', + rhs: Box::new(trailing), + }, + }); + } if arg_idx > 0 && pos_max > 0 { ctx.diag.push_error( "Cannot mix positional and non-positional placeholder in format string".into(), @@ -851,16 +870,13 @@ impl Expression { format!("Format string contains {num} placeholders, but only {} extra arguments were given", values.len()), &node, ); - } + } } - - std::dbg!(&expr); Expression::FunctionCall { function: BuiltinFunction::ParseMarkdown.into(), arguments: vec![ - expr.unwrap()//Expression::StringLiteral(string), - + expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)) ], source_location: Some(node.to_source_location()), } diff --git a/internal/core/lib.rs b/internal/core/lib.rs index 0c86b7f43ca..51b3bc520a9 100644 --- a/internal/core/lib.rs +++ b/internal/core/lib.rs @@ -185,12 +185,25 @@ pub fn open_url(url: &str) { } } -pub fn escape_markdown(text: &str) -> std::string::String { - std::dbg!(text); - text.into() +pub fn escape_markdown(text: &str) -> std::string::String { + let mut out = std::string::String::with_capacity(text.len()); + + for c in text.chars() { + match c { + '*' => out.push_str("\\*"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '_' => out.push_str("\\_"), + '#' => out.push_str("\\#"), + '-' => out.push_str("\\-"), + '`' => out.push_str("\\`"), + _ => out.push(c), + } + } + + out } pub fn parse_markdown(text: &str) -> std::string::String { - std::dbg!(text); text.into() } diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 92fff6f9b1a..581823e27e9 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -1403,12 +1403,12 @@ fn call_builtin_function( BuiltinFunction::EscapeMarkdown => { let text: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); - Value::String(corelib::escape_markdown(&text).into()) + Value::String(corelib::escape_markdown(&text).into()) } BuiltinFunction::ParseMarkdown => { let text: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); - Value::String(corelib::parse_markdown(&text).into()) + Value::String(corelib::parse_markdown(&text).into()) } } } From 4923354a65fc9ab3a8cd6c662dbf82c0fb7ee72d Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 13 Nov 2025 14:43:19 +0100 Subject: [PATCH 04/20] add StyledText Type and Value --- internal/compiler/builtin_macros.rs | 1 + internal/compiler/expression_tree.rs | 3 ++- internal/compiler/langtype.rs | 6 ++++++ internal/compiler/llr/expression.rs | 1 + internal/interpreter/api.rs | 6 ++++++ internal/interpreter/dynamic_item_tree.rs | 2 +- internal/interpreter/eval.rs | 2 ++ 7 files changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index 03f6fa3115e..db4c7a6e981 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -420,6 +420,7 @@ fn to_debug_string( } } Type::Enumeration(_) => Expression::Cast { from: Box::new(expr), to: (Type::String) }, + Type::StyledText => Expression::Invalid, } } diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index fb6235f4c0f..9deb604c044 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -281,7 +281,7 @@ declare_builtin_function_types!( RestartTimer: (Type::ElementReference) -> Type::Void, OpenUrl: (Type::String) -> Type::Void, EscapeMarkdown: (Type::String) -> Type::String, - ParseMarkdown: (Type::String) -> Type::String + ParseMarkdown: (Type::String) -> Type::StyledText ); impl BuiltinFunction { @@ -1434,6 +1434,7 @@ impl Expression { Expression::EnumerationValue(enumeration.clone().default_value()) } Type::ComponentFactory => Expression::EmptyComponentFactory, + Type::StyledText => Expression::Invalid, } } diff --git a/internal/compiler/langtype.rs b/internal/compiler/langtype.rs index dce34556eb2..edc72eac967 100644 --- a/internal/compiler/langtype.rs +++ b/internal/compiler/langtype.rs @@ -64,6 +64,8 @@ pub enum Type { /// This is a `SharedArray` LayoutCache, + + StyledText, } impl core::cmp::PartialEq for Type { @@ -104,6 +106,7 @@ impl core::cmp::PartialEq for Type { Type::UnitProduct(a) => matches!(other, Type::UnitProduct(b) if a == b), Type::ElementReference => matches!(other, Type::ElementReference), Type::LayoutCache => matches!(other, Type::LayoutCache), + Type::StyledText => matches!(other, Type::StyledText), } } } @@ -178,6 +181,7 @@ impl Display for Type { } Type::ElementReference => write!(f, "element ref"), Type::LayoutCache => write!(f, "layout cache"), + Type::StyledText => write!(f, "styled-text"), } } } @@ -213,6 +217,7 @@ impl Type { | Self::Array(_) | Self::Brush | Self::InferredProperty + | Self::StyledText ) } @@ -314,6 +319,7 @@ impl Type { Type::UnitProduct(_) => None, Type::ElementReference => None, Type::LayoutCache => None, + Type::StyledText => None, } } diff --git a/internal/compiler/llr/expression.rs b/internal/compiler/llr/expression.rs index c131c061e5f..5c2a6f8da01 100644 --- a/internal/compiler/llr/expression.rs +++ b/internal/compiler/llr/expression.rs @@ -267,6 +267,7 @@ impl Expression { Expression::EnumerationValue(enumeration.clone().default_value()) } Type::ComponentFactory => Expression::EmptyComponentFactory, + Type::StyledText => return None, }) } diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 1f675529b8b..1536829c015 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -51,6 +51,7 @@ pub enum ValueType { Brush, /// Correspond to `image` type in .slint. Image, + StyledText, /// The type is not a public type but something internal. #[doc(hidden)] Other = -1, @@ -128,6 +129,7 @@ pub enum Value { #[doc(hidden)] /// Correspond to the `component-factory` type in .slint ComponentFactory(ComponentFactory) = 12, + StyledText(()) = 13, } impl Value { @@ -173,6 +175,9 @@ impl PartialEq for Value { Value::ComponentFactory(lhs) => { matches!(other, Value::ComponentFactory(rhs) if lhs == rhs) } + Value::StyledText(lhs) => { + matches!(other, Value::StyledText(rhs) if lhs == rhs) + } } } } @@ -197,6 +202,7 @@ impl std::fmt::Debug for Value { Value::EnumerationValue(n, v) => write!(f, "Value::EnumerationValue({n:?}, {v:?})"), Value::LayoutCache(v) => write!(f, "Value::LayoutCache({v:?})"), Value::ComponentFactory(factory) => write!(f, "Value::ComponentFactory({factory:?})"), + Value::StyledText(text) => write!(f, "Value::StyledText({text:?})"), } } } diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index 8fc92a0ab5d..460e8ac0177 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -1254,7 +1254,7 @@ pub(crate) fn generate_item_tree<'id>( } Type::LayoutCache => property_info::>(), Type::Function { .. } | Type::Callback { .. } => return None, - + Type::StyledText => property_info::(), // These can't be used in properties Type::Invalid | Type::Void diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 581823e27e9..87146198a9f 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -1731,6 +1731,7 @@ fn check_value_type(value: &Value, ty: &Type) -> bool { } Type::LayoutCache => matches!(value, Value::LayoutCache(_)), Type::ComponentFactory => matches!(value, Value::ComponentFactory(_)), + Type::StyledText => matches!(value, Value::StyledText(_)), } } @@ -2031,6 +2032,7 @@ pub fn default_value_for_type(ty: &Type) -> Value { | Type::Function { .. } => { panic!("There can't be such property") } + Type::StyledText => Value::StyledText(Default::default()), } } From bb2a9270d31fbda2c7d0a51d3f84a1bb80f4b72a Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Thu, 13 Nov 2025 14:43:19 +0100 Subject: [PATCH 05/20] Implement more things --- internal/compiler/builtin_macros.rs | 8 ++++++-- internal/compiler/builtins.slint | 5 ++++- internal/compiler/generator/rust.rs | 1 + internal/compiler/typeregister.rs | 1 + internal/core/api.rs | 5 +++++ internal/interpreter/api.rs | 3 ++- internal/interpreter/dynamic_item_tree.rs | 4 ++-- 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index db4c7a6e981..392e39c5d72 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -343,7 +343,12 @@ fn to_debug_string( Type::Float32 | Type::Int32 => expr.maybe_convert_to(Type::String, node, diag), Type::String => expr, // TODO - Type::Color | Type::Brush | Type::Image | Type::Easing | Type::Array(_) => { + Type::Color + | Type::Brush + | Type::Image + | Type::Easing + | Type::StyledText + | Type::Array(_) => { Expression::StringLiteral("".into()) } Type::Duration @@ -420,7 +425,6 @@ fn to_debug_string( } } Type::Enumeration(_) => Expression::Cast { from: Box::new(expr), to: (Type::String) }, - Type::StyledText => Expression::Invalid, } } diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index 8b5fd1fe2ee..e8752347d30 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -131,7 +131,6 @@ export component MarkdownText inherits Empty { in property vertical-alignment; callback link-clicked(link: string); in property link-color: #00f; - in property font-family; in property font-italic; in property overflow; @@ -143,6 +142,10 @@ export component MarkdownText inherits Empty { //-default_size_binding:implicit_size } +export component StyledText inherits Empty { + in property text; +} + export component TouchArea { in property enabled: true; out property pressed; diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index bbd326fffe6..bcc7bbc5999 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -91,6 +91,7 @@ pub fn rust_primitive_type(ty: &Type) -> Option { Type::Percent => Some(quote!(f32)), Type::Bool => Some(quote!(bool)), Type::Image => Some(quote!(sp::Image)), + Type::StyledText => Some(quote!(sp::StyledText)), Type::Struct(s) => { if let Some(name) = &s.name { Some(struct_name_to_tokens(name)) diff --git a/internal/compiler/typeregister.rs b/internal/compiler/typeregister.rs index b304ff99706..43567ba254e 100644 --- a/internal/compiler/typeregister.rs +++ b/internal/compiler/typeregister.rs @@ -402,6 +402,7 @@ impl TypeRegister { register.insert_type(Type::Angle); register.insert_type(Type::Brush); register.insert_type(Type::Rem); + register.insert_type(Type::StyledText); register.types.insert("Point".into(), logical_point_type().into()); BUILTIN.with(|e| e.enums.fill_register(&mut register)); diff --git a/internal/core/api.rs b/internal/core/api.rs index 88066bb90a1..5cddc9e845b 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1219,3 +1219,8 @@ pub fn set_xdg_app_id(app_id: impl Into) -> Result<(), PlatformErr |ctx| ctx.set_xdg_app_id(app_id.into()), ) } + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct StyledText { + inner: (), +} diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 1536829c015..103e7e14bfa 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -129,7 +129,7 @@ pub enum Value { #[doc(hidden)] /// Correspond to the `component-factory` type in .slint ComponentFactory(ComponentFactory) = 12, - StyledText(()) = 13, + StyledText(StyledText) = 13, } impl Value { @@ -245,6 +245,7 @@ declare_value_conversion!(PathData => [PathData]); declare_value_conversion!(EasingCurve => [i_slint_core::animations::EasingCurve]); declare_value_conversion!(LayoutCache => [SharedVector] ); declare_value_conversion!(ComponentFactory => [ComponentFactory] ); +declare_value_conversion!(StyledText => [StyledText] ); /// Implement From / TryFrom for Value that convert a `struct` to/from `Value::Struct` macro_rules! declare_value_struct_conversion { diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index 460e8ac0177..6e644fffb07 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -14,7 +14,7 @@ use i_slint_compiler::{generator, object_tree, parser, CompilerConfiguration}; use i_slint_core::accessibility::{ AccessibilityAction, AccessibleStringProperty, SupportedAccessibilityAction, }; -use i_slint_core::api::LogicalPosition; +use i_slint_core::api::{LogicalPosition, StyledText}; use i_slint_core::component_factory::ComponentFactory; use i_slint_core::item_tree::{ IndexRange, ItemRc, ItemTree, ItemTreeNode, ItemTreeRef, ItemTreeRefPin, ItemTreeVTable, @@ -1254,7 +1254,7 @@ pub(crate) fn generate_item_tree<'id>( } Type::LayoutCache => property_info::>(), Type::Function { .. } | Type::Callback { .. } => return None, - Type::StyledText => property_info::(), + Type::StyledText => property_info::(), // These can't be used in properties Type::Invalid | Type::Void From 3c72c1c44940aaf2dea6ef6fd4b41b9fc7d46b34 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:41:57 +0000 Subject: [PATCH 06/20] [autofix.ci] apply automated fixes --- internal/compiler/passes/resolving.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 864d6539695..89bb9ff7f11 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -876,7 +876,7 @@ impl Expression { Expression::FunctionCall { function: BuiltinFunction::ParseMarkdown.into(), arguments: vec![ - expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)) + expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)), ], source_location: Some(node.to_source_location()), } From f3242c88913b18b33e9f813193ab58d778c5db6d Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Mon, 17 Nov 2025 13:08:26 +0100 Subject: [PATCH 07/20] Rename MarkdownText to StyledText --- api/cpp/cbindgen.rs | 2 +- internal/compiler/builtins.slint | 8 ++----- .../apply_default_properties_from_style.rs | 2 +- internal/compiler/passes/embed_glyphs.rs | 2 +- internal/compiler/typeregister.rs | 2 +- internal/core/items.rs | 2 +- internal/core/items/text.rs | 23 ++++++++++--------- internal/core/rtti.rs | 1 + internal/interpreter/api.rs | 10 ++++---- internal/interpreter/dynamic_item_tree.rs | 2 +- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index eb3e3d559c2..be144e14717 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -305,7 +305,7 @@ fn gen_corelib( "Flickable", "SimpleText", "ComplexText", - "MarkdownText", + "StyledText", "Path", "WindowItem", "TextInput", diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index e8752347d30..84c624ae521 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -120,10 +120,10 @@ component ComplexText inherits SimpleText { export { ComplexText as Text } -export component MarkdownText inherits Empty { +export component StyledText inherits Empty { in property width; in property height; - in property text; + in property text; in property font-size; in property font-weight; in property color; @@ -142,10 +142,6 @@ export component MarkdownText inherits Empty { //-default_size_binding:implicit_size } -export component StyledText inherits Empty { - in property text; -} - export component TouchArea { in property enabled: true; out property pressed; diff --git a/internal/compiler/passes/apply_default_properties_from_style.rs b/internal/compiler/passes/apply_default_properties_from_style.rs index 6595f7374f4..725e323c8cb 100644 --- a/internal/compiler/passes/apply_default_properties_from_style.rs +++ b/internal/compiler/passes/apply_default_properties_from_style.rs @@ -62,7 +62,7 @@ pub fn apply_default_properties_from_style( } }); } - "Text" | "MarkdownText" => { + "Text" | "StyledText" => { elem.set_binding_if_not_set("color".into(), || Expression::Cast { from: Expression::PropertyReference(NamedReference::new( &palette.root_element, diff --git a/internal/compiler/passes/embed_glyphs.rs b/internal/compiler/passes/embed_glyphs.rs index 0fda26c25a2..8d695401da1 100644 --- a/internal/compiler/passes/embed_glyphs.rs +++ b/internal/compiler/passes/embed_glyphs.rs @@ -546,7 +546,7 @@ pub fn collect_font_sizes_used( .to_string() .as_str() { - "TextInput" | "Text" | "SimpleText" | "ComplexText" | "MarkdownText" => { + "TextInput" | "Text" | "SimpleText" | "ComplexText" | "StyledText" => { if let Some(font_size) = try_extract_font_size_from_element(elem, "font-size") { add_font_size(font_size) } diff --git a/internal/compiler/typeregister.rs b/internal/compiler/typeregister.rs index 43567ba254e..3c573d5d9ac 100644 --- a/internal/compiler/typeregister.rs +++ b/internal/compiler/typeregister.rs @@ -587,7 +587,7 @@ impl TypeRegister { register.elements.remove("DropArea").unwrap(); register.types.remove("DropEvent").unwrap(); // Also removed in xtask/src/slintdocs.rs - register.elements.remove("MarkdownText").unwrap(); + register.elements.remove("StyledText").unwrap(); Rc::new(RefCell::new(register)) } diff --git a/internal/core/items.rs b/internal/core/items.rs index d55873c6cda..28d21584971 100644 --- a/internal/core/items.rs +++ b/internal/core/items.rs @@ -1729,7 +1729,7 @@ declare_item_vtable! { } declare_item_vtable! { - fn slint_get_MarkdownTextVTable() -> MarkdownTextVTable for MarkdownText + fn slint_get_StyledTextVTable() -> StyledTextVTable for StyledText } declare_item_vtable! { diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index 39d9018d879..481b17732b6 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -13,6 +13,7 @@ use super::{ TextHorizontalAlignment, TextOverflow, TextStrokeStyle, TextVerticalAlignment, TextWrap, VoidArg, WindowItem, }; +use crate::api; use crate::graphics::{Brush, Color, FontRequest}; use crate::input::{ FocusEvent, FocusEventResult, FocusReason, InputEventFilterResult, InputEventResult, KeyEvent, @@ -225,10 +226,10 @@ impl ComplexText { #[repr(C)] #[derive(FieldOffsets, Default, SlintElement)] #[pin] -pub struct MarkdownText { +pub struct StyledText { pub width: Property, pub height: Property, - pub text: Property, + pub text: Property, pub font_size: Property, pub font_weight: Property, pub color: Property, @@ -248,7 +249,7 @@ pub struct MarkdownText { pub cached_rendering_data: CachedRenderingData, } -impl Item for MarkdownText { +impl Item for StyledText { fn init(self: Pin<&Self>, _self_rc: &ItemRc) {} fn layout_info( @@ -368,14 +369,14 @@ impl Item for MarkdownText { } } -impl ItemConsts for MarkdownText { +impl ItemConsts for StyledText { const cached_rendering_data_offset: const_field_offset::FieldOffset< - MarkdownText, + StyledText, CachedRenderingData, - > = MarkdownText::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); + > = StyledText::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } -impl HasFont for MarkdownText { +impl HasFont for StyledText { fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest { crate::items::WindowItem::resolved_font_request( self_rc, @@ -388,13 +389,13 @@ impl HasFont for MarkdownText { } } -impl RenderString for MarkdownText { +impl RenderString for StyledText { fn text(self: Pin<&Self>) -> SharedString { - self.text() + panic!() } } -impl RenderText for MarkdownText { +impl RenderText for StyledText { fn target_size(self: Pin<&Self>) -> LogicalSize { LogicalSize::from_lengths(self.width(), self.height()) } @@ -430,7 +431,7 @@ impl RenderText for MarkdownText { } } -impl MarkdownText { +impl StyledText { pub fn font_metrics( self: Pin<&Self>, window_adapter: &Rc, diff --git a/internal/core/rtti.rs b/internal/core/rtti.rs index 7d6fb159250..b9db495a0c3 100644 --- a/internal/core/rtti.rs +++ b/internal/core/rtti.rs @@ -54,6 +54,7 @@ macro_rules! declare_ValueType_2 { crate::items::MenuEntry, crate::items::DropEvent, crate::model::ModelRc, + crate::api::StyledText, $(crate::items::$Name,)* ]; }; diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 103e7e14bfa..9fa386ae65d 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -18,8 +18,10 @@ use std::rc::Rc; #[doc(inline)] pub use i_slint_compiler::diagnostics::{Diagnostic, DiagnosticLevel}; - -pub use i_slint_core::api::*; +pub use i_slint_core::api::{ + self, ComponentHandle, EventLoopError, Global, JoinHandle, PlatformError, SharedString, Weak, + Window, +}; // keep in sync with api/rs/slint/lib.rs pub use i_slint_backend_selector::api::*; pub use i_slint_core::graphics::{ @@ -129,7 +131,7 @@ pub enum Value { #[doc(hidden)] /// Correspond to the `component-factory` type in .slint ComponentFactory(ComponentFactory) = 12, - StyledText(StyledText) = 13, + StyledText(api::StyledText) = 13, } impl Value { @@ -245,7 +247,7 @@ declare_value_conversion!(PathData => [PathData]); declare_value_conversion!(EasingCurve => [i_slint_core::animations::EasingCurve]); declare_value_conversion!(LayoutCache => [SharedVector] ); declare_value_conversion!(ComponentFactory => [ComponentFactory] ); -declare_value_conversion!(StyledText => [StyledText] ); +declare_value_conversion!(StyledText => [api::StyledText] ); /// Implement From / TryFrom for Value that convert a `struct` to/from `Value::Struct` macro_rules! declare_value_struct_conversion { diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index 6e644fffb07..9a1b37e9dc5 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -982,7 +982,7 @@ fn generate_rtti() -> HashMap<&'static str, Rc> { rtti_for::(), rtti_for::(), rtti_for::(), - rtti_for::(), + rtti_for::(), rtti_for::(), rtti_for::(), rtti_for::(), From 91f451758136013cf95dd871e1a26b85f2925ebe Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Mon, 17 Nov 2025 13:24:04 +0100 Subject: [PATCH 08/20] Add comments and solve warnings --- internal/compiler/builtins.slint | 1 + internal/compiler/passes/resolving.rs | 2 +- internal/interpreter/api.rs | 7 +++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index 84c624ae521..d3797ae6a54 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -131,6 +131,7 @@ export component StyledText inherits Empty { in property vertical-alignment; callback link-clicked(link: string); in property link-color: #00f; + in property font-family; in property font-italic; in property overflow; diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 89bb9ff7f11..864d6539695 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -876,7 +876,7 @@ impl Expression { Expression::FunctionCall { function: BuiltinFunction::ParseMarkdown.into(), arguments: vec![ - expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)), + expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)) ], source_location: Some(node.to_source_location()), } diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 9fa386ae65d..877c13a5bac 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -19,11 +19,11 @@ use std::rc::Rc; #[doc(inline)] pub use i_slint_compiler::diagnostics::{Diagnostic, DiagnosticLevel}; pub use i_slint_core::api::{ - self, ComponentHandle, EventLoopError, Global, JoinHandle, PlatformError, SharedString, Weak, - Window, + ComponentHandle, EventLoopError, Global, JoinHandle, PlatformError, SharedString, Weak, Window, }; // keep in sync with api/rs/slint/lib.rs pub use i_slint_backend_selector::api::*; +use i_slint_core::api; pub use i_slint_core::graphics::{ Brush, Color, Image, LoadImageError, Rgb8Pixel, Rgba8Pixel, RgbaColor, SharedPixelBuffer, }; @@ -53,6 +53,7 @@ pub enum ValueType { Brush, /// Correspond to `image` type in .slint. Image, + /// Correspond to `styled-text` type in .slint. StyledText, /// The type is not a public type but something internal. #[doc(hidden)] @@ -78,6 +79,7 @@ impl From for ValueType { LangType::Struct { .. } => Self::Struct, LangType::Void => Self::Void, LangType::Image => Self::Image, + LangType::StyledText => Self::StyledText, _ => Self::Other, } } @@ -131,6 +133,7 @@ pub enum Value { #[doc(hidden)] /// Correspond to the `component-factory` type in .slint ComponentFactory(ComponentFactory) = 12, + /// Correspond to the `styled-text` type in .slint StyledText(api::StyledText) = 13, } From e72f523356e006818139efc1f33b3163400d5833 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:39:02 +0000 Subject: [PATCH 09/20] Apply automated fixes --- internal/compiler/passes/resolving.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 864d6539695..89bb9ff7f11 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -876,7 +876,7 @@ impl Expression { Expression::FunctionCall { function: BuiltinFunction::ParseMarkdown.into(), arguments: vec![ - expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)) + expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)), ], source_location: Some(node.to_source_location()), } From ff8b450f6e9c82f6cdcfebaa4a5a8cd664f4f554 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Mon, 17 Nov 2025 15:03:48 +0100 Subject: [PATCH 10/20] Fill out styled text type --- internal/backends/qt/qt_window.rs | 20 + internal/compiler/expression_tree.rs | 2 +- internal/compiler/passes/resolving.rs | 5 +- internal/core/api.rs | 348 +++++++++++++- internal/core/item_rendering.rs | 8 + internal/core/items/text.rs | 90 ++-- internal/core/lib.rs | 4 +- internal/core/partial_renderer.rs | 5 +- internal/core/renderer.rs | 8 + internal/core/software_renderer.rs | 20 + internal/core/textlayout/sharedparley.rs | 524 ++++++--------------- internal/interpreter/eval.rs | 2 +- internal/renderers/femtovg/itemrenderer.rs | 10 + internal/renderers/femtovg/lib.rs | 10 + 14 files changed, 627 insertions(+), 429 deletions(-) diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs index 9e72475f73d..24192b41bdf 100644 --- a/internal/backends/qt/qt_window.rs +++ b/internal/backends/qt/qt_window.rs @@ -686,6 +686,16 @@ impl ItemRenderer for QtItemRenderer<'_> { sharedparley::draw_text(self, text, Some(self_rc), size); } + fn draw_styled_text( + &mut self, + text: Pin<&i_slint_core::items::StyledText>, + self_rc: &ItemRc, + size: LogicalSize, + _cache: &CachedRenderingData, + ) { + sharedparley::draw_styled_text(self, text, Some(self_rc), size); + } + fn draw_text_input( &mut self, text_input: Pin<&items::TextInput>, @@ -2095,6 +2105,16 @@ impl i_slint_core::renderer::RendererSealed for QtWindow { sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap) } + fn styled_text_size( + &self, + text_item: Pin<&i_slint_core::items::StyledText>, + item_rc: &ItemRc, + max_width: Option, + text_wrap: TextWrap, + ) -> LogicalSize { + sharedparley::styled_text_size(self, text_item, item_rc, max_width, text_wrap) + } + fn char_size( &self, text_item: Pin<&dyn i_slint_core::item_rendering::HasFont>, diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 9deb604c044..bf151483a7d 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -378,7 +378,7 @@ impl BuiltinFunction { BuiltinFunction::StopTimer => false, BuiltinFunction::RestartTimer => false, BuiltinFunction::OpenUrl => false, - BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => true, + BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => false, } } diff --git a/internal/compiler/passes/resolving.rs b/internal/compiler/passes/resolving.rs index 89bb9ff7f11..b60a9070641 100644 --- a/internal/compiler/passes/resolving.rs +++ b/internal/compiler/passes/resolving.rs @@ -848,8 +848,9 @@ impl Expression { pos = end + 1; literal_start_pos = pos; } - { - let trailing = Expression::StringLiteral((&string[literal_start_pos..]).into()); + let trailing = &string[literal_start_pos..]; + if !trailing.is_empty() { + let trailing = Expression::StringLiteral(trailing.into()); expr = Some(match expr { None => trailing, Some(expr) => Expression::BinaryExpression { diff --git a/internal/core/api.rs b/internal/core/api.rs index 5cddc9e845b..5b5e2e37d28 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1220,7 +1220,353 @@ pub fn set_xdg_app_id(app_id: impl Into) -> Result<(), PlatformErr ) } +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug, PartialEq)] +pub enum Style { + Emphasis, + Strong, + Strikethrough, + Code, + Link, + Underline, + Color(crate::Color), +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug, PartialEq)] +pub struct FormattedSpan { + pub range: std::ops::Range, + pub style: Style, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug)] +enum ListItemType { + Ordered(u64), + Unordered, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Clone, Debug, PartialEq)] +pub struct StyledTextParagraph { + pub text: std::string::String, + pub formatting: std::vec::Vec, + pub links: std::vec::Vec<(std::ops::Range, std::string::String)>, +} + +#[cfg(feature = "experimental-rich-text")] +#[derive(Debug, thiserror::Error)] +pub enum StyledTextError<'a> { + #[error("Spans are unbalanced: stack already empty when popped")] + Pop, + #[error("Spans are unbalanced: stack contained items at end of function")] + NotEmpty, + #[error("Paragraph not started")] + ParagraphNotStarted, + #[error("Unimplemented: {:?}", .0)] + UnimplementedTag(pulldown_cmark::Tag<'a>), + #[error("Unimplemented: {:?}", .0)] + UnimplementedEvent(pulldown_cmark::Event<'a>), + #[error("Unimplemented: {}", .0)] + UnimplementedHtmlEvent(std::string::String), + #[error("Unimplemented html tag: {}", .0)] + UnimplementedHtmlTag(std::string::String), + #[error("Unexpected {} attribute in html {}", .0, .1)] + UnexpectedAttribute(std::string::String, std::string::String), + #[error("Missing color attribute in html {}", .0)] + MissingColor(std::string::String), + #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] + ClosingTagMismatch(&'a str, std::string::String), +} + +/// Internal styled text type #[derive(Debug, PartialEq, Clone, Default)] pub struct StyledText { - inner: (), + #[cfg(feature = "experimental-rich-text")] + pub paragraphs: std::vec::Vec, +} + +#[cfg(feature = "experimental-rich-text")] +impl StyledText { + fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { + let mut text = std::string::String::with_capacity(indentation as usize * 4); + for _ in 0..indentation { + text.push_str(" "); + } + match list_item_type { + Some(ListItemType::Unordered) => { + if indentation % 3 == 0 { + text.push_str("• ") + } else if indentation % 3 == 1 { + text.push_str("◦ ") + } else { + text.push_str("▪ ") + } + } + Some(ListItemType::Ordered(num)) => text.push_str(&std::format!("{}. ", num)), + None => {} + }; + self.paragraphs.push(StyledTextParagraph { + text, + formatting: Default::default(), + links: Default::default(), + }); + } + + pub fn parse(string: &str) -> Result> { + let parser = + pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); + + let mut styled_text = StyledText::default(); + let mut list_state_stack: std::vec::Vec> = std::vec::Vec::new(); + let mut style_stack = std::vec::Vec::new(); + let mut current_url = None; + + for event in parser { + let indentation = list_state_stack.len().saturating_sub(1) as _; + + match event { + pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => { + styled_text.begin_paragraph(indentation, None); + } + pulldown_cmark::Event::End(pulldown_cmark::TagEnd::List(_)) => { + if list_state_stack.pop().is_none() { + return Err(StyledTextError::Pop); + } + } + pulldown_cmark::Event::End( + pulldown_cmark::TagEnd::Paragraph | pulldown_cmark::TagEnd::Item, + ) => {} + pulldown_cmark::Event::Start(tag) => { + let style = match tag { + pulldown_cmark::Tag::Paragraph => { + styled_text.begin_paragraph(indentation, None); + continue; + } + pulldown_cmark::Tag::Item => { + styled_text.begin_paragraph( + indentation, + Some(match list_state_stack.last().copied() { + Some(Some(index)) => ListItemType::Ordered(index), + _ => ListItemType::Unordered, + }), + ); + if let Some(state) = list_state_stack.last_mut() { + *state = state.map(|state| state + 1); + } + continue; + } + pulldown_cmark::Tag::List(index) => { + list_state_stack.push(index); + continue; + } + pulldown_cmark::Tag::Strong => Style::Strong, + pulldown_cmark::Tag::Emphasis => Style::Emphasis, + pulldown_cmark::Tag::Strikethrough => Style::Strikethrough, + pulldown_cmark::Tag::Link { dest_url, .. } => { + current_url = Some(dest_url); + Style::Link + } + + pulldown_cmark::Tag::Heading { .. } + | pulldown_cmark::Tag::Image { .. } + | pulldown_cmark::Tag::DefinitionList + | pulldown_cmark::Tag::DefinitionListTitle + | pulldown_cmark::Tag::DefinitionListDefinition + | pulldown_cmark::Tag::TableHead + | pulldown_cmark::Tag::TableRow + | pulldown_cmark::Tag::TableCell + | pulldown_cmark::Tag::HtmlBlock + | pulldown_cmark::Tag::Superscript + | pulldown_cmark::Tag::Subscript + | pulldown_cmark::Tag::Table(_) + | pulldown_cmark::Tag::MetadataBlock(_) + | pulldown_cmark::Tag::BlockQuote(_) + | pulldown_cmark::Tag::CodeBlock(_) + | pulldown_cmark::Tag::FootnoteDefinition(_) => { + return Err(StyledTextError::UnimplementedTag(tag)); + } + }; + + style_stack.push(( + style, + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + pulldown_cmark::Event::Text(text) => { + styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .push_str(&text); + } + pulldown_cmark::Event::End(_) => { + let (style, start) = if let Some(value) = style_stack.pop() { + value + } else { + return Err(StyledTextError::Pop); + }; + + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let end = paragraph.text.len(); + + if let Some(url) = current_url.take() { + paragraph.links.push((start..end, url.into())); + } + + paragraph.formatting.push(FormattedSpan { range: start..end, style }); + } + pulldown_cmark::Event::Code(text) => { + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let start = paragraph.text.len(); + paragraph.text.push_str(&text); + paragraph.formatting.push(FormattedSpan { + range: start..paragraph.text.len(), + style: Style::Code, + }); + } + pulldown_cmark::Event::InlineHtml(html) => { + if html.starts_with(" "", + Style::Underline => "", + other => std::unreachable!( + "Got unexpected closing style {:?} with html {}. This error should have been caught earlier.", + other, + html + ), + }; + + if (&*html) != expected_tag { + return Err(StyledTextError::ClosingTagMismatch( + expected_tag, + (&*html).into(), + )); + } + + let paragraph = styled_text + .paragraphs + .last_mut() + .ok_or(StyledTextError::ParagraphNotStarted)?; + let end = paragraph.text.len(); + paragraph.formatting.push(FormattedSpan { range: start..end, style }); + } else { + let mut expecting_color_attribute = false; + + for token in htmlparser::Tokenizer::from(&*html) { + match token { + Ok(htmlparser::Token::ElementStart { local: tag_type, .. }) => { + match &*tag_type { + "u" => { + style_stack.push(( + Style::Underline, + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + "font" => { + expecting_color_attribute = true; + } + _ => { + return Err(StyledTextError::UnimplementedHtmlTag( + (&*tag_type).into(), + )); + } + } + } + Ok(htmlparser::Token::Attribute { + local: key, + value: Some(value), + .. + }) => match &*key { + "color" => { + if !expecting_color_attribute { + return Err(StyledTextError::UnexpectedAttribute( + (&*key).into(), + (&*html).into(), + )); + } + expecting_color_attribute = false; + + let value = + i_slint_common::color_parsing::parse_color_literal( + &*value, + ) + .or_else(|| { + i_slint_common::color_parsing::named_colors() + .get(&*value) + .copied() + }) + .expect("invalid color value"); + + style_stack.push(( + Style::Color(crate::Color::from_argb_encoded(value)), + styled_text + .paragraphs + .last() + .ok_or(StyledTextError::ParagraphNotStarted)? + .text + .len(), + )); + } + _ => { + return Err(StyledTextError::UnexpectedAttribute( + (&*key).into(), + (&*html).into(), + )); + } + }, + Ok(htmlparser::Token::ElementEnd { .. }) => {} + _ => { + return Err(StyledTextError::UnimplementedHtmlEvent( + std::format!("{:?}", token), + )); + } + } + } + + if expecting_color_attribute { + return Err(StyledTextError::MissingColor((&*html).into())); + } + } + } + pulldown_cmark::Event::Rule + | pulldown_cmark::Event::TaskListMarker(_) + | pulldown_cmark::Event::FootnoteReference(_) + | pulldown_cmark::Event::InlineMath(_) + | pulldown_cmark::Event::DisplayMath(_) + | pulldown_cmark::Event::Html(_) => { + return Err(StyledTextError::UnimplementedEvent(event)); + } + } + } + + if !style_stack.is_empty() { + return Err(StyledTextError::NotEmpty); + } + + Ok(styled_text) + } } diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index d05c1e7986d..b06b60015ad 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -8,6 +8,7 @@ use super::items::*; use crate::graphics::{Color, FontRequest, Image, IntRect}; use crate::item_tree::ItemTreeRc; use crate::item_tree::{ItemVisitor, ItemVisitorVTable, VisitChildrenResult}; +use crate::items::StyledText; use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, }; @@ -407,6 +408,13 @@ pub trait ItemRenderer { _size: LogicalSize, _cache: &CachedRenderingData, ); + fn draw_styled_text( + &mut self, + text: Pin<&StyledText>, + _self_rc: &ItemRc, + _size: LogicalSize, + _cache: &CachedRenderingData, + ); fn draw_text_input( &mut self, text_input: Pin<&TextInput>, diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index 481b17732b6..1a8e0a5d08c 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -258,7 +258,7 @@ impl Item for StyledText { window_adapter: &Rc, self_rc: &ItemRc, ) -> LayoutInfo { - text_layout_info( + styled_text_layout_info( self, &self_rc, window_adapter, @@ -351,7 +351,7 @@ impl Item for StyledText { self_rc: &ItemRc, size: LogicalSize, ) -> RenderingResult { - (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data); + (*backend).draw_styled_text(self, self_rc, size, &self.cached_rendering_data); RenderingResult::ContinueRenderingChildren } @@ -389,48 +389,6 @@ impl HasFont for StyledText { } } -impl RenderString for StyledText { - fn text(self: Pin<&Self>) -> SharedString { - panic!() - } -} - -impl RenderText for StyledText { - fn target_size(self: Pin<&Self>) -> LogicalSize { - LogicalSize::from_lengths(self.width(), self.height()) - } - - fn color(self: Pin<&Self>) -> Brush { - self.color() - } - - fn link_color(self: Pin<&Self>) -> Color { - self.link_color() - } - - fn alignment( - self: Pin<&Self>, - ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) { - (self.horizontal_alignment(), self.vertical_alignment()) - } - - fn wrap(self: Pin<&Self>) -> TextWrap { - self.wrap() - } - - fn overflow(self: Pin<&Self>) -> TextOverflow { - self.overflow() - } - - fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) { - (self.stroke(), self.stroke_width(), self.stroke_style()) - } - - fn is_markdown(self: Pin<&Self>) -> bool { - true - } -} - impl StyledText { pub fn font_metrics( self: Pin<&Self>, @@ -662,6 +620,50 @@ fn text_layout_info( } } +fn styled_text_layout_info( + text: Pin<&StyledText>, + self_rc: &ItemRc, + window_adapter: &Rc, + orientation: Orientation, + width: Pin<&Property>, +) -> LayoutInfo { + let implicit_size = |max_width, text_wrap| { + window_adapter.renderer().styled_text_size(text, self_rc, max_width, text_wrap) + }; + + // Stretch uses `round_layout` to explicitly align the top left and bottom right of layout nodes + // to pixel boundaries. To avoid rounding down causing the minimum width to become so little that + // letters will be cut off, apply the ceiling here. + match orientation { + Orientation::Horizontal => { + let implicit_size = implicit_size(None, TextWrap::NoWrap); + let min = match text.overflow() { + TextOverflow::Elide => implicit_size + .width + .min(window_adapter.renderer().char_size(text, self_rc, '…').width), + TextOverflow::Clip => match text.wrap() { + TextWrap::NoWrap => implicit_size.width, + TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord, + }, + }; + LayoutInfo { + min: min.ceil(), + preferred: implicit_size.width.ceil(), + ..LayoutInfo::default() + } + } + Orientation::Vertical => { + let h = match text.wrap() { + TextWrap::NoWrap => implicit_size(None, TextWrap::NoWrap).height, + TextWrap::WordWrap => implicit_size(Some(width.get()), TextWrap::WordWrap).height, + TextWrap::CharWrap => implicit_size(Some(width.get()), TextWrap::CharWrap).height, + } + .ceil(); + LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() } + } + } +} + #[repr(C)] #[derive(Default, Clone, Copy, PartialEq)] /// Similar as `Option>` but `repr(C)` diff --git a/internal/core/lib.rs b/internal/core/lib.rs index 51b3bc520a9..c6aa1ad26f7 100644 --- a/internal/core/lib.rs +++ b/internal/core/lib.rs @@ -204,6 +204,6 @@ pub fn escape_markdown(text: &str) -> std::string::String { out } -pub fn parse_markdown(text: &str) -> std::string::String { - text.into() +pub fn parse_markdown(text: &str) -> crate::api::StyledText { + crate::api::StyledText::parse(text).unwrap() } diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index 17b8594f21c..d686f88f064 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -23,7 +23,9 @@ use crate::item_rendering::{ use crate::item_tree::{ItemTreeRc, ItemTreeWeak, ItemVisitorResult}; #[cfg(feature = "std")] use crate::items::Path; -use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, TextInput}; +use crate::items::{ + BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, StyledText, TextInput, +}; use crate::lengths::{ ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect, LogicalSize, LogicalVector, @@ -667,6 +669,7 @@ impl ItemRenderer for PartialRenderer<'_ forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle)); forward_rendering_call2!(fn draw_image(dyn RenderImage)); forward_rendering_call2!(fn draw_text(dyn RenderText)); + forward_rendering_call2!(fn draw_styled_text(StyledText)); forward_rendering_call!(fn draw_text_input(TextInput)); #[cfg(feature = "std")] forward_rendering_call!(fn draw_path(Path)); diff --git a/internal/core/renderer.rs b/internal/core/renderer.rs index 656c5d14ba8..53fa0784d77 100644 --- a/internal/core/renderer.rs +++ b/internal/core/renderer.rs @@ -36,6 +36,14 @@ pub trait RendererSealed { text_wrap: TextWrap, ) -> LogicalSize; + fn styled_text_size( + &self, + text_item: Pin<&crate::items::StyledText>, + item_rc: &crate::item_tree::ItemRc, + max_width: Option, + text_wrap: TextWrap, + ) -> LogicalSize; + /// Returns the size of the individual character in logical pixels. fn char_size( &self, diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 4d714d69713..2a25363cf5b 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -774,6 +774,16 @@ impl RendererSealed for SoftwareRenderer { } } + fn styled_text_size( + &self, + text_item: Pin<&crate::items::StyledText>, + item_rc: &crate::item_tree::ItemRc, + max_width: Option, + text_wrap: TextWrap, + ) -> LogicalSize { + panic!() + } + fn char_size( &self, text_item: Pin<&dyn crate::item_rendering::HasFont>, @@ -2621,6 +2631,16 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T } } + fn draw_styled_text( + &mut self, + text: Pin<&crate::items::StyledText>, + self_rc: &ItemRc, + size: LogicalSize, + _cache: &CachedRenderingData, + ) { + panic!() + } + fn draw_text_input( &mut self, text_input: Pin<&crate::items::TextInput>, diff --git a/internal/core/textlayout/sharedparley.rs b/internal/core/textlayout/sharedparley.rs index 095653133df..9832275a873 100644 --- a/internal/core/textlayout/sharedparley.rs +++ b/internal/core/textlayout/sharedparley.rs @@ -225,9 +225,11 @@ impl LayoutWithoutLineBreaksBuilder { &self, text: &str, selection: Option<(Range, Color)>, - formatting: impl IntoIterator, + formatting: impl IntoIterator, link_color: Option, ) -> parley::Layout { + use crate::api::Style; + CONTEXTS.with_borrow_mut(|contexts| { let mut builder = self.ranged_builder(contexts.as_mut(), text); @@ -306,7 +308,7 @@ impl LayoutWithoutLineBreaksBuilder { enum Text<'a> { PlainText(&'a str), #[cfg(feature = "experimental-rich-text")] - RichText(RichText<'a>), + StyledText(crate::api::StyledText), } fn create_text_paragraphs( @@ -318,7 +320,7 @@ fn create_text_paragraphs( let paragraph_from_text = |text: &str, range: std::ops::Range, - formatting: Vec, + formatting: Vec, links: Vec<(std::ops::Range, std::string::String)>| { let selection = selection.clone().and_then(|(selection, selection_color)| { let sel_start = selection.start.max(range.start); @@ -373,17 +375,13 @@ fn create_text_paragraphs( } } #[cfg(feature = "experimental-rich-text")] - Text::RichText(rich_text) => { + Text::StyledText(rich_text) => { for paragraph in rich_text.paragraphs { paragraphs.push(paragraph_from_text( ¶graph.text, 0..0, paragraph.formatting, - paragraph - .links - .into_iter() - .map(|(range, link)| (range, link.into_string())) - .collect(), + paragraph.links.into_iter().collect(), )); } } @@ -838,352 +836,6 @@ impl Layout { } } -#[cfg_attr(not(feature = "experimental-rich-text"), allow(unused))] -#[derive(Debug, PartialEq)] -enum Style { - Emphasis, - Strong, - Strikethrough, - Code, - Link, - Underline, - Color(Color), -} - -#[derive(Debug, PartialEq)] -struct FormattedSpan { - range: Range, - style: Style, -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Debug)] -enum ListItemType { - Ordered(u64), - Unordered, -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Debug, PartialEq)] -struct RichTextParagraph<'a> { - text: std::string::String, - formatting: Vec, - #[cfg(feature = "experimental-rich-text")] - links: std::vec::Vec<(Range, pulldown_cmark::CowStr<'a>)>, - #[cfg(not(feature = "experimental-rich-text"))] - _phantom: std::marker::PhantomData<&'a ()>, -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Debug, Default)] -struct RichText<'a> { - paragraphs: Vec>, -} - -#[cfg(feature = "experimental-rich-text")] -impl<'a> RichText<'a> { - fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { - let mut text = std::string::String::with_capacity(indentation as usize * 4); - for _ in 0..indentation { - text.push_str(" "); - } - match list_item_type { - Some(ListItemType::Unordered) => { - if indentation % 3 == 0 { - text.push_str("• ") - } else if indentation % 3 == 1 { - text.push_str("◦ ") - } else { - text.push_str("▪ ") - } - } - Some(ListItemType::Ordered(num)) => text.push_str(&std::format!("{}. ", num)), - None => {} - }; - self.paragraphs.push(RichTextParagraph { - text, - formatting: Default::default(), - links: Default::default(), - }); - } -} - -#[cfg(feature = "experimental-rich-text")] -#[derive(Debug, thiserror::Error)] -enum RichTextError<'a> { - #[error("Spans are unbalanced: stack already empty when popped")] - Pop, - #[error("Spans are unbalanced: stack contained items at end of function")] - NotEmpty, - #[error("Paragraph not started")] - ParagraphNotStarted, - #[error("Unimplemented: {:?}", .0)] - UnimplementedTag(pulldown_cmark::Tag<'a>), - #[error("Unimplemented: {:?}", .0)] - UnimplementedEvent(pulldown_cmark::Event<'a>), - #[error("Unimplemented: {}", .0)] - UnimplementedHtmlEvent(std::string::String), - #[error("Unimplemented html tag: {}", .0)] - UnimplementedHtmlTag(std::string::String), - #[error("Unexpected {} attribute in html {}", .0, .1)] - UnexpectedAttribute(std::string::String, std::string::String), - #[error("Missing color attribute in html {}", .0)] - MissingColor(std::string::String), - #[error("Closing html tag doesn't match the opening tag. Expected {}, got {}", .0, .1)] - ClosingTagMismatch(&'a str, std::string::String), -} - -#[cfg(feature = "experimental-rich-text")] -fn parse_markdown(string: &str) -> Result, RichTextError<'_>> { - let parser = - pulldown_cmark::Parser::new_ext(string, pulldown_cmark::Options::ENABLE_STRIKETHROUGH); - - let mut rich_text = RichText::default(); - let mut list_state_stack: std::vec::Vec> = std::vec::Vec::new(); - let mut style_stack = std::vec::Vec::new(); - let mut current_url = None; - - for event in parser { - let indentation = list_state_stack.len().saturating_sub(1) as _; - - match event { - pulldown_cmark::Event::SoftBreak | pulldown_cmark::Event::HardBreak => { - rich_text.begin_paragraph(indentation, None); - } - pulldown_cmark::Event::End(pulldown_cmark::TagEnd::List(_)) => { - if list_state_stack.pop().is_none() { - return Err(RichTextError::Pop); - } - } - pulldown_cmark::Event::End( - pulldown_cmark::TagEnd::Paragraph | pulldown_cmark::TagEnd::Item, - ) => {} - pulldown_cmark::Event::Start(tag) => { - let style = match tag { - pulldown_cmark::Tag::Paragraph => { - rich_text.begin_paragraph(indentation, None); - continue; - } - pulldown_cmark::Tag::Item => { - rich_text.begin_paragraph( - indentation, - Some(match list_state_stack.last().copied() { - Some(Some(index)) => ListItemType::Ordered(index), - _ => ListItemType::Unordered, - }), - ); - if let Some(state) = list_state_stack.last_mut() { - *state = state.map(|state| state + 1); - } - continue; - } - pulldown_cmark::Tag::List(index) => { - list_state_stack.push(index); - continue; - } - pulldown_cmark::Tag::Strong => Style::Strong, - pulldown_cmark::Tag::Emphasis => Style::Emphasis, - pulldown_cmark::Tag::Strikethrough => Style::Strikethrough, - pulldown_cmark::Tag::Link { dest_url, .. } => { - current_url = Some(dest_url); - Style::Link - } - - pulldown_cmark::Tag::Heading { .. } - | pulldown_cmark::Tag::Image { .. } - | pulldown_cmark::Tag::DefinitionList - | pulldown_cmark::Tag::DefinitionListTitle - | pulldown_cmark::Tag::DefinitionListDefinition - | pulldown_cmark::Tag::TableHead - | pulldown_cmark::Tag::TableRow - | pulldown_cmark::Tag::TableCell - | pulldown_cmark::Tag::HtmlBlock - | pulldown_cmark::Tag::Superscript - | pulldown_cmark::Tag::Subscript - | pulldown_cmark::Tag::Table(_) - | pulldown_cmark::Tag::MetadataBlock(_) - | pulldown_cmark::Tag::BlockQuote(_) - | pulldown_cmark::Tag::CodeBlock(_) - | pulldown_cmark::Tag::FootnoteDefinition(_) => { - return Err(RichTextError::UnimplementedTag(tag)); - } - }; - - style_stack.push(( - style, - rich_text - .paragraphs - .last() - .ok_or(RichTextError::ParagraphNotStarted)? - .text - .len(), - )); - } - pulldown_cmark::Event::Text(text) => { - rich_text - .paragraphs - .last_mut() - .ok_or(RichTextError::ParagraphNotStarted)? - .text - .push_str(&text); - } - pulldown_cmark::Event::End(_) => { - let (style, start) = if let Some(value) = style_stack.pop() { - value - } else { - return Err(RichTextError::Pop); - }; - - let paragraph = - rich_text.paragraphs.last_mut().ok_or(RichTextError::ParagraphNotStarted)?; - let end = paragraph.text.len(); - - if let Some(url) = current_url.take() { - paragraph.links.push((start..end, url)); - } - - paragraph.formatting.push(FormattedSpan { range: start..end, style }); - } - pulldown_cmark::Event::Code(text) => { - let paragraph = - rich_text.paragraphs.last_mut().ok_or(RichTextError::ParagraphNotStarted)?; - let start = paragraph.text.len(); - paragraph.text.push_str(&text); - paragraph - .formatting - .push(FormattedSpan { range: start..paragraph.text.len(), style: Style::Code }); - } - pulldown_cmark::Event::InlineHtml(html) => { - if html.starts_with(" "", - Style::Underline => "", - other => std::unreachable!( - "Got unexpected closing style {:?} with html {}. This error should have been caught earlier.", - other, - html - ), - }; - - if (&*html) != expected_tag { - return Err(RichTextError::ClosingTagMismatch( - expected_tag, - (&*html).into(), - )); - } - - let paragraph = rich_text - .paragraphs - .last_mut() - .ok_or(RichTextError::ParagraphNotStarted)?; - let end = paragraph.text.len(); - paragraph.formatting.push(FormattedSpan { range: start..end, style }); - } else { - let mut expecting_color_attribute = false; - - for token in htmlparser::Tokenizer::from(&*html) { - match token { - Ok(htmlparser::Token::ElementStart { local: tag_type, .. }) => { - match &*tag_type { - "u" => { - style_stack.push(( - Style::Underline, - rich_text - .paragraphs - .last() - .ok_or(RichTextError::ParagraphNotStarted)? - .text - .len(), - )); - } - "font" => { - expecting_color_attribute = true; - } - _ => { - return Err(RichTextError::UnimplementedHtmlTag( - (&*tag_type).into(), - )); - } - } - } - Ok(htmlparser::Token::Attribute { - local: key, - value: Some(value), - .. - }) => match &*key { - "color" => { - if !expecting_color_attribute { - return Err(RichTextError::UnexpectedAttribute( - (&*key).into(), - (&*html).into(), - )); - } - expecting_color_attribute = false; - - let value = - i_slint_common::color_parsing::parse_color_literal(&*value) - .or_else(|| { - i_slint_common::color_parsing::named_colors() - .get(&*value) - .copied() - }) - .expect("invalid color value"); - - style_stack.push(( - Style::Color(Color::from_argb_encoded(value)), - rich_text - .paragraphs - .last() - .ok_or(RichTextError::ParagraphNotStarted)? - .text - .len(), - )); - } - _ => { - return Err(RichTextError::UnexpectedAttribute( - (&*key).into(), - (&*html).into(), - )); - } - }, - Ok(htmlparser::Token::ElementEnd { .. }) => {} - _ => { - return Err(RichTextError::UnimplementedHtmlEvent(std::format!( - "{:?}", token - ))); - } - } - } - - if expecting_color_attribute { - return Err(RichTextError::MissingColor((&*html).into())); - } - } - } - pulldown_cmark::Event::Rule - | pulldown_cmark::Event::TaskListMarker(_) - | pulldown_cmark::Event::FootnoteReference(_) - | pulldown_cmark::Event::InlineMath(_) - | pulldown_cmark::Event::DisplayMath(_) - | pulldown_cmark::Event::Html(_) => { - return Err(RichTextError::UnimplementedEvent(event)); - } - } - } - - if !style_stack.is_empty() { - return Err(RichTextError::NotEmpty); - } - - Ok(rich_text) -} - #[cfg(feature = "experimental-rich-text")] #[test] fn markdown_parsing() { @@ -1393,17 +1045,7 @@ pub fn draw_text( let str = text.text(); #[cfg(feature = "experimental-rich-text")] - let layout_text = if text.is_markdown() { - Text::RichText(match parse_markdown(&str) { - Ok(rich_text) => rich_text, - Err(error) => { - crate::debug_log!("{}", error); - return; - } - }) - } else { - Text::PlainText(&str) - }; + let layout_text = { Text::PlainText(&str) }; #[cfg(not(feature = "experimental-rich-text"))] let layout_text = Text::PlainText(&str); @@ -1455,34 +1097,124 @@ pub fn draw_text( } } +pub fn draw_styled_text( + item_renderer: &mut impl GlyphRenderer, + text: Pin<&crate::items::StyledText>, + item_rc: Option<&crate::item_tree::ItemRc>, + size: LogicalSize, +) { + let max_width = size.width_length(); + let max_height = size.height_length(); + + if max_width.get() <= 0. || max_height.get() <= 0. { + return; + } + + let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size) + else { + // Nothing to draw + return; + }; + + let scale_factor = ScaleFactor::new(item_renderer.scale_factor()); + + let (stroke_brush, stroke_width, stroke_style) = + (text.stroke(), text.stroke_width(), text.stroke_style()); + let platform_stroke_brush = if !stroke_brush.is_transparent() { + let stroke_width = if stroke_width.get() != 0.0 { + (stroke_width * scale_factor).get() + } else { + // Hairline stroke + 1.0 + }; + let stroke_width = match stroke_style { + TextStrokeStyle::Outside => stroke_width * 2.0, + TextStrokeStyle::Center => stroke_width, + }; + item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size) + } else { + None + }; + + let layout_builder = LayoutWithoutLineBreaksBuilder::new( + None, //item_rc.map(|item_rc| text.font_request(item_rc)), + text.wrap(), + platform_stroke_brush.is_some().then_some(stroke_style), + scale_factor, + ); + + let layout_text = { Text::StyledText(text.text()) }; + + let paragraphs_without_linebreaks = + create_text_paragraphs(&layout_builder, layout_text, None, text.link_color()); + + let (horizontal_align, vertical_align) = + (text.horizontal_alignment(), text.vertical_alignment()); + let text_overflow = text.overflow(); + + let layout = layout( + &layout_builder, + paragraphs_without_linebreaks, + scale_factor, + LayoutOptions { + horizontal_align, + vertical_align, + max_height: Some(max_height), + max_width: Some(max_width), + text_overflow: text.overflow(), + }, + ); + + let render = if text_overflow == TextOverflow::Clip { + item_renderer.save_state(); + + item_renderer.combine_clip( + LogicalRect::new(LogicalPoint::default(), size), + LogicalBorderRadius::zero(), + LogicalLength::zero(), + ) + } else { + true + }; + + if render { + layout.draw( + item_renderer, + platform_fill_brush, + platform_stroke_brush, + &mut |item_renderer, font, font_size, brush, y_offset, glyphs_it| { + item_renderer.draw_glyph_run(font, font_size, brush, y_offset, glyphs_it); + }, + ); + } + + if text_overflow == TextOverflow::Clip { + item_renderer.restore_state(); + } +} + #[cfg(feature = "experimental-rich-text")] pub fn link_under_cursor( scale_factor: ScaleFactor, - text: Pin<&dyn crate::item_rendering::RenderText>, + text: Pin<&crate::items::StyledText>, item_rc: &crate::item_tree::ItemRc, size: LogicalSize, cursor: PhysicalPoint, ) -> Option { let layout_builder = LayoutWithoutLineBreaksBuilder::new( - Some(text.font_request(item_rc)), + None, //Some(text.font_request(item_rc)), text.wrap(), None, scale_factor, ); - let str = text.text(); - let layout_text = Text::RichText(match parse_markdown(&str) { - Ok(rich_text) => rich_text, - Err(error) => { - crate::debug_log!("{}", error); - return None; - } - }); + let layout_text = { Text::StyledText(text.text()) }; let paragraphs_without_linebreaks = create_text_paragraphs(&layout_builder, layout_text, None, text.link_color()); - let (horizontal_align, vertical_align) = text.alignment(); + let (horizontal_align, vertical_align) = + (text.horizontal_alignment(), text.vertical_alignment()); let layout = layout( &layout_builder, @@ -1663,6 +1395,44 @@ pub fn text_size( PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor } +pub fn styled_text_size( + renderer: &dyn RendererSealed, + text_item: Pin<&crate::items::StyledText>, + item_rc: &crate::item_tree::ItemRc, + max_width: Option, + text_wrap: TextWrap, +) -> LogicalSize { + let Some(scale_factor) = renderer.scale_factor() else { + return LogicalSize::default(); + }; + + let layout_builder = LayoutWithoutLineBreaksBuilder::new( + None, //Some(text_item.font_request(item_rc)), + text_wrap, + None, + scale_factor, + ); + + let layout_text = { Text::StyledText(text_item.text()) }; + + let paragraphs_without_linebreaks = + create_text_paragraphs(&layout_builder, layout_text, None, Color::default()); + + let layout = layout( + &layout_builder, + paragraphs_without_linebreaks, + scale_factor, + LayoutOptions { + max_width, + max_height: None, + horizontal_align: TextHorizontalAlignment::Left, + vertical_align: TextVerticalAlignment::Top, + text_overflow: TextOverflow::Clip, + }, + ); + PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor +} + pub fn char_size( text_item: Pin<&dyn crate::item_rendering::HasFont>, item_rc: &crate::item_tree::ItemRc, diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 87146198a9f..977f9cce8e4 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -1408,7 +1408,7 @@ fn call_builtin_function( BuiltinFunction::ParseMarkdown => { let text: SharedString = eval_expression(&arguments[0], local_context).try_into().unwrap(); - Value::String(corelib::parse_markdown(&text).into()) + Value::StyledText(corelib::parse_markdown(&text)) } } } diff --git a/internal/renderers/femtovg/itemrenderer.rs b/internal/renderers/femtovg/itemrenderer.rs index 4a4ca9f912f..006a68763fd 100644 --- a/internal/renderers/femtovg/itemrenderer.rs +++ b/internal/renderers/femtovg/itemrenderer.rs @@ -340,6 +340,16 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer sharedparley::draw_text(self, text, Some(self_rc), size); } + fn draw_styled_text( + &mut self, + text: Pin<&i_slint_core::items::StyledText>, + self_rc: &ItemRc, + size: LogicalSize, + _cache: &CachedRenderingData, + ) { + panic!() + } + fn draw_text_input( &mut self, text_input: Pin<&items::TextInput>, diff --git a/internal/renderers/femtovg/lib.rs b/internal/renderers/femtovg/lib.rs index 295834ab5c2..ced09c43f1a 100644 --- a/internal/renderers/femtovg/lib.rs +++ b/internal/renderers/femtovg/lib.rs @@ -292,6 +292,16 @@ impl RendererSealed for FemtoVGRenderer { sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap) } + fn styled_text_size( + &self, + text_item: Pin<&i_slint_core::items::StyledText>, + item_rc: &ItemRc, + max_width: Option, + text_wrap: TextWrap, + ) -> LogicalSize { + panic!() + } + fn char_size( &self, text_item: Pin<&dyn i_slint_core::item_rendering::HasFont>, From 02200976c45a9715a8a8867640708d40d5501d18 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Tue, 18 Nov 2025 14:07:38 +0100 Subject: [PATCH 11/20] Change rendertext trait to use enum --- internal/backends/qt/qt_window.rs | 20 --- internal/core/api.rs | 2 - internal/core/item_rendering.rs | 18 +- internal/core/items/text.rs | 106 +++++------ internal/core/lib.rs | 9 +- internal/core/partial_renderer.rs | 5 +- internal/core/renderer.rs | 8 - internal/core/software_renderer.rs | 51 ++---- internal/core/textlayout/sharedparley.rs | 195 +++------------------ internal/renderers/femtovg/itemrenderer.rs | 10 -- internal/renderers/femtovg/lib.rs | 10 -- 11 files changed, 114 insertions(+), 320 deletions(-) diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs index 24192b41bdf..9e72475f73d 100644 --- a/internal/backends/qt/qt_window.rs +++ b/internal/backends/qt/qt_window.rs @@ -686,16 +686,6 @@ impl ItemRenderer for QtItemRenderer<'_> { sharedparley::draw_text(self, text, Some(self_rc), size); } - fn draw_styled_text( - &mut self, - text: Pin<&i_slint_core::items::StyledText>, - self_rc: &ItemRc, - size: LogicalSize, - _cache: &CachedRenderingData, - ) { - sharedparley::draw_styled_text(self, text, Some(self_rc), size); - } - fn draw_text_input( &mut self, text_input: Pin<&items::TextInput>, @@ -2105,16 +2095,6 @@ impl i_slint_core::renderer::RendererSealed for QtWindow { sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap) } - fn styled_text_size( - &self, - text_item: Pin<&i_slint_core::items::StyledText>, - item_rc: &ItemRc, - max_width: Option, - text_wrap: TextWrap, - ) -> LogicalSize { - sharedparley::styled_text_size(self, text_item, item_rc, max_width, text_wrap) - } - fn char_size( &self, text_item: Pin<&dyn i_slint_core::item_rendering::HasFont>, diff --git a/internal/core/api.rs b/internal/core/api.rs index 5b5e2e37d28..9cfd82ec289 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1220,7 +1220,6 @@ pub fn set_xdg_app_id(app_id: impl Into) -> Result<(), PlatformErr ) } -#[cfg(feature = "experimental-rich-text")] #[derive(Clone, Debug, PartialEq)] pub enum Style { Emphasis, @@ -1232,7 +1231,6 @@ pub enum Style { Color(crate::Color), } -#[cfg(feature = "experimental-rich-text")] #[derive(Clone, Debug, PartialEq)] pub struct FormattedSpan { pub range: std::ops::Range, diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index b06b60015ad..d5ac6abd30f 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -293,10 +293,15 @@ pub trait HasFont { fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest; } +pub enum PlainOrStyledText { + Plain(SharedString), + Styled(crate::api::StyledText), +} + /// Trait for an item that represents an string towards the renderer #[allow(missing_docs)] pub trait RenderString: HasFont { - fn text(self: Pin<&Self>) -> SharedString; + fn text(self: Pin<&Self>) -> PlainOrStyledText; } /// Trait for an item that represents an Text towards the renderer @@ -326,8 +331,8 @@ impl HasFont for (SharedString, Brush) { } impl RenderString for (SharedString, Brush) { - fn text(self: Pin<&Self>) -> SharedString { - self.0.clone() + fn text(self: Pin<&Self>) -> PlainOrStyledText { + PlainOrStyledText::Plain(self.0.clone()) } } @@ -408,13 +413,6 @@ pub trait ItemRenderer { _size: LogicalSize, _cache: &CachedRenderingData, ); - fn draw_styled_text( - &mut self, - text: Pin<&StyledText>, - _self_rc: &ItemRc, - _size: LogicalSize, - _cache: &CachedRenderingData, - ); fn draw_text_input( &mut self, text_input: Pin<&TextInput>, diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index 1a8e0a5d08c..051951583b1 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -19,7 +19,9 @@ use crate::input::{ FocusEvent, FocusEventResult, FocusReason, InputEventFilterResult, InputEventResult, KeyEvent, KeyboardModifiers, MouseEvent, StandardShortcut, TextShortcut, key_codes, }; -use crate::item_rendering::{CachedRenderingData, HasFont, ItemRenderer, RenderString, RenderText}; +use crate::item_rendering::{ + CachedRenderingData, HasFont, ItemRenderer, PlainOrStyledText, RenderString, RenderText, +}; use crate::layout::{LayoutInfo, Orientation}; use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize}; use crate::platform::Clipboard; @@ -170,8 +172,8 @@ impl HasFont for ComplexText { } impl RenderString for ComplexText { - fn text(self: Pin<&Self>) -> SharedString { - self.text() + fn text(self: Pin<&Self>) -> PlainOrStyledText { + PlainOrStyledText::Plain(self.text()) } } @@ -258,7 +260,7 @@ impl Item for StyledText { window_adapter: &Rc, self_rc: &ItemRc, ) -> LayoutInfo { - styled_text_layout_info( + text_layout_info( self, &self_rc, window_adapter, @@ -351,7 +353,7 @@ impl Item for StyledText { self_rc: &ItemRc, size: LogicalSize, ) -> RenderingResult { - (*backend).draw_styled_text(self, self_rc, size, &self.cached_rendering_data); + (*backend).draw_text(self, self_rc, size, &self.cached_rendering_data); RenderingResult::ContinueRenderingChildren } @@ -389,6 +391,48 @@ impl HasFont for StyledText { } } +impl RenderString for StyledText { + fn text(self: Pin<&Self>) -> PlainOrStyledText { + PlainOrStyledText::Styled(self.text()) + } +} + +impl RenderText for StyledText { + fn target_size(self: Pin<&Self>) -> LogicalSize { + LogicalSize::from_lengths(self.width(), self.height()) + } + + fn color(self: Pin<&Self>) -> Brush { + self.color() + } + + fn link_color(self: Pin<&Self>) -> Color { + self.link_color() + } + + fn alignment( + self: Pin<&Self>, + ) -> (super::TextHorizontalAlignment, super::TextVerticalAlignment) { + (self.horizontal_alignment(), self.vertical_alignment()) + } + + fn wrap(self: Pin<&Self>) -> TextWrap { + self.wrap() + } + + fn overflow(self: Pin<&Self>) -> TextOverflow { + self.overflow() + } + + fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle) { + (self.stroke(), self.stroke_width(), self.stroke_style()) + } + + fn is_markdown(self: Pin<&Self>) -> bool { + true + } +} + impl StyledText { pub fn font_metrics( self: Pin<&Self>, @@ -525,8 +569,8 @@ impl HasFont for SimpleText { } impl RenderString for SimpleText { - fn text(self: Pin<&Self>) -> SharedString { - self.text() + fn text(self: Pin<&Self>) -> PlainOrStyledText { + PlainOrStyledText::Plain(self.text()) } } @@ -620,50 +664,6 @@ fn text_layout_info( } } -fn styled_text_layout_info( - text: Pin<&StyledText>, - self_rc: &ItemRc, - window_adapter: &Rc, - orientation: Orientation, - width: Pin<&Property>, -) -> LayoutInfo { - let implicit_size = |max_width, text_wrap| { - window_adapter.renderer().styled_text_size(text, self_rc, max_width, text_wrap) - }; - - // Stretch uses `round_layout` to explicitly align the top left and bottom right of layout nodes - // to pixel boundaries. To avoid rounding down causing the minimum width to become so little that - // letters will be cut off, apply the ceiling here. - match orientation { - Orientation::Horizontal => { - let implicit_size = implicit_size(None, TextWrap::NoWrap); - let min = match text.overflow() { - TextOverflow::Elide => implicit_size - .width - .min(window_adapter.renderer().char_size(text, self_rc, '…').width), - TextOverflow::Clip => match text.wrap() { - TextWrap::NoWrap => implicit_size.width, - TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord, - }, - }; - LayoutInfo { - min: min.ceil(), - preferred: implicit_size.width.ceil(), - ..LayoutInfo::default() - } - } - Orientation::Vertical => { - let h = match text.wrap() { - TextWrap::NoWrap => implicit_size(None, TextWrap::NoWrap).height, - TextWrap::WordWrap => implicit_size(Some(width.get()), TextWrap::WordWrap).height, - TextWrap::CharWrap => implicit_size(Some(width.get()), TextWrap::CharWrap).height, - } - .ceil(); - LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() } - } - } -} - #[repr(C)] #[derive(Default, Clone, Copy, PartialEq)] /// Similar as `Option>` but `repr(C)` @@ -1271,8 +1271,8 @@ impl HasFont for TextInput { } impl RenderString for TextInput { - fn text(self: Pin<&Self>) -> SharedString { - self.as_ref().text() + fn text(self: Pin<&Self>) -> PlainOrStyledText { + PlainOrStyledText::Plain(self.as_ref().text()) } } diff --git a/internal/core/lib.rs b/internal/core/lib.rs index c6aa1ad26f7..09e446f636b 100644 --- a/internal/core/lib.rs +++ b/internal/core/lib.rs @@ -197,6 +197,7 @@ pub fn escape_markdown(text: &str) -> std::string::String { '#' => out.push_str("\\#"), '-' => out.push_str("\\-"), '`' => out.push_str("\\`"), + '&' => out.push_str("\\&"), _ => out.push(c), } } @@ -204,6 +205,12 @@ pub fn escape_markdown(text: &str) -> std::string::String { out } +#[cfg_attr(not(feature = "experimental-rich-text"), allow(unused))] pub fn parse_markdown(text: &str) -> crate::api::StyledText { - crate::api::StyledText::parse(text).unwrap() + #[cfg(feature = "experimental-rich-text")] + { + crate::api::StyledText::parse(text).unwrap() + } + #[cfg(not(feature = "experimental-rich-text"))] + Default::default() } diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index d686f88f064..17b8594f21c 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -23,9 +23,7 @@ use crate::item_rendering::{ use crate::item_tree::{ItemTreeRc, ItemTreeWeak, ItemVisitorResult}; #[cfg(feature = "std")] use crate::items::Path; -use crate::items::{ - BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, StyledText, TextInput, -}; +use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, TextInput}; use crate::lengths::{ ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect, LogicalSize, LogicalVector, @@ -669,7 +667,6 @@ impl ItemRenderer for PartialRenderer<'_ forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle)); forward_rendering_call2!(fn draw_image(dyn RenderImage)); forward_rendering_call2!(fn draw_text(dyn RenderText)); - forward_rendering_call2!(fn draw_styled_text(StyledText)); forward_rendering_call!(fn draw_text_input(TextInput)); #[cfg(feature = "std")] forward_rendering_call!(fn draw_path(Path)); diff --git a/internal/core/renderer.rs b/internal/core/renderer.rs index 53fa0784d77..656c5d14ba8 100644 --- a/internal/core/renderer.rs +++ b/internal/core/renderer.rs @@ -36,14 +36,6 @@ pub trait RendererSealed { text_wrap: TextWrap, ) -> LogicalSize; - fn styled_text_size( - &self, - text_item: Pin<&crate::items::StyledText>, - item_rc: &crate::item_tree::ItemRc, - max_width: Option, - text_wrap: TextWrap, - ) -> LogicalSize; - /// Returns the size of the individual character in logical pixels. fn char_size( &self, diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 2a25363cf5b..d6a9456d70b 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -22,7 +22,8 @@ use crate::api::PlatformError; use crate::graphics::rendering_metrics_collector::{RefreshMode, RenderingMetricsCollector}; use crate::graphics::{BorderRadius, Rgba8Pixel, SharedImageBuffer, SharedPixelBuffer}; use crate::item_rendering::{ - CachedRenderingData, ItemRenderer, RenderBorderRectangle, RenderImage, RenderRectangle, + CachedRenderingData, ItemRenderer, PlainOrStyledText, RenderBorderRectangle, RenderImage, + RenderRectangle, }; use crate::item_tree::ItemTreeWeak; use crate::items::{ItemRc, TextOverflow, TextWrap}; @@ -743,15 +744,14 @@ impl RendererSealed for SoftwareRenderer { let font_request = text_item.font_request(item_rc); let font = fonts::match_font(&font_request, scale_factor); - match (font, parley_disabled()) { + match (font, parley_disabled(), text_item.text()) { #[cfg(feature = "software-renderer-systemfonts")] - (fonts::Font::VectorFont(_), false) => { + (fonts::Font::VectorFont(_), false, _) => { sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap) } #[cfg(feature = "software-renderer-systemfonts")] - (fonts::Font::VectorFont(vf), true) => { + (fonts::Font::VectorFont(vf), true, PlainOrStyledText::Plain(text)) => { let layout = fonts::text_layout_for_font(&vf, &font_request, scale_factor); - let text = text_item.text(); let (longest_line_width, height) = layout.text_size( text.as_str(), max_width.map(|max_width| (max_width.cast() * scale_factor).cast()), @@ -760,9 +760,8 @@ impl RendererSealed for SoftwareRenderer { (PhysicalSize::from_lengths(longest_line_width, height).cast() / scale_factor) .cast() } - (fonts::Font::PixelFont(pf), _) => { + (fonts::Font::PixelFont(pf), _, PlainOrStyledText::Plain(text)) => { let layout = fonts::text_layout_for_font(&pf, &font_request, scale_factor); - let text = text_item.text(); let (longest_line_width, height) = layout.text_size( text.as_str(), max_width.map(|max_width| (max_width.cast() * scale_factor).cast()), @@ -771,19 +770,13 @@ impl RendererSealed for SoftwareRenderer { (PhysicalSize::from_lengths(longest_line_width, height).cast() / scale_factor) .cast() } + (_, true, PlainOrStyledText::Styled(_)) + | (fonts::Font::PixelFont(_), _, PlainOrStyledText::Styled(_)) => { + panic!("Unable to get text size of styled text without parley") + } } } - fn styled_text_size( - &self, - text_item: Pin<&crate::items::StyledText>, - item_rc: &crate::item_tree::ItemRc, - max_width: Option, - text_wrap: TextWrap, - ) -> LogicalSize { - panic!() - } - fn char_size( &self, text_item: Pin<&dyn crate::item_rendering::HasFont>, @@ -2540,14 +2533,13 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T let font = fonts::match_font(&font_request, self.scale_factor); - match (font, parley_disabled()) { + match (font, parley_disabled(), text.text()) { #[cfg(feature = "software-renderer-systemfonts")] - (fonts::Font::VectorFont(_), false) => { + (fonts::Font::VectorFont(_), false, _) => { sharedparley::draw_text(self, text, Some(self_rc), size); } #[cfg(feature = "software-renderer-systemfonts")] - (fonts::Font::VectorFont(vf), true) => { - let string = text.text(); + (fonts::Font::VectorFont(vf), true, PlainOrStyledText::Plain(string)) => { if string.trim().is_empty() { return; } @@ -2587,8 +2579,7 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T self.draw_text_paragraph(¶graph, physical_clip, offset, color, None); } - (fonts::Font::PixelFont(pf), _) => { - let string = text.text(); + (fonts::Font::PixelFont(pf), _, PlainOrStyledText::Plain(string)) => { if string.trim().is_empty() { return; } @@ -2628,19 +2619,13 @@ impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T self.draw_text_paragraph(¶graph, physical_clip, offset, color, None); } + (_, true, PlainOrStyledText::Styled(_)) + | (fonts::Font::PixelFont(_), _, PlainOrStyledText::Styled(_)) => { + panic!("Unable to get draw styled text without parley") + } } } - fn draw_styled_text( - &mut self, - text: Pin<&crate::items::StyledText>, - self_rc: &ItemRc, - size: LogicalSize, - _cache: &CachedRenderingData, - ) { - panic!() - } - fn draw_text_input( &mut self, text_input: Pin<&crate::items::TextInput>, diff --git a/internal/core/textlayout/sharedparley.rs b/internal/core/textlayout/sharedparley.rs index 9832275a873..377b27ee640 100644 --- a/internal/core/textlayout/sharedparley.rs +++ b/internal/core/textlayout/sharedparley.rs @@ -13,6 +13,7 @@ use std::cell::RefCell; use crate::{ Color, SharedString, graphics::FontRequest, + item_rendering::PlainOrStyledText, items::TextStrokeStyle, lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, @@ -305,15 +306,9 @@ impl LayoutWithoutLineBreaksBuilder { } } -enum Text<'a> { - PlainText(&'a str), - #[cfg(feature = "experimental-rich-text")] - StyledText(crate::api::StyledText), -} - fn create_text_paragraphs( layout_builder: &LayoutWithoutLineBreaksBuilder, - text: Text, + text: PlainOrStyledText, selection: Option<(Range, Color)>, link_color: Color, ) -> Vec { @@ -343,7 +338,7 @@ fn create_text_paragraphs( let mut paragraphs = Vec::with_capacity(1); match text { - Text::PlainText(text) => { + PlainOrStyledText::Plain(ref text) => { let paragraph_ranges = core::iter::from_fn({ let mut start = 0; let mut char_it = text.char_indices().peekable(); @@ -374,14 +369,15 @@ fn create_text_paragraphs( )); } } - #[cfg(feature = "experimental-rich-text")] - Text::StyledText(rich_text) => { + PlainOrStyledText::Styled(rich_text) => + { + #[cfg(feature = "experimental-rich-text")] for paragraph in rich_text.paragraphs { paragraphs.push(paragraph_from_text( ¶graph.text, 0..0, paragraph.formatting, - paragraph.links.into_iter().collect(), + paragraph.links, )); } } @@ -1042,16 +1038,8 @@ pub fn draw_text( scale_factor, ); - let str = text.text(); - - #[cfg(feature = "experimental-rich-text")] - let layout_text = { Text::PlainText(&str) }; - - #[cfg(not(feature = "experimental-rich-text"))] - let layout_text = Text::PlainText(&str); - let paragraphs_without_linebreaks = - create_text_paragraphs(&layout_builder, layout_text, None, text.link_color()); + create_text_paragraphs(&layout_builder, text.text(), None, Default::default()); let (horizontal_align, vertical_align) = text.alignment(); let text_overflow = text.overflow(); @@ -1097,124 +1085,27 @@ pub fn draw_text( } } -pub fn draw_styled_text( - item_renderer: &mut impl GlyphRenderer, - text: Pin<&crate::items::StyledText>, - item_rc: Option<&crate::item_tree::ItemRc>, - size: LogicalSize, -) { - let max_width = size.width_length(); - let max_height = size.height_length(); - - if max_width.get() <= 0. || max_height.get() <= 0. { - return; - } - - let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size) - else { - // Nothing to draw - return; - }; - - let scale_factor = ScaleFactor::new(item_renderer.scale_factor()); - - let (stroke_brush, stroke_width, stroke_style) = - (text.stroke(), text.stroke_width(), text.stroke_style()); - let platform_stroke_brush = if !stroke_brush.is_transparent() { - let stroke_width = if stroke_width.get() != 0.0 { - (stroke_width * scale_factor).get() - } else { - // Hairline stroke - 1.0 - }; - let stroke_width = match stroke_style { - TextStrokeStyle::Outside => stroke_width * 2.0, - TextStrokeStyle::Center => stroke_width, - }; - item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size) - } else { - None - }; - - let layout_builder = LayoutWithoutLineBreaksBuilder::new( - None, //item_rc.map(|item_rc| text.font_request(item_rc)), - text.wrap(), - platform_stroke_brush.is_some().then_some(stroke_style), - scale_factor, - ); - - let layout_text = { Text::StyledText(text.text()) }; - - let paragraphs_without_linebreaks = - create_text_paragraphs(&layout_builder, layout_text, None, text.link_color()); - - let (horizontal_align, vertical_align) = - (text.horizontal_alignment(), text.vertical_alignment()); - let text_overflow = text.overflow(); - - let layout = layout( - &layout_builder, - paragraphs_without_linebreaks, - scale_factor, - LayoutOptions { - horizontal_align, - vertical_align, - max_height: Some(max_height), - max_width: Some(max_width), - text_overflow: text.overflow(), - }, - ); - - let render = if text_overflow == TextOverflow::Clip { - item_renderer.save_state(); - - item_renderer.combine_clip( - LogicalRect::new(LogicalPoint::default(), size), - LogicalBorderRadius::zero(), - LogicalLength::zero(), - ) - } else { - true - }; - - if render { - layout.draw( - item_renderer, - platform_fill_brush, - platform_stroke_brush, - &mut |item_renderer, font, font_size, brush, y_offset, glyphs_it| { - item_renderer.draw_glyph_run(font, font_size, brush, y_offset, glyphs_it); - }, - ); - } - - if text_overflow == TextOverflow::Clip { - item_renderer.restore_state(); - } -} - #[cfg(feature = "experimental-rich-text")] pub fn link_under_cursor( scale_factor: ScaleFactor, - text: Pin<&crate::items::StyledText>, + text: Pin<&dyn crate::item_rendering::RenderText>, item_rc: &crate::item_tree::ItemRc, size: LogicalSize, cursor: PhysicalPoint, ) -> Option { let layout_builder = LayoutWithoutLineBreaksBuilder::new( - None, //Some(text.font_request(item_rc)), + Some(text.font_request(item_rc)), text.wrap(), None, scale_factor, ); - let layout_text = { Text::StyledText(text.text()) }; + let layout_text = text.text(); let paragraphs_without_linebreaks = create_text_paragraphs(&layout_builder, layout_text, None, text.link_color()); - let (horizontal_align, vertical_align) = - (text.horizontal_alignment(), text.vertical_alignment()); + let (horizontal_align, vertical_align) = text.alignment(); let layout = layout( &layout_builder, @@ -1306,7 +1197,7 @@ pub fn draw_text_input( let paragraphs_without_linebreaks = create_text_paragraphs( &layout_builder, - Text::PlainText(&text), + PlainOrStyledText::Plain(text), selection_and_color, Color::default(), ); @@ -1373,50 +1264,8 @@ pub fn text_size( let text = text_item.text(); - let paragraphs_without_linebreaks = create_text_paragraphs( - &layout_builder, - Text::PlainText(text.as_str()), - None, - Color::default(), - ); - - let layout = layout( - &layout_builder, - paragraphs_without_linebreaks, - scale_factor, - LayoutOptions { - max_width, - max_height: None, - horizontal_align: TextHorizontalAlignment::Left, - vertical_align: TextVerticalAlignment::Top, - text_overflow: TextOverflow::Clip, - }, - ); - PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor -} - -pub fn styled_text_size( - renderer: &dyn RendererSealed, - text_item: Pin<&crate::items::StyledText>, - item_rc: &crate::item_tree::ItemRc, - max_width: Option, - text_wrap: TextWrap, -) -> LogicalSize { - let Some(scale_factor) = renderer.scale_factor() else { - return LogicalSize::default(); - }; - - let layout_builder = LayoutWithoutLineBreaksBuilder::new( - None, //Some(text_item.font_request(item_rc)), - text_wrap, - None, - scale_factor, - ); - - let layout_text = { Text::StyledText(text_item.text()) }; - let paragraphs_without_linebreaks = - create_text_paragraphs(&layout_builder, layout_text, None, Color::default()); + create_text_paragraphs(&layout_builder, text, None, Color::default()); let layout = layout( &layout_builder, @@ -1512,8 +1361,12 @@ pub fn text_input_byte_offset_for_position( ); let text = text_input.text(); - let paragraphs_without_linebreaks = - create_text_paragraphs(&layout_builder, Text::PlainText(&text), None, Color::default()); + let paragraphs_without_linebreaks = create_text_paragraphs( + &layout_builder, + PlainOrStyledText::Plain(text), + None, + Color::default(), + ); let layout = layout( &layout_builder, @@ -1553,8 +1406,12 @@ pub fn text_input_cursor_rect_for_byte_offset( } let text = text_input.text(); - let paragraphs_without_linebreaks = - create_text_paragraphs(&layout_builder, Text::PlainText(&text), None, Color::default()); + let paragraphs_without_linebreaks = create_text_paragraphs( + &layout_builder, + PlainOrStyledText::Plain(text), + None, + Color::default(), + ); let layout = layout( &layout_builder, diff --git a/internal/renderers/femtovg/itemrenderer.rs b/internal/renderers/femtovg/itemrenderer.rs index 006a68763fd..4a4ca9f912f 100644 --- a/internal/renderers/femtovg/itemrenderer.rs +++ b/internal/renderers/femtovg/itemrenderer.rs @@ -340,16 +340,6 @@ impl<'a, R: femtovg::Renderer + TextureImporter> ItemRenderer for GLItemRenderer sharedparley::draw_text(self, text, Some(self_rc), size); } - fn draw_styled_text( - &mut self, - text: Pin<&i_slint_core::items::StyledText>, - self_rc: &ItemRc, - size: LogicalSize, - _cache: &CachedRenderingData, - ) { - panic!() - } - fn draw_text_input( &mut self, text_input: Pin<&items::TextInput>, diff --git a/internal/renderers/femtovg/lib.rs b/internal/renderers/femtovg/lib.rs index ced09c43f1a..295834ab5c2 100644 --- a/internal/renderers/femtovg/lib.rs +++ b/internal/renderers/femtovg/lib.rs @@ -292,16 +292,6 @@ impl RendererSealed for FemtoVGRenderer { sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap) } - fn styled_text_size( - &self, - text_item: Pin<&i_slint_core::items::StyledText>, - item_rc: &ItemRc, - max_width: Option, - text_wrap: TextWrap, - ) -> LogicalSize { - panic!() - } - fn char_size( &self, text_item: Pin<&dyn i_slint_core::item_rendering::HasFont>, From d2fc4b521d8d9d5bdf1098657f35f32a4233478c Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Tue, 18 Nov 2025 15:12:47 +0100 Subject: [PATCH 12/20] Add allow missing docs to stuff --- internal/core/api.rs | 6 ++++++ internal/core/item_rendering.rs | 2 +- internal/core/textlayout/sharedparley.rs | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/core/api.rs b/internal/core/api.rs index 9cfd82ec289..decc407ad79 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1221,6 +1221,7 @@ pub fn set_xdg_app_id(app_id: impl Into) -> Result<(), PlatformErr } #[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] pub enum Style { Emphasis, Strong, @@ -1232,6 +1233,7 @@ pub enum Style { } #[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] pub struct FormattedSpan { pub range: std::ops::Range, pub style: Style, @@ -1246,6 +1248,7 @@ enum ListItemType { #[cfg(feature = "experimental-rich-text")] #[derive(Clone, Debug, PartialEq)] +#[allow(missing_docs)] pub struct StyledTextParagraph { pub text: std::string::String, pub formatting: std::vec::Vec, @@ -1254,6 +1257,7 @@ pub struct StyledTextParagraph { #[cfg(feature = "experimental-rich-text")] #[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] pub enum StyledTextError<'a> { #[error("Spans are unbalanced: stack already empty when popped")] Pop, @@ -1279,12 +1283,14 @@ pub enum StyledTextError<'a> { /// Internal styled text type #[derive(Debug, PartialEq, Clone, Default)] +#[allow(missing_docs)] pub struct StyledText { #[cfg(feature = "experimental-rich-text")] pub paragraphs: std::vec::Vec, } #[cfg(feature = "experimental-rich-text")] +#[allow(missing_docs)] impl StyledText { fn begin_paragraph(&mut self, indentation: u32, list_item_type: Option) { let mut text = std::string::String::with_capacity(indentation as usize * 4); diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index d5ac6abd30f..45dfdc9731e 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -8,7 +8,6 @@ use super::items::*; use crate::graphics::{Color, FontRequest, Image, IntRect}; use crate::item_tree::ItemTreeRc; use crate::item_tree::{ItemVisitor, ItemVisitorVTable, VisitChildrenResult}; -use crate::items::StyledText; use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, }; @@ -293,6 +292,7 @@ pub trait HasFont { fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest; } +#[allow(missing_docs)] pub enum PlainOrStyledText { Plain(SharedString), Styled(crate::api::StyledText), diff --git a/internal/core/textlayout/sharedparley.rs b/internal/core/textlayout/sharedparley.rs index 377b27ee640..5983315864f 100644 --- a/internal/core/textlayout/sharedparley.rs +++ b/internal/core/textlayout/sharedparley.rs @@ -369,6 +369,7 @@ fn create_text_paragraphs( )); } } + #[cfg_attr(not(feature = "experimental-rich-text"), allow(unused))] PlainOrStyledText::Styled(rich_text) => { #[cfg(feature = "experimental-rich-text")] From 70bb110d3f8c29ee9bd41962b63ff45091ab4023 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Tue, 18 Nov 2025 15:16:17 +0100 Subject: [PATCH 13/20] Fix testing backend thing --- internal/backends/testing/testing_backend.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/backends/testing/testing_backend.rs b/internal/backends/testing/testing_backend.rs index 02fbfa46609..1534e2c76d0 100644 --- a/internal/backends/testing/testing_backend.rs +++ b/internal/backends/testing/testing_backend.rs @@ -3,6 +3,7 @@ use i_slint_core::api::PhysicalSize; use i_slint_core::graphics::euclid::{Point2D, Size2D}; +use i_slint_core::item_rendering::PlainOrStyledText; use i_slint_core::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize}; use i_slint_core::platform::PlatformError; use i_slint_core::renderer::{Renderer, RendererSealed}; @@ -164,7 +165,11 @@ impl RendererSealed for TestingWindow { _max_width: Option, _text_wrap: TextWrap, ) -> LogicalSize { - LogicalSize::new(text_item.text().len() as f32 * 10., 10.) + if let PlainOrStyledText::Plain(text) = text_item.text() { + LogicalSize::new(text.len() as f32 * 10., 10.) + } else { + Default::default() + } } fn char_size( From 9cca35aca64bef416940327c517058432eeb8848 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Tue, 18 Nov 2025 18:29:35 +0100 Subject: [PATCH 14/20] Move test --- internal/core/api.rs | 176 +++++++++++++++++++++++ internal/core/textlayout/sharedparley.rs | 161 --------------------- 2 files changed, 176 insertions(+), 161 deletions(-) diff --git a/internal/core/api.rs b/internal/core/api.rs index decc407ad79..73e609aeb94 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1574,3 +1574,179 @@ impl StyledText { Ok(styled_text) } } + +#[cfg(feature = "experimental-rich-text")] +#[test] +fn markdown_parsing() { + assert_eq!( + StyledText::parse("hello *world*").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse( + " +- line 1 +- line 2 + " + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "• line 1".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "• line 2".into(), + formatting: std::vec![], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +1. a +2. b +4. c + " + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "1. a".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "2. b".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: "3. c".into(), + formatting: std::vec![], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +Normal _italic_ **strong** ~~strikethrough~~ `code` +new *line* +" + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "Normal italic strong strikethrough code".into(), + formatting: std::vec![ + FormattedSpan { range: 7..13, style: Style::Emphasis }, + FormattedSpan { range: 14..20, style: Style::Strong }, + FormattedSpan { range: 21..34, style: Style::Strikethrough }, + FormattedSpan { range: 35..39, style: Style::Code } + ], + links: std::vec![] + }, + StyledTextParagraph { + text: "new line".into(), + formatting: std::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], + links: std::vec![] + } + ] + ); + + assert_eq!( + StyledText::parse( + " +- root + - child + - grandchild + - great grandchild +" + ) + .unwrap() + .paragraphs, + [ + StyledTextParagraph { + text: "• root".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " ◦ child".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " ▪ grandchild".into(), + formatting: std::vec![], + links: std::vec![] + }, + StyledTextParagraph { + text: " • great grandchild".into(), + formatting: std::vec![], + links: std::vec![] + }, + ] + ); + + assert_eq!( + StyledText::parse("hello [*world*](https://example.com)").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![ + FormattedSpan { range: 6..11, style: Style::Emphasis }, + FormattedSpan { range: 6..11, style: Style::Link } + ], + links: std::vec![(6..11, "https://example.com".into())] + }] + ); + + assert_eq!( + StyledText::parse("hello world").unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { range: 0..11, style: Style::Underline },], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse(r#"hello world"#).unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![FormattedSpan { + range: 0..11, + style: Style::Color(crate::Color::from_rgb_u8(0, 0, 255)) + },], + links: std::vec![] + }] + ); + + assert_eq!( + StyledText::parse(r#"hello world"#).unwrap().paragraphs, + [StyledTextParagraph { + text: "hello world".into(), + formatting: std::vec![ + FormattedSpan { + range: 0..11, + style: Style::Color(crate::Color::from_rgb_u8(255, 0, 0)) + }, + FormattedSpan { range: 0..11, style: Style::Underline }, + ], + links: std::vec![] + }] + ); +} diff --git a/internal/core/textlayout/sharedparley.rs b/internal/core/textlayout/sharedparley.rs index 5983315864f..fbbe38b8424 100644 --- a/internal/core/textlayout/sharedparley.rs +++ b/internal/core/textlayout/sharedparley.rs @@ -833,167 +833,6 @@ impl Layout { } } -#[cfg(feature = "experimental-rich-text")] -#[test] -fn markdown_parsing() { - assert_eq!( - parse_markdown("hello *world*").unwrap().paragraphs, - [RichTextParagraph { - text: "hello world".into(), - formatting: std::vec![FormattedSpan { range: 6..11, style: Style::Emphasis }], - links: std::vec![] - }] - ); - - assert_eq!( - parse_markdown( - " -- line 1 -- line 2 - " - ) - .unwrap() - .paragraphs, - [ - RichTextParagraph { - text: "• line 1".into(), - formatting: std::vec![], - links: std::vec![] - }, - RichTextParagraph { - text: "• line 2".into(), - formatting: std::vec![], - links: std::vec![] - } - ] - ); - - assert_eq!( - parse_markdown( - " -1. a -2. b -4. c - " - ) - .unwrap() - .paragraphs, - [ - RichTextParagraph { text: "1. a".into(), formatting: std::vec![], links: std::vec![] }, - RichTextParagraph { text: "2. b".into(), formatting: std::vec![], links: std::vec![] }, - RichTextParagraph { text: "3. c".into(), formatting: std::vec![], links: std::vec![] } - ] - ); - - assert_eq!( - parse_markdown( - " -Normal _italic_ **strong** ~~strikethrough~~ `code` -new *line* -" - ) - .unwrap() - .paragraphs, - [ - RichTextParagraph { - text: "Normal italic strong strikethrough code".into(), - formatting: std::vec![ - FormattedSpan { range: 7..13, style: Style::Emphasis }, - FormattedSpan { range: 14..20, style: Style::Strong }, - FormattedSpan { range: 21..34, style: Style::Strikethrough }, - FormattedSpan { range: 35..39, style: Style::Code } - ], - links: std::vec![] - }, - RichTextParagraph { - text: "new line".into(), - formatting: std::vec![FormattedSpan { range: 4..8, style: Style::Emphasis },], - links: std::vec![] - } - ] - ); - - assert_eq!( - parse_markdown( - " -- root - - child - - grandchild - - great grandchild -" - ) - .unwrap() - .paragraphs, - [ - RichTextParagraph { - text: "• root".into(), - formatting: std::vec![], - links: std::vec![] - }, - RichTextParagraph { - text: " ◦ child".into(), - formatting: std::vec![], - links: std::vec![] - }, - RichTextParagraph { - text: " ▪ grandchild".into(), - formatting: std::vec![], - links: std::vec![] - }, - RichTextParagraph { - text: " • great grandchild".into(), - formatting: std::vec![], - links: std::vec![] - }, - ] - ); - - assert_eq!( - parse_markdown("hello [*world*](https://example.com)").unwrap().paragraphs, - [RichTextParagraph { - text: "hello world".into(), - formatting: std::vec![ - FormattedSpan { range: 6..11, style: Style::Emphasis }, - FormattedSpan { range: 6..11, style: Style::Link } - ], - links: std::vec![(6..11, pulldown_cmark::CowStr::Borrowed("https://example.com"))] - }] - ); - - assert_eq!( - parse_markdown("hello world").unwrap().paragraphs, - [RichTextParagraph { - text: "hello world".into(), - formatting: std::vec![FormattedSpan { range: 0..11, style: Style::Underline },], - links: std::vec![] - }] - ); - - assert_eq!( - parse_markdown(r#"hello world"#).unwrap().paragraphs, - [RichTextParagraph { - text: "hello world".into(), - formatting: std::vec![FormattedSpan { - range: 0..11, - style: Style::Color(Color::from_rgb_u8(0, 0, 255)) - },], - links: std::vec![] - }] - ); - - assert_eq!( - parse_markdown(r#"hello world"#).unwrap().paragraphs, - [RichTextParagraph { - text: "hello world".into(), - formatting: std::vec![ - FormattedSpan { range: 0..11, style: Style::Color(Color::from_rgb_u8(255, 0, 0)) }, - FormattedSpan { range: 0..11, style: Style::Underline }, - ], - links: std::vec![] - }] - ); -} - pub fn draw_text( item_renderer: &mut impl GlyphRenderer, text: Pin<&dyn crate::item_rendering::RenderText>, From 31841321ddb4c7aff414e2e19eac47dc9973dfd4 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Tue, 18 Nov 2025 18:38:56 +0100 Subject: [PATCH 15/20] Don't use std:: for certain types --- internal/core/api.rs | 2 +- internal/core/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/core/api.rs b/internal/core/api.rs index 73e609aeb94..180357d540e 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -1235,7 +1235,7 @@ pub enum Style { #[derive(Clone, Debug, PartialEq)] #[allow(missing_docs)] pub struct FormattedSpan { - pub range: std::ops::Range, + pub range: core::ops::Range, pub style: Style, } diff --git a/internal/core/lib.rs b/internal/core/lib.rs index 09e446f636b..535948b9dcc 100644 --- a/internal/core/lib.rs +++ b/internal/core/lib.rs @@ -185,8 +185,8 @@ pub fn open_url(url: &str) { } } -pub fn escape_markdown(text: &str) -> std::string::String { - let mut out = std::string::String::with_capacity(text.len()); +pub fn escape_markdown(text: &str) -> alloc::string::String { + let mut out = alloc::string::String::with_capacity(text.len()); for c in text.chars() { match c { From 264527c4f013e2af4a196bbcc7d085f52c1d6ec1 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Tue, 18 Nov 2025 18:55:56 +0100 Subject: [PATCH 16/20] Fix api/node --- api/node/rust/interpreter/value.rs | 5 ++++- internal/interpreter/api.rs | 6 ++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/node/rust/interpreter/value.rs b/api/node/rust/interpreter/value.rs index 4b2bb0db15e..2a675e223a3 100644 --- a/api/node/rust/interpreter/value.rs +++ b/api/node/rust/interpreter/value.rs @@ -25,6 +25,7 @@ pub enum JsValueType { Struct, Brush, Image, + StyledText, } impl From for JsValueType { @@ -37,6 +38,7 @@ impl From for JsValueType { slint_interpreter::ValueType::Struct => JsValueType::Struct, slint_interpreter::ValueType::Brush => JsValueType::Brush, slint_interpreter::ValueType::Image => JsValueType::Image, + slint_interpreter::ValueType::StyledText => JsValueType::StyledText, _ => JsValueType::Void, } } @@ -290,7 +292,8 @@ pub fn to_value(env: &Env, unknown: JsUnknown, typ: &Type) -> Result { | Type::Easing | Type::PathData | Type::LayoutCache - | Type::ElementReference => Err(napi::Error::from_reason("reason")), + | Type::ElementReference + | Type::StyledText => Err(napi::Error::from_reason("reason")), } } diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index 877c13a5bac..d3bbb727e37 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -18,16 +18,14 @@ use std::rc::Rc; #[doc(inline)] pub use i_slint_compiler::diagnostics::{Diagnostic, DiagnosticLevel}; -pub use i_slint_core::api::{ - ComponentHandle, EventLoopError, Global, JoinHandle, PlatformError, SharedString, Weak, Window, -}; +pub use i_slint_core::api::*; // keep in sync with api/rs/slint/lib.rs pub use i_slint_backend_selector::api::*; use i_slint_core::api; pub use i_slint_core::graphics::{ Brush, Color, Image, LoadImageError, Rgb8Pixel, Rgba8Pixel, RgbaColor, SharedPixelBuffer, }; -use i_slint_core::items::*; +use i_slint_core::items::{MenuEntry, ItemTreeVTable, FontMetrics,StateInfo,TableColumn,StandardListViewItem,DropEvent,KeyEvent,PointerEvent,PointerScrollEvent,KeyboardModifiers}; use crate::dynamic_item_tree::{ErasedItemTreeBox, WindowOptions}; From aa668989f2884be36010e3b27d4719ab4eee23f1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:58:36 +0000 Subject: [PATCH 17/20] [autofix.ci] apply automated fixes --- internal/interpreter/api.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index d3bbb727e37..9a5bc70e69f 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -25,7 +25,10 @@ use i_slint_core::api; pub use i_slint_core::graphics::{ Brush, Color, Image, LoadImageError, Rgb8Pixel, Rgba8Pixel, RgbaColor, SharedPixelBuffer, }; -use i_slint_core::items::{MenuEntry, ItemTreeVTable, FontMetrics,StateInfo,TableColumn,StandardListViewItem,DropEvent,KeyEvent,PointerEvent,PointerScrollEvent,KeyboardModifiers}; +use i_slint_core::items::{ + DropEvent, FontMetrics, ItemTreeVTable, KeyEvent, KeyboardModifiers, MenuEntry, PointerEvent, + PointerScrollEvent, StandardListViewItem, StateInfo, TableColumn, +}; use crate::dynamic_item_tree::{ErasedItemTreeBox, WindowOptions}; From 8a9bfcfb0aa2abb0a8ec379472f2a63c46aa3b46 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Wed, 19 Nov 2025 16:38:15 +0100 Subject: [PATCH 18/20] Rename to StyledTextItem --- api/cpp/cbindgen.rs | 2 +- internal/compiler/builtins.slint | 4 +++- internal/core/items.rs | 2 +- internal/core/items/text.rs | 18 +++++++++--------- internal/interpreter/dynamic_item_tree.rs | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index 515be6b289a..ff2145db250 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -305,7 +305,7 @@ fn gen_corelib( "Flickable", "SimpleText", "ComplexText", - "StyledText", + "StyledTextItem", "Path", "WindowItem", "TextInput", diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint index fb6e9c3c47d..b28261cc075 100644 --- a/internal/compiler/builtins.slint +++ b/internal/compiler/builtins.slint @@ -120,7 +120,7 @@ component ComplexText inherits SimpleText { export { ComplexText as Text } -export component StyledText inherits Empty { +component StyledTextItem inherits Empty { in property width; in property height; in property text; @@ -143,6 +143,8 @@ export component StyledText inherits Empty { //-default_size_binding:implicit_size } +export { StyledTextItem as StyledText } + export component TouchArea { in property enabled: true; out property pressed; diff --git a/internal/core/items.rs b/internal/core/items.rs index b8fc24f6991..f1851d4e978 100644 --- a/internal/core/items.rs +++ b/internal/core/items.rs @@ -1729,7 +1729,7 @@ declare_item_vtable! { } declare_item_vtable! { - fn slint_get_StyledTextVTable() -> StyledTextVTable for StyledText + fn slint_get_StyledTextItemVTable() -> StyledTextItemVTable for StyledTextItem } declare_item_vtable! { diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index 051951583b1..b40177dd567 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -228,7 +228,7 @@ impl ComplexText { #[repr(C)] #[derive(FieldOffsets, Default, SlintElement)] #[pin] -pub struct StyledText { +pub struct StyledTextItem { pub width: Property, pub height: Property, pub text: Property, @@ -251,7 +251,7 @@ pub struct StyledText { pub cached_rendering_data: CachedRenderingData, } -impl Item for StyledText { +impl Item for StyledTextItem { fn init(self: Pin<&Self>, _self_rc: &ItemRc) {} fn layout_info( @@ -371,14 +371,14 @@ impl Item for StyledText { } } -impl ItemConsts for StyledText { +impl ItemConsts for StyledTextItem { const cached_rendering_data_offset: const_field_offset::FieldOffset< - StyledText, + StyledTextItem, CachedRenderingData, - > = StyledText::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); + > = StyledTextItem::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection(); } -impl HasFont for StyledText { +impl HasFont for StyledTextItem { fn font_request(self: Pin<&Self>, self_rc: &crate::items::ItemRc) -> FontRequest { crate::items::WindowItem::resolved_font_request( self_rc, @@ -391,13 +391,13 @@ impl HasFont for StyledText { } } -impl RenderString for StyledText { +impl RenderString for StyledTextItem { fn text(self: Pin<&Self>) -> PlainOrStyledText { PlainOrStyledText::Styled(self.text()) } } -impl RenderText for StyledText { +impl RenderText for StyledTextItem { fn target_size(self: Pin<&Self>) -> LogicalSize { LogicalSize::from_lengths(self.width(), self.height()) } @@ -433,7 +433,7 @@ impl RenderText for StyledText { } } -impl StyledText { +impl StyledTextItem { pub fn font_metrics( self: Pin<&Self>, window_adapter: &Rc, diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index 306cb545f06..4c358ecf81a 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -986,7 +986,7 @@ fn generate_rtti() -> HashMap<&'static str, Rc> { rtti_for::(), rtti_for::(), rtti_for::(), - rtti_for::(), + rtti_for::(), rtti_for::(), rtti_for::(), rtti_for::(), From feac54a29d0da6a5f59061efdf3f0de8965f2e47 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Wed, 19 Nov 2025 19:29:51 +0100 Subject: [PATCH 19/20] Ignore styledtext in cpp for now --- api/cpp/cbindgen.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index ff2145db250..7c82bf908be 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -305,7 +305,6 @@ fn gen_corelib( "Flickable", "SimpleText", "ComplexText", - "StyledTextItem", "Path", "WindowItem", "TextInput", From bce8665d38e1297a18a7c15cb180338a2ff93883 Mon Sep 17 00:00:00 2001 From: Ashley Ruglys Date: Fri, 21 Nov 2025 15:23:20 +0100 Subject: [PATCH 20/20] Remove whitespace change --- internal/interpreter/api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/interpreter/api.rs b/internal/interpreter/api.rs index e3ffba7a594..a0dd765072b 100644 --- a/internal/interpreter/api.rs +++ b/internal/interpreter/api.rs @@ -19,6 +19,7 @@ use std::rc::Rc; #[doc(inline)] pub use i_slint_compiler::diagnostics::{Diagnostic, DiagnosticLevel}; + // keep in sync with api/rs/slint/lib.rs pub use i_slint_backend_selector::api::*; #[cfg(feature = "std")]