-
Notifications
You must be signed in to change notification settings - Fork 111
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
Add examples of text layouting #216
Comments
I've been thinking about this a bit recently, and I totally agree. I was thinking that perhaps a fully-working example, but with a monospace font (which would avoid complication around text shaping) might be a good way to approach this. FWIW, the quick high level answer is:
If you're looking for code that does actually implement full-on text layout (including for variable-width fonts) in Rust, then you may want to look at https://github.com/dfrg/parley or https://github.com/dfrg/swash_demo. But be aware that this not simple to do at all, and the code is therefore rather complicated. |
I understood that I had to provide a |
Not currently, but I'm going to be adding this as part of my work to support CSS Grid (it should already have it for Flexbox, but it doesn't). The way it will work (at least the way I'm currently planning for it to work) is instead of Hopefully we can make the docs clearer once we've made this change!
I think in most cases if you're given I'm not sure what would happen if you returned a larger width in this case. I don't think it'd break, but I also think it's probably not what you'd actually want in most cases. |
(trying very hard not to sound like a dick) Do you have an idea when your work to support CSS Grid will be "ready" ? |
@bestouff Feel free to split out this change and I can merge it more quickly! |
@bestouff Probably at least 2 months before it's in a releasable state. Good progress has been made, but the trickiest part (track sizing) hasn't yet been implemented. And beyond that there will need to be a good bit of testing (and no-doubt fixing) before it's ready. Having said that, it would be possible to pull this change out separately. I guess that would involve:
@alice-i-cecile My current design for this is: enum AvailableSpace {
Definite(f32),
MinContent,
MaxContent,
} (There is no with the signature of the Fn(Size<AvailableSpace>) -> Size<f32> A couple of things I'm not 100% on yet:
|
I like your I'll wait for your Grid implementation anyway - my use-case is a terminal markdown viewer in which I want to get rid of my custom layout code, so I'll need tables-like features. Also I need integer-only operations but I digress. |
This aligns with my thinking too. I'm in favor of splitting this work out; it seems useful otherwise and makes the PR review more feasible. |
I has a stab at implementing this, but I've realised that I had missed something in my initial analysis. The existing
I had originally assumed that the only purpose was (1). Now I can see it's definitely used for (2). I think it is likely used for (1) as well, although I'm not 100% sure about this: I suppose that it would be possible to just not pass max-size constraints to the I believe will necessitate either adding an additional variant to the enum, or otherwise communicating the known sizes separately from the available space constraint. Something like: enum Constraint {
KnownSize(f32),
MaxSize(f32),
MinContent,
MaxContent,
} If the |
Yes please; that seems dramatically clearer. |
Maybe I'm missing something here but if you had a measure function for a particular axis, perhaps determined by the flex direction, could you not replace the For example, let's say I have a node with a flexible width and an undetermined height. The width is not determined by the height so can be computed independently, including any min/max constraints. This width can then be passed to a measure function which delegates the height calculation to the user so the height can be determined by the wrapped text. One thing that confuses me is the idea of have a min/max content size for the input to the measure function. Surely it's impossible to determine this if the content is text, which then depends on the measure function. Can nodes contain both text and other nodes and have layout depend on them both? |
I believe that is roughly what happens when there is a defined width, but in general it is possible for the width to depend on content size too (I believe the flex-basis of a flex item frequently does). If you play around with min-content and max-content widths for nodes containing text in browsers, you'll see that max-content will render everything on a single line and take up the full width of all the text rendered on that single line while min-content will line-break at every space and other "line break opportunity" and only take up the width of the widest unbroken word/character sequence. The measure function will get called twice in these cases. The second time with a known size in (at least) one dimension.
Currently if a layout contains other nodes then it is treated a flexbox node and the measure function is ignored. Hopefully we can make that behaviour a bit more explicit in future (perhaps with a "Leaf" or "Custom" layout mode, but I think this basic principle that a Taffy node is EITHER a taffy layout type XOR has a custom measure function (which would include text nodes) will remain. However, it's worth noting the measure function doesn't always directly determine the final size of the node. It's more like a desired size and may still for example be clamped by the min-size and max-size or at behest of the parent node's layout.
Are you talking about the |
Ah okay, I was not aware of this behaviour. I suppose that makes sense. Does this really need two passes of the measure function though? Since the min/max content size in that case is independent on the height could the user not just set the min/max size constraint of the node itself based on the text? In my mind the measure function only needs to come in to play when one dimension depends on the other, like for wrapping of text. But maybe I'm thinking about it wrong?
Yep I think that makes a lot of sense and simplifies things, particularly for understanding.
Yes I thought that would be the case, though this does not sound like an easy constraints problem to solve. Particularly if you have flexible nodes along the same axis as the undetermined size of a node with a measure function.
I was talking about |
If only min/max constraints are set, then the measure function will still be called to determine where in that range the the size should be set (it's probably possible to optimise the min=max case in some places, but I don't think that's been implemented). If the actual size property is set, then that will indeed prevent the measure function from being called in many cases. However, I think the model is that the min/max/exact size properties are the end-user to set, and that the measure function would be how a text-layouting library would communicate it's text layout based dimensions to taffy. Taffy does do some caching if the measure function is called with the exact same inputs (and mark_dirty clears those caches), but if that text-layouting module wants to do further caching to avoid relayouting then it should do that internally within the measure function.
It's not, but luckily the flexbox spec authors have done all the hard work for us here. The general principle is that the measure function is used to independently determine the flex-basis for all children (if a node has an explicit flex-basis or width/height then the measure func won't be called), and growing and shrinking is then done in proportion to those flex-bases and in accordance with the flex-grow and flex-shrink properties. |
So if I read this correctly some of this enum variants are input/output, some are input-only ? That better be well described in the documentation. |
One would return the f32 contained in the |
Ah yes. Looks good. |
We can probably use the new https://github.com/pop-os/cosmic-text for a text layouting example. |
Yes please, I'm happy to demonstrate cosmic-text integration. |
I'm hoping together a more end-to-end example of this soon, but for anyone wanting to take the DIY approach:
Creating a node with a measure_func that captures some text: use taffy::prelude::*;
// The text in the node
let TEXT = String::new("Paragraph of text node goes here");
// MeasureFunc to pass to node. Moves text into the closure.
let measure_func = MeasureFunc::Boxed(Box::new(move |known_dimensions, available_space| {
measure_standard_text(known_dimensions, available_space, TEXT)
}))
// Create node with created measure_func
let text_node = taffy.new_leaf_with_measure(Style::DEFAULT, measure_func); A rudimentary text layouting function that is called by the measure_func: use taffy::prelude::*;
// Naive function to layout text based based on known dimensions and available space.
// We assume:
// - Text contains only H and zero-width space characters
// - An H measures 10 units wide by 10 units high
// - A zero-width space measure 0 units wide by 0 zero units high and provides a line-breaking opportunity
// - Text is laid out in a horizontal left-to-right direction and height depends on width
fn measure_standard_text(
known_dimensions: taffy::geometry::Size<Option<f32>>,
available_space: taffy::geometry::Size<taffy::layout::AvailableSpace>,
text_content: &str,
) -> taffy::geometry::Size<f32> {
const ZERO_WITH_SPACE: char = '\u{200B}';
const H_WIDTH: f32 = 10.0;
const H_HEIGHT: f32 = 10.0;
// If both dimensions are known then simply return them (this shouldn't ever really happen,
// but best to cover this case).
if let Size { width: Some(width), height: Some(height) } = known_dimensions {
return Size { width, height };
}
// Split text into lines at all possible line break opportunities. Here this only zero-width spaces,
// but in a more realistic scenario this would likely include all whitespace as well as things like
// hyphens. Your text layouting library will probably handle this for you.
let lines: Vec<&str> = text_content.split(ZERO_WITH_SPACE).collect();
// Return a zero size in both dimensions if there is no non-whitespace text. This makes sense in our case because our
// whitespace is zero-sized. In general you would want to account for space taken up by whitespace.
if lines.len() == 0 {
return Size::ZERO;
}
// Find the number of characters in:
// - The longest single line
// - All lines added toghether
let min_line_length: usize = lines.iter().map(|line| line.len()).max().unwrap_or(0);
let max_line_length: usize = lines.iter().map(|line| line.len()).sum();
// Calculate the width:
// - If the width is passed as a "known_dimension", then we simply return that.
// - Otherwise our width depends on the available_space in that dimension:
// - MinContent: width is the width of the longest single line
// - MaxContent: width is the width of all lines added together
// - Definite (a specific width in points): width is that width, clamped by the max-content and min-content width
let width = known_dimensions.width.unwrap_or_else(|| match available_space.width {
AvailableSpace::MinContent => min_line_length as f32 * H_WIDTH,
AvailableSpace::MaxContent => max_line_length as f32 * H_WIDTH,
AvailableSpace::Definite(width) => {
width.min(max_line_length as f32 * H_WIDTH).max(min_line_length as f32 * H_WIDTH)
}
});
// Calculate the height (based on the width):
// - If the height is passed as a "known_dimension", then we simply return that.
// - Otherwise:
// - We pack lines of text into the width starting a new line when a line doesn't
// fit into the current line.
// - This allow us to determine how many lines are required to fit our text
// - We multiply the number of lines by the line height to get the height
let height = known_dimensions.height.unwrap_or_else(|| {
let width_line_length = (width / H_WIDTH).floor() as usize;
let mut line_count = 1;
let mut current_line_length = 0;
for line in &lines {
if current_line_length + line.len() > width_line_length {
line_count += 1;
current_line_length = line.len();
} else {
current_line_length += line.len();
};
}
(line_count as f32) * H_HEIGHT
});
// Return computed width and height
Size { width, height }
} |
What problem does this solve or what need does it fill?
Newcomers to taffy may have a hard time understanding how to do text layouting:
What solution would you like?
I'd like a few examples for text layout (basic, wrapping, truncation)
What alternative(s) have you considered?
Maybe try to understand taffy innards from the source code in my copious spare time ?
The text was updated successfully, but these errors were encountered: