Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion api/cpp/cbindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ fn gen_corelib(
"Flickable",
"SimpleText",
"ComplexText",
"MarkdownText",
"Path",
"WindowItem",
"TextInput",
Expand Down
7 changes: 6 additions & 1 deletion internal/backends/testing/testing_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -164,7 +165,11 @@ impl RendererSealed for TestingWindow {
_max_width: Option<LogicalLength>,
_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(
Expand Down
6 changes: 4 additions & 2 deletions internal/compiler/builtins.slint
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ component ComplexText inherits SimpleText {

export { ComplexText as Text }

export component MarkdownText inherits Empty {
component StyledTextItem inherits Empty {
in property <length> width;
in property <length> height;
in property <string> text;
in property <styled-text> text;
in property <length> font-size;
in property <int> font-weight;
in property <brush> color;
Expand All @@ -143,6 +143,8 @@ export component MarkdownText inherits Empty {
//-default_size_binding:implicit_size
}

export { StyledTextItem as StyledText }

export component TouchArea {
in property <bool> enabled: true;
out property <bool> pressed;
Expand Down
8 changes: 7 additions & 1 deletion internal/compiler/expression_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ pub enum BuiltinFunction {
StopTimer,
RestartTimer,
OpenUrl,
ParseMarkdown,
EscapeMarkdown,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -273,7 +275,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::StyledText
);

impl BuiltinFunction {
Expand Down Expand Up @@ -370,6 +374,7 @@ impl BuiltinFunction {
BuiltinFunction::StopTimer => false,
BuiltinFunction::RestartTimer => false,
BuiltinFunction::OpenUrl => false,
BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => false,
}
}

Expand Down Expand Up @@ -448,6 +453,7 @@ impl BuiltinFunction {
BuiltinFunction::StopTimer => false,
BuiltinFunction::RestartTimer => false,
BuiltinFunction::OpenUrl => false,
BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => true,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions internal/compiler/generator/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4213,6 +4213,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)
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions internal/compiler/generator/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3383,6 +3383,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))
}
}
}

Expand Down
1 change: 1 addition & 0 deletions internal/compiler/llr/optim_passes/inline_expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::StopTimer => 10,
BuiltinFunction::RestartTimer => 10,
BuiltinFunction::OpenUrl => isize::MAX,
BuiltinFunction::ParseMarkdown | BuiltinFunction::EscapeMarkdown => isize::MAX,
}
}

Expand Down
1 change: 1 addition & 0 deletions internal/compiler/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions internal/compiler/parser/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -438,6 +441,37 @@ fn parse_tr(p: &mut impl Parser) {
p.expect(SyntaxKind::RParent);
}

fn parse_markdown(p: &mut impl Parser) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some basic tests to this function. You can see with parse_tr() right above that there's a framework for that. That way we can catch any future refactoring mistakes earlier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also test errors in similar in a syntax test, similar to internal/compiler/tests/syntax/basic/tr.slint and tr2.slint
https://github.com/slint-ui/slint/blob/master/docs/testing.md#syntax-tests

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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/passes/embed_glyphs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
138 changes: 138 additions & 0 deletions internal/compiler/passes/resolving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -745,6 +746,143 @@ impl Expression {
}
}

fn from_at_markdown(node: syntax_nodes::AtMarkdown, ctx: &mut LookupCtx) -> Expression {
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,
&n,
ctx.diag,
)
});
let values = subs.collect::<Vec<_>>();

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];
let argument_index = if argument.is_empty() {
let argument_index = arg_idx;
arg_idx += 1;
argument_index
} else if let Ok(n) = argument.parse::<u16>() {
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(),
)),
op: '+',
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,
Some(expr) => Expression::BinaryExpression {
lhs: Box::new(expr),
op: '+',
rhs: Box::new(add),
},
});
pos = end + 1;
literal_start_pos = pos;
}
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 {
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(),
&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,
);
}
}

Expression::FunctionCall {
function: BuiltinFunction::ParseMarkdown.into(),
arguments: vec![
expr.unwrap_or_else(|| Expression::default_value_for_type(&Type::String)),
],
source_location: Some(node.to_source_location()),
}
}

fn from_at_tr(node: syntax_nodes::AtTr, ctx: &mut LookupCtx) -> Expression {
let Some(string) = node
.child_text(SyntaxKind::StringLiteral)
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/typeregister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to keep the feature internal, we also need to remove the "styled-text" from register.types


Rc::new(RefCell::new(register))
}
Expand Down
12 changes: 9 additions & 3 deletions internal/core/item_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,16 @@ 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),
}

/// 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
Expand Down Expand Up @@ -329,8 +335,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())
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/core/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1729,7 +1729,7 @@ declare_item_vtable! {
}

declare_item_vtable! {
fn slint_get_MarkdownTextVTable() -> MarkdownTextVTable for MarkdownText
fn slint_get_StyledTextItemVTable() -> StyledTextItemVTable for StyledTextItem
}

declare_item_vtable! {
Expand Down
Loading
Loading