Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rich text support #295

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Rich text support #295

wants to merge 2 commits into from

Conversation

rhelmot
Copy link
Collaborator

@rhelmot rhelmot commented Jan 25, 2023

Adds rich text to labels, and any custom view that wants to make its text rich by figuring out what indices should be what styles.

Essentially, by constructing child TextSpan views as children of an entity with text set, the spans described in the TextSpan view will be styled according to the child element's style. Supported properties right now are color, font-family, font-style, and font-weight.

High level support for labels is supported at two levels. At the ftl level, there are functions you can call to insert markup into text, and a function Label::new_rich_formatted to parse this markup. The ftl functions are fairly self-explanatory based on the example, and the markup is pretty simple so could write it by hand in a pinch.

Copy link
Collaborator

@geom3trik geom3trik left a comment

Choose a reason for hiding this comment

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

Overall looking good to me. I'm still in two minds about the use of fluent functions and the simple markup you've made. I know you're not a fan of html/xml tags but in theory, with the xml parsing crate you used for the DSL stuff, how hard would it be to add a parsing stage to the fluent ftl? For example, how hard would it be parse: here is some <span class="bold">bold text</span> or something similar? (not saying to actually do that in this PR but I just want to get your thoughts on it).

On the rust side of things I'm still deciding on a good API for declarative rich text (that isn't just parsed from the string) but it will probably look something like:

RichText::new(cx, |cx|{
    TextSpan::new("here is some");
    TextSpan::new("bold text").class("bold");
});

Or maybe using the trait + operators method:

RichText::new(cx, 
    "here is some" + "bold text".class("bold")
);

In any case, I think we need some low level API, probably crate private, that allows us to apply attributes directly to spans. Something like:

Label::new(cx, "here is some bold text").attributes(span, attr);

where span is the cursor span and attr is a collection of inline text attributes to apply to the span and/or class/id names to add to the entities that get generated for each span. This is essentially what you already have done but exposed at a lower level so you don't have to go through the parser you've made. What do you think?

let base_attrs = Self::extract_attrs(buf.font_system(), entity, style);
let child_attrs = TreeIterator::subtree(tree, entity)
.filter_map(|child_entity| {
views.get(&child_entity).and_then(|view| view.downcast_ref::<TextSpan>()).map(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if maybe the cursor spans should just be stored in a sparseset within style? That would remove the need for downcasting views to retrieve the spans.

Comment on lines +4 to +16
#[derive(Copy, Clone)]
pub struct TextSpan {
pub cursor_start: Cursor,
pub cursor_end: Cursor,
}

impl TextSpan {
pub fn new(cx: &mut Context, cursor_start: Cursor, cursor_end: Cursor) -> Handle<Self> {
Self { cursor_end, cursor_start }.build(cx, |_| {})
}
}

impl View for TextSpan {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm in two minds whether it's a good idea or not to have these text spans as actual views. We obviously need the entity IDs and for them to exist in the tree so they can undergo style matching and so that the spans can be retrieved during style syncing, but if we're not careful these views will end up getting iterated during data binding and layout. One option is to manually create the entities and add them to the tree, or if we do want to keep them as views then we should add properties to them to make sure they are ignored by binding & layout.

@@ -113,6 +114,79 @@ impl ResourceManager {
.expect("Failed to parse translation as FTL");
let bundle =
self.translations.entry(lang.clone()).or_insert_with(|| FluentBundle::new(vec![lang]));
bundle
.add_function("CLASS", |args, _| {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not familiar with the syntax for defining new fluent functions, so what's the difference between CLASS and WITH_CLASS?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

CLASS("xx") just inserts the begin tag and needs to be followed by an END(). WITH_CLASS inserts the begin tag, its child text, and then the end tag.

@nicoburns
Copy link

In any case, I think we need some low level API, probably crate private, that allows us to apply attributes directly to spans

Is there any reason to make this private? If it's public then you open up the possibility of alternate high-level APIs in 3rd party crates. It could potentially go in a seperate impl block to make sure it ends up below other methods in the documentation?

@geom3trik
Copy link
Collaborator

In any case, I think we need some low level API, probably crate private, that allows us to apply attributes directly to spans

Is there any reason to make this private? If it's public then you open up the possibility of alternate high-level APIs in 3rd party crates. It could potentially go in a seperate impl block to make sure it ends up below other methods in the documentation?

I have to admit I haven't put a lot of thought into 3rd party integration with vizia yet. In theory exposing this API to allow for 3rd party crates is probably a good idea, but my main concern is that I don't want to have too many ways to do the same thing for regular users. At the moment there are a lot of internal details that are exposed publicly and I'm already seeing users (and we don't even have many yet) using them the wrong way (e.g. extracting views from the context and downcasting them to manipulate local view data, bypassing the binding/events systems). Perhaps some good docs and guides will mitigate this issue to some extent.

});
}
Handle { entity, p: PhantomData::<Self>::default(), cx }.text(&text);
println!("SO true bestie {:?}", entity);
Copy link
Contributor

Choose a reason for hiding this comment

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

This probably shouldn't be here. 😉

@geom3trik
Copy link
Collaborator

I've been thinking some more about this and I think I'd like to add a modifier to views to allow specifying styled text runs with a vector of (span, style). My thinking is that things like the output of syntax highlighting algorithms, or fuzzy search algorithms, will often output the results as an array of spans. This way we can do those computations in a model and then bind it to the modifier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants