mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-09 04:42:30 -04:00
Merge pull request #3613 from lightpanda-io/scroll-text-extent
Measure wrapped text in an element's content height
This commit is contained in:
3 files changed
+113
-6
No files matched your search
@@ -664,3 +664,33 @@
|
||||
testing.expectEqual(window.innerHeight / 10, $('#sheetVh').clientHeight);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="textBox" style="height: 100px; width: 200px; overflow: auto;">
|
||||
one two three four five six seven eight nine ten eleven twelve thirteen
|
||||
fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one
|
||||
twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven
|
||||
</div>
|
||||
<div id="textFree">one two three four five six seven eight nine ten eleven twelve</div>
|
||||
<div id="textSmall" style="width: 200px; font-size: 8px">one two three four five six seven eight nine ten eleven twelve</div>
|
||||
|
||||
<script id="textWrapsAtExplicitWidth">
|
||||
{
|
||||
// Text wraps at an explicit width
|
||||
const box = $('#textBox');
|
||||
testing.expectEqual(100, box.clientHeight);
|
||||
testing.expectEqual(200, box.scrollWidth);
|
||||
testing.expectTrue(box.scrollHeight > box.clientHeight);
|
||||
|
||||
box.scrollTop = 9999;
|
||||
testing.expectTrue(box.scrollTop > 0);
|
||||
testing.expectEqual(box.scrollHeight - box.clientHeight, box.scrollTop);
|
||||
|
||||
// Without one, it isn't measured
|
||||
testing.expectEqual(5, $('#textFree').scrollHeight);
|
||||
|
||||
const small = $('#textSmall');
|
||||
const before = small.clientHeight;
|
||||
small.style.fontSize = '4px';
|
||||
testing.expectTrue(small.clientHeight < before);
|
||||
}
|
||||
</script>
|
||||
@@ -63,6 +63,50 @@ pub fn substringWidth(text: []const u8, charnum: u32, nchars: u32, font_size: f6
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Line height for `line-height: normal`, in em.
|
||||
pub const LINE_HEIGHT = 1.2;
|
||||
|
||||
/// Greedy line breaking under `white-space: normal`. Text can be added in
|
||||
/// pieces (one per text node) that flow on from each other.
|
||||
pub const LineWrap = struct {
|
||||
line_width: f64,
|
||||
font_size: f64,
|
||||
lines: u32 = 0,
|
||||
// Width used on the current line.
|
||||
x: f64 = 0,
|
||||
// A space is due before the next word.
|
||||
space: bool = false,
|
||||
|
||||
pub fn add(self: *LineWrap, text: []const u8) void {
|
||||
if (text.len == 0) {
|
||||
return;
|
||||
}
|
||||
if (std.ascii.isWhitespace(text[0])) {
|
||||
self.space = true;
|
||||
}
|
||||
var words = std.mem.tokenizeAny(u8, text, &std.ascii.whitespace);
|
||||
while (words.next()) |word| {
|
||||
const w = width(word, self.font_size);
|
||||
const gap = if (self.space) advance(' ', self.font_size) else 0;
|
||||
if (self.lines == 0) {
|
||||
self.lines = 1;
|
||||
self.x = w;
|
||||
} else if (self.x + gap + w > self.line_width) {
|
||||
self.lines += 1;
|
||||
self.x = w;
|
||||
} else {
|
||||
self.x += gap + w;
|
||||
}
|
||||
self.space = true;
|
||||
}
|
||||
self.space = std.ascii.isWhitespace(text[text.len - 1]);
|
||||
}
|
||||
|
||||
pub fn height(self: LineWrap) f64 {
|
||||
return @as(f64, @floatFromInt(self.lines)) * self.font_size * LINE_HEIGHT;
|
||||
}
|
||||
};
|
||||
|
||||
fn advance(codepoint: u21, font_size: f64) f64 {
|
||||
if (isZeroWidth(codepoint)) return 0;
|
||||
if (codepoint == '\n' or codepoint == '\r') return 0;
|
||||
@@ -109,3 +153,23 @@ test "fallback metrics count utf-16 units and ignore combining marks" {
|
||||
try std.testing.expectError(error.IndexSizeError, substringWidth("A", 1, 0, 10));
|
||||
try std.testing.expectError(error.IndexSizeError, substringWidth("", 0, 0, 10));
|
||||
}
|
||||
|
||||
test "LineWrap: collapses whitespace and breaks between words" {
|
||||
// At 10px: a letter is 6px, a space 3.3px
|
||||
var wrap: LineWrap = .{ .line_width = 60, .font_size = 10 };
|
||||
try std.testing.expectEqual(0, wrap.height());
|
||||
|
||||
wrap.add(" aaaa \n\t aaaa aaaa ");
|
||||
try std.testing.expectEqual(2, wrap.lines);
|
||||
|
||||
wrap.add("aaaa");
|
||||
try std.testing.expectEqual(2, wrap.lines);
|
||||
wrap.add("aaaa");
|
||||
try std.testing.expectEqual(3, wrap.lines);
|
||||
try std.testing.expectApproxEqAbs(36, wrap.height(), 0.0001);
|
||||
|
||||
// Long words aren't split
|
||||
var narrow: LineWrap = .{ .line_width = 10, .font_size = 10 };
|
||||
narrow.add("aaaa aaaa");
|
||||
try std.testing.expectEqual(2, narrow.lines);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const js = @import("../js/js.zig");
|
||||
const dump = @import("../dump.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Factory = @import("../Factory.zig");
|
||||
const text_measure = @import("../text_measure.zig");
|
||||
|
||||
const CSS = @import("CSS.zig");
|
||||
const Node = @import("Node.zig");
|
||||
@@ -1756,26 +1757,38 @@ fn scrollExtent(self: *Element, frame: *Frame, comptime axis: Axis) ?f64 {
|
||||
// `scrollWidth` passes a threshold (the infinite-marquee idiom) never
|
||||
// terminates when the metric ignores what it just inserted.
|
||||
//
|
||||
// Text children are not measured. Estimating a text run from its length would
|
||||
// need a per-character advance, which in turn has to track font-size or
|
||||
// "shrink the font until it fits" loops stop converging — and it would report
|
||||
// overflow for practically every element containing text, since a few words
|
||||
// already exceed the default box. Element children are what content grown by
|
||||
// script actually consists of.
|
||||
// Text children add height only under an explicit width to wrap at.
|
||||
// Otherwise almost every element with text would report overflow.
|
||||
fn contentAxis(self: *Element, frame: *Frame, comptime axis: Axis) f64 {
|
||||
var total: f64 = 0;
|
||||
const owner = self.ownerFrame(frame) orelse return 0;
|
||||
const style_manager = &owner._style_manager;
|
||||
|
||||
var wrap: ?text_measure.LineWrap = null;
|
||||
if (axis == .height) {
|
||||
const width = self.getElementAxis(frame, .width);
|
||||
if (width.explicit) {
|
||||
wrap = .{ .line_width = width.value, .font_size = style_manager.computedFontSize(self) };
|
||||
}
|
||||
}
|
||||
|
||||
var child = self.asNode().firstChild();
|
||||
while (child) |node| : (child = node.nextSibling()) {
|
||||
if (node.is(Element)) |el| {
|
||||
if (!style_manager.hasDisplayNone(el)) {
|
||||
total += el.getElementAxis(frame, axis).value;
|
||||
}
|
||||
} else if (wrap) |*w| {
|
||||
if (node.is(Node.CData.Text)) |text| {
|
||||
w.add(text.ownData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wrap) |w| {
|
||||
// Whole pixels, like scroll offsets
|
||||
total += @ceil(w.height());
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user