use crate::{Color, Font, Rect, Texture, Vec2, vec2};
use taffy::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UiId(usize);
impl UiId {
pub const fn bits(self) -> u64 {
self.0 as u64
}
pub const fn from_bits(bits: u64) -> Self {
Self(if bits > usize::MAX as u64 {
usize::MAX
} else {
bits as usize
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UiWidget {
Panel,
Label,
Button,
Image,
Scroll,
TextInput,
Toggle,
Slider,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum UiTextAlign {
#[default]
Start,
Center,
End,
}
#[derive(Debug, Clone, Copy)]
pub struct UiPaint {
pub background: Color,
pub hover: Color,
pub pressed: Color,
pub disabled: Color,
pub border: Color,
pub border_width: f32,
pub color: Color,
pub font: Font,
pub font_size: f32,
pub wrap: bool,
pub align: UiTextAlign,
pub texture: Option<Texture>,
pub slice: f32,
}
impl Default for UiPaint {
fn default() -> Self {
Self {
background: Color::rgba(0.12, 0.14, 0.18, 1.0),
hover: Color::rgba(0.25, 0.3, 0.4, 1.0),
pressed: Color::rgba(0.08, 0.1, 0.15, 1.0),
disabled: Color::rgba(0.15, 0.15, 0.15, 0.5),
border: Color::hex(0xf28c28),
border_width: 0.0,
color: Color::WHITE,
font: Font::DEFAULT,
font_size: 18.0,
wrap: false,
align: UiTextAlign::Start,
texture: None,
slice: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiEvent {
Clicked(UiId),
Changed(UiId),
Submitted(UiId),
ScreenPopped(UiId),
}
#[derive(Debug, Default, Clone)]
pub struct UiInput {
pub pointer: Vec2,
pub pressed: bool,
pub held: bool,
pub released: bool,
pub wheel: Vec2,
pub next: bool,
pub previous: bool,
pub direction: Vec2,
pub confirm: bool,
pub cancel: bool,
pub shift: bool,
pub select_all: bool,
pub backspace: bool,
pub delete: bool,
pub home: bool,
pub end: bool,
pub text: String,
}
pub(super) struct Node {
pub widget: UiWidget,
pub layout: NodeId,
pub parent: Option<UiId>,
pub children: Vec<UiId>,
pub text: String,
pub paint: UiPaint,
pub enabled: bool,
pub rect: Rect,
pub clip: Rect,
pub scroll: Vec2,
pub max_scroll: Vec2,
pub value: f32,
pub caret: usize,
pub anchor: usize,
}
struct Screen {
root: UiId,
modal: bool,
saved_focus: Option<UiId>,
}
#[derive(Clone, Copy)]
enum Drag {
Scroll(UiId, Vec2, Vec2),
Bar(UiId, f32, f32),
Text(UiId),
}
pub struct Ui {
pub(super) tree: TaffyTree<usize>,
pub(super) nodes: Vec<Option<Node>>,
screens: Vec<Screen>,
pub(super) visible: Vec<UiId>,
events: Vec<UiEvent>,
pub(super) focus: Option<UiId>,
pub(super) hovered: Option<UiId>,
pub(super) pressed: Option<UiId>,
drag: Option<Drag>,
scale: f32,
viewport: Vec2,
pointer: Vec2,
measure: crate::text::Text,
external_fonts: bool,
consumed: bool,
}
unsafe impl Send for Ui {}
unsafe impl Sync for Ui {}
impl Default for Ui {
fn default() -> Self {
Self {
tree: TaffyTree::new(),
nodes: Vec::new(),
screens: Vec::new(),
visible: Vec::new(),
events: Vec::new(),
focus: None,
hovered: None,
pressed: None,
drag: None,
scale: 1.0,
viewport: Vec2::ZERO,
pointer: Vec2::ZERO,
measure: crate::text::Text::new(),
external_fonts: false,
consumed: false,
}
}
}
impl Ui {
pub(super) fn node(&self, id: UiId) -> Option<&Node> {
self.nodes.get(id.0)?.as_ref()
}
fn node_mut(&mut self, id: UiId) -> Option<&mut Node> {
self.nodes.get_mut(id.0)?.as_mut()
}
pub fn add(&mut self, widget: UiWidget, style: Style) -> UiId {
let id = UiId(self.nodes.len());
let layout = self.tree.new_leaf_with_context(style, id.0).expect("valid UI layout");
let mut paint = UiPaint::default();
if matches!(widget, UiWidget::Label | UiWidget::Image) {
paint.background = Color::rgba(0.0, 0.0, 0.0, 0.0);
}
self.nodes.push(Some(Node {
widget,
layout,
parent: None,
children: Vec::new(),
text: String::new(),
paint,
enabled: true,
rect: Rect::default(),
clip: Rect::default(),
scroll: Vec2::ZERO,
max_scroll: Vec2::ZERO,
value: 0.0,
caret: 0,
anchor: 0,
}));
id
}
pub fn add_child(&mut self, parent: UiId, widget: UiWidget, style: Style) -> UiId {
let p = self.node(parent).expect("UI parent must exist").layout;
let id = self.add(widget, style);
let node = self.node_mut(id).unwrap();
node.parent = Some(parent);
let layout = node.layout;
self.tree.add_child(p, layout).expect("valid UI child");
self.node_mut(parent).unwrap().children.push(id);
id
}
pub fn remove(&mut self, id: UiId) {
while let Some(child) = self.node(id).and_then(|n| n.children.last().copied()) {
self.remove(child);
}
if let Some(n) = self.nodes.get_mut(id.0).and_then(Option::take) {
if let Some(parent) = n.parent.and_then(|p| self.node_mut(p)) {
parent.children.retain(|&c| c != id);
}
let _ = self.tree.remove(n.layout);
}
self.screens.retain(|s| s.root != id);
self.visible.retain(|&n| n != id);
if self.drag.is_some_and(|d| match d {
Drag::Scroll(n, _, _) | Drag::Bar(n, _, _) | Drag::Text(n) => n == id,
}) {
self.drag = None;
}
if self.focus == Some(id) {
self.focus = None;
}
if self.pressed == Some(id) {
self.pressed = None;
self.drag = None;
}
self.events.retain(|e| match e {
UiEvent::Clicked(n) | UiEvent::Changed(n) | UiEvent::Submitted(n) | UiEvent::ScreenPopped(n) => *n != id,
});
}
pub fn set_layout(&mut self, id: UiId, style: Style) {
if let Some(n) = self.node(id) {
let _ = self.tree.set_style(n.layout, style);
}
}
pub fn layout_style(&self, id: UiId) -> Option<&Style> {
self.tree.style(self.node(id)?.layout).ok()
}
pub fn set_paint(&mut self, id: UiId, paint: UiPaint) {
if let Some(n) = self.node_mut(id) {
n.paint = paint;
}
}
pub fn set_text(&mut self, id: UiId, text: impl AsRef<str>) {
if let Some(n) = self.node_mut(id) {
n.text.clear();
if n.widget == UiWidget::TextInput {
n.text.extend(text.as_ref().chars().filter(|c| !c.is_control()));
} else {
n.text.push_str(text.as_ref());
}
n.caret = n.text.len();
n.anchor = n.caret;
}
}
pub fn text(&self, id: UiId) -> Option<&str> {
Some(&self.node(id)?.text)
}
pub fn set_enabled(&mut self, id: UiId, enabled: bool) {
if let Some(n) = self.node_mut(id) {
n.enabled = enabled;
}
}
pub fn set_value(&mut self, id: UiId, value: f32) {
if let Some(n) = self.node_mut(id) {
n.value = if value.is_finite() { value.clamp(0.0, 1.0) } else { 0.0 };
}
}
pub fn value(&self, id: UiId) -> Option<f32> {
Some(self.node(id)?.value)
}
pub fn set_scroll(&mut self, id: UiId, offset: Vec2) {
if let Some(n) = self.node_mut(id) {
n.scroll = offset.max(Vec2::ZERO);
}
}
pub fn scroll(&self, id: UiId) -> Option<Vec2> {
Some(self.node(id)?.scroll)
}
pub fn push_screen(&mut self, root: UiId, modal: bool) {
assert!(
self.node(root).is_some_and(|n| n.parent.is_none()),
"UI screen must be a detached root"
);
assert!(
!self.screens.iter().any(|s| s.root == root),
"UI screen already on stack"
);
self.screens.push(Screen {
root,
modal,
saved_focus: self.focus.take(),
});
self.pressed = None;
self.drag = None;
self.hovered = None;
}
pub fn pop_screen(&mut self) -> Option<UiId> {
let screen = self.screens.pop()?;
self.consumed = true;
self.focus = screen.saved_focus.filter(|&id| self.interactive(id));
self.pressed = None;
self.drag = None;
self.hovered = None;
Some(screen.root)
}
pub fn parent(&self, id: UiId) -> Option<UiId> {
self.node(id)?.parent
}
pub fn is_screen(&self, id: UiId) -> bool {
self.screens.iter().any(|s| s.root == id)
}
pub fn top_screen(&self) -> Option<UiId> {
self.screens.last().map(|s| s.root)
}
pub fn set_scale(&mut self, scale: f32) {
self.scale = if scale.is_finite() { scale.clamp(0.25, 8.0) } else { 1.0 };
}
pub fn rect(&self, id: UiId) -> Option<Rect> {
Some(self.node(id)?.rect)
}
pub fn focus(&self) -> Option<UiId> {
self.focus
}
pub fn events(&mut self) -> impl Iterator<Item = UiEvent> + '_ {
self.events.drain(..)
}
pub fn next_event(&mut self) -> Option<UiEvent> {
(!self.events.is_empty()).then(|| self.events.remove(0))
}
pub fn wants_pointer(&self) -> bool {
self.consumed
|| self.pressed.is_some()
|| self.drag.is_some()
|| self.hit_test(self.pointer).is_some()
|| self.screens.last().is_some_and(|s| s.modal)
}
pub fn wants_keyboard(&self) -> bool {
self.consumed || !self.screens.is_empty()
}
fn interactive(&self, id: UiId) -> bool {
let mut at = Some(id);
while let Some(id) = at {
let Some(n) = self.node(id) else { return false };
if !n.enabled {
return false;
}
if Some(id) == self.top_screen() {
return true;
}
at = n.parent;
}
false
}
fn focusable(&self, id: UiId) -> bool {
self.interactive(id)
&& self.node(id).is_some_and(|n| {
matches!(
n.widget,
UiWidget::Button | UiWidget::TextInput | UiWidget::Toggle | UiWidget::Slider
) && n.rect.size().cmpgt(Vec2::ZERO).all()
})
}
pub fn hit_test(&self, point: Vec2) -> Option<UiId> {
self.visible.iter().rev().copied().find(|&id| {
self.interactive(id)
&& self.node(id).is_some_and(|n| {
n.rect.size().cmpgt(Vec2::ZERO).all()
&& n.rect.contains(point)
&& n.clip.contains(point)
&& n.clip.size().cmpgt(Vec2::ZERO).all()
})
})
}
pub fn layout(&mut self, viewport: Vec2) {
self.viewport = viewport.max(Vec2::ZERO);
self.visible.clear();
for n in self.nodes.iter().flatten() {
let _ = self.tree.mark_dirty(n.layout);
}
let nodes = &self.nodes;
let text = &mut self.measure;
let external_fonts = self.external_fonts;
for screen in &self.screens {
let layout = nodes[screen.root.0].as_ref().unwrap().layout;
let _ = self.tree.compute_layout_with_measure(
layout,
Size {
width: AvailableSpace::Definite(viewport.x / self.scale),
height: AvailableSpace::Definite(viewport.y / self.scale),
},
|inputs, _, context, style| {
let Some(n) = context.and_then(|index| nodes[*index].as_ref()) else {
return taffy::tree::LayoutOutput::HIDDEN;
};
taffy::compute_leaf_layout(
inputs,
style,
|_, _| 0.0,
|known, available| {
let size = text.layout(
&n.text,
if external_fonts { n.paint.font } else { Font::DEFAULT },
n.paint.font_size,
false,
);
let font = if external_fonts { n.paint.font } else { Font::DEFAULT };
let width = known.width.unwrap_or_else(|| match available.width {
AvailableSpace::Definite(w) if n.paint.wrap => size.x.min(w.max(0.0)),
AvailableSpace::MinContent if n.paint.wrap => n
.text
.split_whitespace()
.map(|word| text.layout(word, font, n.paint.font_size, false).x)
.fold(0.0, f32::max),
_ => size.x,
});
let mut height = 0.0;
for paragraph in n.text.split('\n') {
let mut rest = paragraph;
loop {
let end = if n.paint.wrap {
wrap_end(text, rest, font, n.paint.font_size, width)
} else {
rest.len()
};
height += text.layout(&rest[..end], font, n.paint.font_size, false).y;
if end == rest.len() {
break;
}
rest = rest[end..].trim_start();
}
}
if n.text.is_empty()
&& !matches!(n.widget, UiWidget::TextInput | UiWidget::Button | UiWidget::Toggle)
{
height = 0.0;
}
Size {
width,
height: known.height.unwrap_or(height),
}
},
)
},
);
}
for i in 0..self.screens.len() {
self.place(
self.screens[i].root,
Vec2::ZERO,
Rect::new(0.0, 0.0, viewport.x, viewport.y),
);
}
if self.focus.is_some_and(|id| !self.focusable(id)) {
self.focus = None;
}
}
fn place(&mut self, id: UiId, origin: Vec2, clip: Rect) {
let layout = *self.tree.layout(self.node(id).unwrap().layout).unwrap();
let scale = self.scale;
let pos = origin + vec2(layout.location.x, layout.location.y) * scale;
let size = vec2(layout.size.width, layout.size.height) * scale;
let rect = Rect {
min: pos,
max: pos + size,
};
let n = self.node_mut(id).unwrap();
n.rect = rect;
n.clip = clip;
let scrollable = n.widget == UiWidget::Scroll;
n.max_scroll = if scrollable {
vec2(layout.scroll_width(), layout.scroll_height())
} else {
Vec2::ZERO
};
n.scroll = n.scroll.clamp(Vec2::ZERO, n.max_scroll);
let scroll = n.scroll * scale;
self.visible.push(id);
let child_clip = if scrollable {
let mut inner = rect;
if self.node(id).unwrap().max_scroll.y > 0.0 {
inner.max.x = (inner.max.x - 12.0 * scale).max(inner.min.x);
}
intersect(clip, inner)
} else {
clip
};
let count = self.node(id).unwrap().children.len();
for i in 0..count {
let child = self.node(id).unwrap().children[i];
self.place(child, pos - scroll, child_clip);
}
}
pub fn frame(&mut self, viewport: Vec2, input: &UiInput) {
self.events.clear();
self.consumed = false;
self.pointer = input.pointer;
self.layout(viewport);
if input.cancel {
if let Some(root) = self.pop_screen() {
self.events.push(UiEvent::ScreenPopped(root));
}
self.layout(viewport);
return;
}
self.hovered = self.hit_test(input.pointer);
if input.next || input.previous {
self.tab(input.previous);
}
if self.focus.is_some_and(|id| !self.focusable(id)) {
self.focus = None;
}
if input.pressed {
self.pressed = self.hovered;
self.drag = None;
if let Some(id) = self.hovered {
self.focus = self.focusable(id).then_some(id);
let n = self.node(id).unwrap();
if n.widget == UiWidget::TextInput {
let caret = self.caret_at(id, input.pointer);
let n = self.node_mut(id).unwrap();
n.caret = caret;
if !input.shift {
n.anchor = caret;
}
self.drag = Some(Drag::Text(id));
} else if n.widget != UiWidget::Slider {
let mut at = Some(id);
while let Some(id) = at {
let n = self.node(id).unwrap();
if n.widget == UiWidget::Scroll {
self.drag = Some(if input.pointer.x >= n.rect.max.x - 12.0 * self.scale {
Drag::Bar(id, input.pointer.y, n.scroll.y)
} else {
Drag::Scroll(id, input.pointer, n.scroll)
});
break;
}
at = n.parent;
}
}
} else {
self.focus = None;
}
}
let mut at = self.hovered;
while let Some(id) = at {
let n = self.node(id).unwrap();
if n.widget == UiWidget::Scroll {
let next = n.scroll + input.wheel / self.scale;
self.node_mut(id).unwrap().scroll = next;
break;
}
at = n.parent;
}
if input.held {
if let Some(drag) = self.drag {
match drag {
Drag::Scroll(id, start, offset) => {
let scale = self.scale;
if let Some(n) = self.node_mut(id) {
n.scroll = offset - (input.pointer - start) / scale;
}
if (input.pointer - start).length() > 4.0 {
self.pressed = None;
}
}
Drag::Bar(id, start, offset) => {
let n = self.node(id).unwrap();
let h = n.rect.size().y;
let thumb = (h * h / (h + n.max_scroll.y * self.scale))
.max(20.0 * self.scale)
.min(h);
let track = (h - thumb).max(1.0);
let value = offset + (input.pointer.y - start) / track * n.max_scroll.y;
self.node_mut(id).unwrap().scroll.y = value;
self.pressed = None;
}
Drag::Text(id) => {
let caret = self.caret_at(id, input.pointer);
self.node_mut(id).unwrap().caret = caret;
}
}
}
if let Some(id) = self
.pressed
.filter(|&id| self.node(id).is_some_and(|n| n.widget == UiWidget::Slider))
{
let n = self.node(id).unwrap();
let value = ((input.pointer.x - n.rect.min.x) / n.rect.size().x.max(1.0)).clamp(0.0, 1.0);
self.change_value(id, value);
}
}
if input.released {
if let Some(id) = self.pressed.take().filter(|id| Some(*id) == self.hovered)
&& self
.node(id)
.is_some_and(|n| matches!(n.widget, UiWidget::Button | UiWidget::Toggle))
{
self.activate(id);
}
self.drag = None;
}
let editing = self
.focus
.is_some_and(|id| self.node(id).unwrap().widget == UiWidget::TextInput);
if editing && input.direction.y == 0.0 {
self.edit(input);
} else if let Some(id) = self
.focus
.filter(|&id| self.node(id).unwrap().widget == UiWidget::Slider)
&& input.direction.x != 0.0
{
let value = self.value(id).unwrap() + input.direction.x * 0.05;
self.change_value(id, value.clamp(0.0, 1.0));
} else if input.direction != Vec2::ZERO {
self.navigate(input.direction);
}
if input.confirm
&& let Some(id) = self.focus
{
self.activate(id);
}
self.layout(viewport);
if input.next || input.previous || input.direction != Vec2::ZERO {
self.reveal_focus();
self.layout(viewport);
}
}
fn reveal_focus(&mut self) {
let Some(id) = self.focus else { return };
let mut rect = self.node(id).unwrap().rect;
let mut at = self.node(id).unwrap().parent;
let scale = self.scale;
while let Some(id) = at {
let n = self.node_mut(id).unwrap();
if n.widget == UiWidget::Scroll {
let delta = (rect.max - n.rect.max).max(Vec2::ZERO) + (rect.min - n.rect.min).min(Vec2::ZERO);
let old = n.scroll;
n.scroll = (n.scroll + delta / scale).clamp(Vec2::ZERO, n.max_scroll);
let moved = (n.scroll - old) * scale;
rect.min -= moved;
rect.max -= moved;
}
at = n.parent;
}
}
fn tab(&mut self, back: bool) {
let current = self.focus.and_then(|id| self.visible.iter().position(|&n| n == id));
let len = self.visible.len();
for step in 1..=len {
let index = if back {
(current.unwrap_or(0) + len - step % len) % len
} else {
current.map_or(step - 1, |i| (i + step) % len)
};
let id = self.visible[index];
if self.focusable(id) {
self.focus = Some(id);
break;
}
}
}
fn navigate(&mut self, direction: Vec2) {
let Some(from) = self.focus.and_then(|id| self.rect(id)) else {
self.tab(false);
return;
};
let mut best = None;
let mut score = f32::INFINITY;
for &id in &self.visible {
if !self.focusable(id) || Some(id) == self.focus {
continue;
}
let delta = self.rect(id).unwrap().center() - from.center();
let along = delta.dot(direction);
if along <= 0.0 {
continue;
}
let cross = delta.perp_dot(direction).abs();
let cost = along + cross * 4.0;
if cost < score {
best = Some(id);
score = cost;
}
}
if best.is_some() {
self.focus = best;
}
}
fn activate(&mut self, id: UiId) {
if !self.interactive(id) {
return;
}
match self.node(id).unwrap().widget {
UiWidget::Button => self.events.push(UiEvent::Clicked(id)),
UiWidget::Toggle => {
let v = 1.0 - self.value(id).unwrap();
self.change_value(id, v);
}
UiWidget::TextInput => self.events.push(UiEvent::Submitted(id)),
_ => {}
}
}
fn change_value(&mut self, id: UiId, value: f32) {
if self.value(id) != Some(value) {
self.set_value(id, value);
self.events.push(UiEvent::Changed(id));
}
}
fn caret_at(&mut self, id: UiId, p: Vec2) -> usize {
let n = self.nodes[id.0].as_ref().unwrap();
let layout = self.tree.layout(n.layout).unwrap();
let font = if self.external_fonts {
n.paint.font
} else {
Font::DEFAULT
};
let x = (p.x - n.rect.min.x) / self.scale - layout.padding.left - layout.border.left;
let caret_x = self
.measure
.layout(&n.text[..n.caret], font, n.paint.font_size, false)
.x;
let available = n.rect.size().x / self.scale - layout.padding.left - layout.padding.right;
let target = x + (caret_x - available + 3.0).max(0.0);
let mut prev = 0.0;
for (i, ch) in n.text.char_indices() {
let next = i + ch.len_utf8();
let width = self.measure.layout(&n.text[..next], font, n.paint.font_size, false).x;
if target < (prev + width) * 0.5 {
return i;
}
prev = width;
}
n.text.len()
}
fn edit(&mut self, input: &UiInput) {
let id = self.focus.unwrap();
let n = self.node_mut(id).unwrap();
let mut changed = false;
if input.select_all {
n.anchor = 0;
n.caret = n.text.len();
}
if input.home {
n.caret = 0;
if !input.shift {
n.anchor = n.caret;
}
}
if input.end {
n.caret = n.text.len();
if !input.shift {
n.anchor = n.caret;
}
}
if input.direction.x != 0.0 {
n.caret = if input.direction.x < 0.0 {
n.text[..n.caret].char_indices().next_back().map_or(0, |(i, _)| i)
} else {
n.text[n.caret..]
.chars()
.next()
.map_or(n.caret, |c| n.caret + c.len_utf8())
};
if !input.shift {
n.anchor = n.caret;
}
}
if input.backspace || input.delete || input.text.chars().any(|c| !c.is_control()) {
let lo = n.caret.min(n.anchor);
let hi = n.caret.max(n.anchor);
if lo != hi {
n.text.replace_range(lo..hi, "");
n.caret = lo;
changed = true;
} else if input.backspace && n.caret > 0 {
let prev = n.text[..n.caret].char_indices().next_back().unwrap().0;
n.text.replace_range(prev..n.caret, "");
n.caret = prev;
changed = true;
} else if input.delete && n.caret < n.text.len() {
let end = n.caret + n.text[n.caret..].chars().next().unwrap().len_utf8();
n.text.replace_range(n.caret..end, "");
changed = true;
}
for ch in input.text.chars().filter(|c| !c.is_control()) {
n.text.insert(n.caret, ch);
n.caret += ch.len_utf8();
changed = true;
}
n.anchor = n.caret;
}
if changed {
self.events.push(UiEvent::Changed(id));
}
}
pub(super) fn layout_with_text(&mut self, viewport: Vec2, text: &mut crate::text::Text) {
std::mem::swap(&mut self.measure, text);
self.external_fonts = true;
self.layout(viewport);
self.external_fonts = false;
std::mem::swap(&mut self.measure, text);
}
pub(super) fn frame_with_text(&mut self, viewport: Vec2, input: &UiInput, text: &mut crate::text::Text) {
std::mem::swap(&mut self.measure, text);
self.external_fonts = true;
self.frame(viewport, input);
self.external_fonts = false;
std::mem::swap(&mut self.measure, text);
}
pub(super) fn scale(&self) -> f32 {
self.scale
}
pub(super) fn modal_root(&self, id: UiId) -> bool {
self.screens.iter().any(|s| s.root == id && s.modal)
}
}
pub(super) fn intersect(a: Rect, b: Rect) -> Rect {
let min = a.min.max(b.min);
Rect {
min,
max: a.max.min(b.max).max(min),
}
}
pub(super) fn wrap_end(text: &mut crate::text::Text, line: &str, font: Font, size: f32, width: f32) -> usize {
let mut space = None;
for (i, ch) in line.char_indices() {
if ch.is_whitespace() {
space = Some(i);
}
let next = i + ch.len_utf8();
if text.layout(&line[..next], font, size, false).x > width {
return space.filter(|&s| s > 0).unwrap_or(if i == 0 { next } else { i });
}
}
line.len()
}