Skip to content
Merged
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: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Changed

### Fixed
- Fixed wrapped UI text measurement so automatic heights include every rendered line.

### Removed

Expand Down
54 changes: 42 additions & 12 deletions selene-core/src/ui/layout.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -281,25 +281,55 @@ fn implicit_root_style(size : @math.Vec2) -> @style.Style {
///|
fn measure_node(
known_dimensions : @geometry.Size[Double?],
_available : @geometry.Size[@geometry.AvailableSpace],
available : @geometry.Size[@geometry.AvailableSpace],
_node_id : Int,
entity : @entity.Entity?,
_style : @style.Style,
style : @style.Style,
) -> @geometry.Size[Double] {
guard entity is Some(entity) else { return @geometry.Size::zero() }
let (intrinsic_width, intrinsic_height) = if texts().get(entity) is Some(text) {
let measured = @platform_render.measure_text(
text_style_for(entity).unwrap_or({
font: None,
family: "Arial",
size: 16.0,
color: rgba(255, 255, 255, 1.0),
align: Left,
baseline: Top,
}),
let text_style = text_style_for(entity).unwrap_or({
font: None,
family: "Arial",
size: 16.0,
color: rgba(255, 255, 255, 1.0),
align: Left,
baseline: Top,
})
let width_basis = known_dimensions.width.unwrap_or(
match available.width {
AvailDefinite(width) => width
_ => 0.0
},
)
let horizontal_inset = @util.resolve_dimension_width_basis(
style.padding.left,
width_basis,
) +
@util.resolve_dimension_width_basis(style.padding.right, width_basis) +
@util.resolve_dimension_width_basis(style.border.left, width_basis) +
@util.resolve_dimension_width_basis(style.border.right, width_basis)
let available_content_width = match known_dimensions.width {
Some(width) =>
@style_helpers.available_space_definite(
@cmp.maximum(0.0, width - horizontal_inset),
)
None =>
match available.width {
AvailDefinite(width) =>
@style_helpers.available_space_definite(
@cmp.maximum(0.0, width - horizontal_inset),
)
constraint => constraint
}
}
let prepared = prepare_text_layout(
text.content,
text_style,
text_layout(entity),
available_content_width,
)
(measured[X], measured[Y])
(prepared.width, prepared.height)
} else if ui_images().get(entity) is Some(image) {
(image.size[X], image.size[Y])
} else {
Expand Down
42 changes: 42 additions & 0 deletions selene-core/src/ui/layout_wbtest.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,48 @@ test "ui nodes map block display and aspect ratio into block layout" {
inspect(layout.location.y == 15.0, content="true")
}

///|
test "ui wrapped text contributes lines to auto height" {
let entity = @entity.Entity()
texts().set(entity, Text("hello world"))
text_fonts().set(entity, TextFont(size=16.0))
text_layouts().set(entity, TextLayout(wrap=WordWrap))

let measured = measure_node(
Size(width=Some(60.0), height=None),
Size(
width=@style_helpers.available_space_definite(60.0),
height=@style_helpers.max_content(),
),
0,
Some(entity),
node_to_taffy_style(Node(width=Val::px(60.0))),
)

inspect(measured.width, content="60")
inspect(measured.height, content="32")

let measured_with_padding = measure_node(
Size(width=Some(100.0), height=None),
Size(
width=@style_helpers.available_space_definite(100.0),
height=@style_helpers.max_content(),
),
0,
Some(entity),
node_to_taffy_style(
Node(
width=Val::px(100.0),
padding=UiRectValues::px(left=20.0, right=20.0),
),
),
)

inspect(measured_with_padding.width, content="100")
inspect(measured_with_padding.height, content="32")
cleanup_layout_test_entity(entity)
}

///|
test "ui nodes map grid placement and self alignment into grid layout" {
let taffy : @tree.TaffyTree[Unit] = TaffyTree()
Expand Down
1 change: 1 addition & 0 deletions selene-core/src/ui/moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
"Milky2018/moon_taffy/style",
"Milky2018/moon_taffy/style_helpers",
"Milky2018/moon_taffy/tree",
"Milky2018/moon_taffy/util",
"moonbitlang/core/cmp",
"moonbitlang/core/string",
}
71 changes: 20 additions & 51 deletions selene-core/src/ui/render.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -715,46 +715,37 @@ fn push_image(
}

///|
fn measure_text_fragment(entity : @entity.Entity, text : String) -> @math.Vec2 {
@platform_render.measure_text(
render_text_style(entity).unwrap_or(
scaled_text_style({
font: None,
family: "Arial",
size: 16.0,
color: rgba(255, 255, 255, 1.0),
align: Left,
baseline: Top,
}),
),
text,
)
fn measure_text_fragment(
style : @render2d_types.TextStyle2D,
text : String,
) -> @math.Vec2 {
@platform_render.measure_text(style, text)
}

///|
fn truncate_line(
entity : @entity.Entity,
style : @render2d_types.TextStyle2D,
text : String,
max_width : Double,
) -> String {
if max_width <= 0.0 {
return ""
}
if measure_text_fragment(entity, text)[X] <= max_width {
if measure_text_fragment(style, text)[X] <= max_width {
return text
}
let ellipsis = "..."
let mut current = text
while current.length() > 0 &&
measure_text_fragment(entity, current + ellipsis)[X] > max_width {
measure_text_fragment(style, current + ellipsis)[X] > max_width {
current = current.unsafe_substring(start=0, end=current.length() - 1)
}
current + ellipsis
}

///|
fn wrap_line(
entity : @entity.Entity,
style : @render2d_types.TextStyle2D,
text : String,
max_width : Double,
) -> Array[String] {
Expand All @@ -766,16 +757,16 @@ fn wrap_line(
words.push(word.to_owned())
}
if words.length() <= 1 {
return [truncate_line(entity, text, max_width)]
return [truncate_line(style, text, max_width)]
}
let lines : Array[String] = []
let mut current = ""
for word in words {
let candidate = if current == "" { word } else { current + " " + word }
if measure_text_fragment(entity, candidate)[X] <= max_width {
if measure_text_fragment(style, candidate)[X] <= max_width {
current = candidate
} else if current == "" {
lines.push(truncate_line(entity, word, max_width))
lines.push(truncate_line(style, word, max_width))
} else {
lines.push(current)
current = word
Expand All @@ -790,36 +781,11 @@ fn wrap_line(
///|
fn prepared_text_lines(
entity : @entity.Entity,
content_rect : @math.Rect,
available_width : @geometry.AvailableSpace,
) -> Array[String] {
guard texts().get(entity) is Some(text) else { return [] }
let layout = text_layout(entity)
let raw_lines : Array[String] = []
for raw_line in text.content.split("\n") {
raw_lines.push(raw_line.to_owned())
}
let lines : Array[String] = []
for raw_line in raw_lines {
match layout.wrap {
NoWrap =>
lines.push(
match layout.overflow {
Clip => raw_line
Ellipsis => truncate_line(entity, raw_line, content_rect.size[X])
},
)
WordWrap =>
for line in wrap_line(entity, raw_line, content_rect.size[X]) {
lines.push(
match layout.overflow {
Clip => line
Ellipsis => truncate_line(entity, line, content_rect.size[X])
},
)
}
}
}
lines
guard text_style_for(entity) is Some(style) else { return [] }
prepare_text_layout(text.content, style, text_layout(entity), available_width).lines
}

///|
Expand All @@ -828,7 +794,10 @@ fn push_text(entity : @entity.Entity, camera_entity_id? : UInt? = None) -> Unit
guard global_ui_nodes().get(entity) is Some(transform) else { return }
guard render_text_style(entity) is Some(style) else { return }
let layout = text_layout(entity)
let lines = prepared_text_lines(entity, transform.screen_content_rect)
let lines = prepared_text_lines(
entity,
AvailDefinite(transform.logical_content_rect.size[X]),
)
if lines.is_empty() {
return
}
Expand All @@ -845,7 +814,7 @@ fn push_text(entity : @entity.Entity, camera_entity_id? : UInt? = None) -> Unit
block_height
}
for index, line in lines {
let measured = measure_text_fragment(entity, line)
let measured = measure_text_fragment(style, line)
let x = match layout.align {
Left => transform.screen_content_rect.position[X]
Center =>
Expand Down
61 changes: 61 additions & 0 deletions selene-core/src/ui/text_layout.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
///|
priv struct PreparedTextLayout {
lines : Array[String]
width : Double
height : Double
}

///|
fn prepare_text_layout(
content : String,
style : @render2d_types.TextStyle2D,
layout : TextLayout,
available_width : @geometry.AvailableSpace,
) -> PreparedTextLayout {
let wrap_width : Double? = match available_width {
AvailDefinite(width) => Some(@cmp.maximum(0.0, width))
AvailMaxContent => None
AvailMinContent => {
let mut longest = 0.0
for raw_line in content.split("\n") {
for word in raw_line.split(" ") {
longest = @cmp.maximum(
longest,
measure_text_fragment(style, word.to_owned())[X],
)
}
}
Some(longest)
}
}
let lines : Array[String] = []
for raw_line_view in content.split("\n") {
let raw_line = raw_line_view.to_owned()
match (layout.wrap, wrap_width) {
(WordWrap, Some(max_width)) =>
for line in wrap_line(style, raw_line, max_width) {
lines.push(
match (layout.overflow, available_width) {
(Ellipsis, AvailDefinite(_)) =>
truncate_line(style, line, max_width)
_ => line
},
)
}
_ =>
lines.push(
match (layout.overflow, available_width) {
(Ellipsis, AvailDefinite(max_width)) =>
truncate_line(style, raw_line, max_width)
_ => raw_line
},
)
}
}
let mut width = 0.0
for line in lines {
width = @cmp.maximum(width, measure_text_fragment(style, line)[X])
}
let line_height = style.size * @cmp.maximum(1.0, layout.line_height)
{ lines, width, height: line_height * lines.length().to_double() }
}
Loading