#![forbid(unsafe_code)]
use crate::core::{Align, Anchor, Axis, BoxStyle, Cx, Element, KeyInput, OverlaySpec, ViewportId};
use crate::geometry::{Insets, Point};
use crate::render::Color;
use crate::style::Theme;
pub fn column(children: Vec<Element>) -> Element {
Element::stack(Axis::Vertical, children)
}
pub fn row(children: Vec<Element>) -> Element {
Element::stack(Axis::Horizontal, children)
}
pub fn spacer() -> Element {
Element::stack(Axis::Horizontal, Vec::new()).grow(1.0)
}
pub fn scroll<S>(cx: &mut Cx<S>, height: f64, content: Element) -> Element {
let id = cx.register_scroll();
Element::stack(Axis::Vertical, vec![content])
.height(height)
.scrollable(id)
.align(Align::Start, Align::Stretch)
}
pub fn viewport(theme: &Theme, id: ViewportId, width: f64, height: f64) -> Element {
Element::viewport(id)
.width(width)
.height(height)
.border(theme.palette.border, 1.0)
}
pub fn panel(theme: &Theme, children: Vec<Element>) -> Element {
Element::stack(Axis::Vertical, children)
.fill(theme.palette.surface)
.radius(theme.radius)
.border(theme.palette.border, 1.0)
.padding(Insets::uniform(theme.spacing.lg))
.gap(theme.spacing.md)
.align(Align::Start, Align::Stretch)
}
pub fn button(theme: &Theme) -> Element {
Element::boxed(BoxStyle {
fill: Some(theme.palette.primary),
radius: theme.radius,
border: None,
})
.width(96.0)
.height(36.0)
}
pub fn label(theme: &Theme, text: impl Into<String>) -> Element {
Element::text(text, theme.typography.body, theme.palette.text)
}
pub fn heading(theme: &Theme, text: impl Into<String>) -> Element {
Element::text(text, theme.typography.heading, theme.palette.text)
}
pub fn paragraph(theme: &Theme, text: impl Into<String>) -> Element {
Element::text(text, theme.typography.body, theme.palette.text).wrap()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Variant {
Primary,
Secondary,
Ghost,
Danger,
}
pub fn button_variant(theme: &Theme, label: impl Into<String>, variant: Variant) -> Element {
let p = &theme.palette;
let (fill, ink, border): (Option<Color>, Color, Option<Color>) = match variant {
Variant::Primary => (Some(p.primary), p.on_primary, None),
Variant::Secondary => (Some(p.surface), p.text, Some(p.border)),
Variant::Ghost => (None, p.primary, None),
Variant::Danger => (Some(p.danger), p.danger.on_color(), None),
};
let mut el = Element::stack(
Axis::Horizontal,
vec![Element::text(label, theme.typography.body, ink)],
)
.radius(theme.radius)
.padding(Insets::symmetric(theme.spacing.lg, theme.spacing.sm))
.align(Align::Center, Align::Center);
if let Some(fill) = fill {
el = el.fill(fill);
}
if let Some(border) = border {
el = el.border(border, 1.0);
}
el
}
pub fn button_labeled(theme: &Theme, label: impl Into<String>) -> Element {
button_variant(theme, label, Variant::Primary)
}
pub fn edit_string(value: &mut String, input: &KeyInput) {
match input {
KeyInput::Text(t) => value.push_str(t),
KeyInput::Backspace => {
value.pop();
}
_ => {}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EditBuffer {
text: String,
caret: usize,
anchor: Option<usize>,
}
impl EditBuffer {
pub fn new() -> Self {
Self::default()
}
pub fn from_text(text: impl Into<String>) -> Self {
let text = text.into();
let caret = text.len();
Self {
text,
caret,
anchor: None,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn caret(&self) -> usize {
self.caret
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn selection(&self) -> Option<(usize, usize)> {
match self.anchor {
Some(a) if a != self.caret => Some((a.min(self.caret), a.max(self.caret))),
_ => None,
}
}
fn delete_selection(&mut self) -> bool {
if let Some((s, e)) = self.selection() {
self.text.replace_range(s..e, "");
self.caret = s;
self.anchor = None;
true
} else {
self.anchor = None;
false
}
}
pub fn insert(&mut self, s: &str) {
self.delete_selection();
self.text.insert_str(self.caret, s);
self.caret += s.len();
}
pub fn backspace(&mut self) {
if self.delete_selection() {
return;
}
if let Some(prev) = self.prev_boundary(self.caret) {
self.text.replace_range(prev..self.caret, "");
self.caret = prev;
}
}
pub fn delete(&mut self) {
if self.delete_selection() {
return;
}
if let Some(next) = self.next_boundary(self.caret) {
self.text.replace_range(self.caret..next, "");
}
}
pub fn move_left(&mut self) {
if let Some((s, _)) = self.selection() {
self.caret = s;
} else if let Some(prev) = self.prev_boundary(self.caret) {
self.caret = prev;
}
self.anchor = None;
}
pub fn move_right(&mut self) {
if let Some((_, e)) = self.selection() {
self.caret = e;
} else if let Some(next) = self.next_boundary(self.caret) {
self.caret = next;
}
self.anchor = None;
}
pub fn home(&mut self) {
self.caret = self.line_start(self.caret);
self.anchor = None;
}
pub fn end(&mut self) {
self.caret = self.line_end(self.caret);
self.anchor = None;
}
pub fn up(&mut self) {
self.caret = self.caret_up(self.caret);
self.anchor = None;
}
pub fn down(&mut self) {
self.caret = self.caret_down(self.caret);
self.anchor = None;
}
fn line_start(&self, byte: usize) -> usize {
self.text[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0)
}
fn line_end(&self, byte: usize) -> usize {
self.text[byte..]
.find('\n')
.map(|i| byte + i)
.unwrap_or(self.text.len())
}
fn caret_up(&self, byte: usize) -> usize {
let start = self.line_start(byte);
if start == 0 {
return 0; }
let col = byte - start;
let prev_start = self.line_start(start - 1);
let prev_end = start - 1; self.snap((prev_start + col).min(prev_end))
}
fn caret_down(&self, byte: usize) -> usize {
let end = self.line_end(byte);
if end == self.text.len() {
return self.text.len(); }
let col = byte - self.line_start(byte);
let next_start = end + 1;
let next_end = self.line_end(next_start);
self.snap((next_start + col).min(next_end))
}
fn extend(&mut self, motion: impl FnOnce(&mut Self)) {
if self.anchor.is_none() {
self.anchor = Some(self.caret);
}
let anchor = self.anchor;
motion(self);
self.anchor = anchor; }
pub fn select_left(&mut self) {
self.extend(|b| {
if let Some(prev) = b.prev_boundary(b.caret) {
b.caret = prev;
}
});
}
pub fn select_right(&mut self) {
self.extend(|b| {
if let Some(next) = b.next_boundary(b.caret) {
b.caret = next;
}
});
}
pub fn select_home(&mut self) {
self.extend(|b| b.caret = b.line_start(b.caret));
}
pub fn select_end(&mut self) {
self.extend(|b| b.caret = b.line_end(b.caret));
}
pub fn select_all(&mut self) {
self.anchor = Some(0);
self.caret = self.text.len();
}
pub fn place_caret(&mut self, i: usize) {
self.caret = self.snap(i);
self.anchor = None;
}
pub fn extend_to(&mut self, i: usize) {
if self.anchor.is_none() {
self.anchor = Some(self.caret);
}
self.caret = self.snap(i);
}
fn snap(&self, i: usize) -> usize {
let mut i = i.min(self.text.len());
while !self.text.is_char_boundary(i) {
i -= 1;
}
i
}
pub fn apply(&mut self, input: &KeyInput) {
match input {
KeyInput::Text(t) => self.insert(t),
KeyInput::Backspace => self.backspace(),
KeyInput::Delete => self.delete(),
KeyInput::Left => self.move_left(),
KeyInput::Right => self.move_right(),
KeyInput::Up => self.up(),
KeyInput::Down => self.down(),
KeyInput::Home => self.home(),
KeyInput::End => self.end(),
KeyInput::SelectLeft => self.select_left(),
KeyInput::SelectRight => self.select_right(),
KeyInput::SelectUp => self.extend(|b| b.caret = b.caret_up(b.caret)),
KeyInput::SelectDown => self.extend(|b| b.caret = b.caret_down(b.caret)),
KeyInput::SelectHome => self.select_home(),
KeyInput::SelectEnd => self.select_end(),
KeyInput::SelectAll => self.select_all(),
KeyInput::Copy => {
if let Some(sel) = self.selected_text() {
crate::core::set_clipboard_text(&sel);
}
}
KeyInput::Cut => {
if let Some(sel) = self.selected_text() {
crate::core::set_clipboard_text(&sel);
self.delete_selection();
}
}
KeyInput::Paste => {
let text = crate::core::clipboard_text();
if !text.is_empty() {
self.insert(&text);
}
}
KeyInput::Enter => self.insert("\n"),
KeyInput::Escape => {}
}
}
pub fn selected_text(&self) -> Option<String> {
self.selection().map(|(s, e)| self.text[s..e].to_string())
}
fn prev_boundary(&self, i: usize) -> Option<usize> {
if i == 0 {
return None;
}
let mut p = i - 1;
while !self.text.is_char_boundary(p) {
p -= 1;
}
Some(p)
}
fn next_boundary(&self, i: usize) -> Option<usize> {
if i >= self.text.len() {
return None;
}
let mut n = i + 1;
while n < self.text.len() && !self.text.is_char_boundary(n) {
n += 1;
}
Some(n)
}
}
pub fn text_field<S>(
cx: &mut Cx<S>,
theme: &Theme,
value: &str,
on_key: impl FnMut(&mut S, &KeyInput) + 'static,
) -> Element {
let shown = if value.is_empty() {
String::from(" ")
} else {
value.to_string()
};
Element::stack(
Axis::Horizontal,
vec![Element::text(shown, theme.font_size, theme.palette.text)],
)
.fill(theme.palette.surface)
.radius(theme.radius)
.border(theme.palette.border, 1.0)
.padding(Insets::symmetric(theme.spacing.md, theme.spacing.sm))
.align(Align::Start, Align::Center)
.width(200.0)
.on_key(cx, on_key)
}
pub fn text_editor<S>(
cx: &mut Cx<S>,
theme: &Theme,
buffer: &EditBuffer,
field: impl Fn(&mut S) -> &mut EditBuffer + Copy + 'static,
) -> Element {
let shown = if buffer.is_empty() {
String::from(" ")
} else {
buffer.text().to_string()
};
let text = Element::text(shown, theme.font_size, theme.palette.text)
.caret(buffer.caret())
.selection(buffer.selection());
Element::stack(Axis::Horizontal, vec![text])
.fill(theme.palette.surface)
.radius(theme.radius)
.border(theme.palette.border, 1.0)
.padding(Insets::symmetric(theme.spacing.md, theme.spacing.sm))
.align(Align::Start, Align::Center)
.width(200.0)
.on_key(cx, move |s, k| field(s).apply(k))
.on_text_pos(cx, move |s, index, extend| {
let b = field(s);
if extend {
b.extend_to(index);
} else {
b.place_caret(index);
}
})
}
pub fn text_area<S>(
cx: &mut Cx<S>,
theme: &Theme,
buffer: &EditBuffer,
field: impl Fn(&mut S) -> &mut EditBuffer + Copy + 'static,
) -> Element {
let shown = if buffer.is_empty() {
String::from(" ")
} else {
buffer.text().to_string()
};
let text = Element::text(shown, theme.font_size, theme.palette.text)
.caret(buffer.caret())
.selection(buffer.selection());
Element::stack(Axis::Horizontal, vec![text])
.fill(theme.palette.surface)
.radius(theme.radius)
.border(theme.palette.border, 1.0)
.padding(Insets::symmetric(theme.spacing.md, theme.spacing.sm))
.align(Align::Start, Align::Start)
.width(280.0)
.height(120.0)
.on_key(cx, move |s, k| field(s).apply(k))
.on_text_pos(cx, move |s, index, extend| {
let b = field(s);
if extend {
b.extend_to(index);
} else {
b.place_caret(index);
}
})
}
pub fn checkbox<S>(
cx: &mut Cx<S>,
theme: &Theme,
checked: bool,
on_toggle: impl FnMut(&mut S) + 'static,
) -> Element {
let mark = if checked {
vec![Element::text(
"✓",
theme.font_size,
theme.palette.on_primary,
)]
} else {
Vec::new()
};
Element::stack(Axis::Horizontal, mark)
.fill(if checked {
theme.palette.primary
} else {
theme.palette.surface
})
.border(theme.palette.border, 1.0)
.radius(theme.radius / 2.0)
.width(22.0)
.height(22.0)
.align(Align::Center, Align::Center)
.on_tap(cx, on_toggle)
}
pub fn switch<S>(
cx: &mut Cx<S>,
theme: &Theme,
on: bool,
on_toggle: impl FnMut(&mut S) + 'static,
) -> Element {
let knob = Element::boxed(BoxStyle {
fill: Some(Color::WHITE),
radius: 8.0,
border: None,
})
.width(16.0)
.height(16.0);
let children = if on {
vec![spacer(), knob]
} else {
vec![knob, spacer()]
};
Element::stack(Axis::Horizontal, children)
.fill(if on {
theme.palette.primary
} else {
theme.palette.border
})
.radius(11.0)
.width(40.0)
.height(22.0)
.padding(Insets::uniform(3.0))
.align(Align::Start, Align::Center)
.on_tap(cx, on_toggle)
}
pub fn slider<S>(
cx: &mut Cx<S>,
theme: &Theme,
value: f64,
on_change: impl FnMut(&mut S, f64) + 'static,
) -> Element {
let v = value.clamp(0.0, 1.0);
let knob = Element::boxed(BoxStyle {
fill: Some(theme.palette.primary),
radius: 8.0,
border: None,
})
.width(16.0)
.height(16.0);
Element::stack(
Axis::Horizontal,
vec![spacer().grow(v), knob, spacer().grow(1.0 - v)],
)
.fill(theme.palette.border)
.radius(11.0)
.width(160.0)
.height(22.0)
.padding(Insets::symmetric(3.0, 3.0))
.align(Align::Start, Align::Center)
.on_drag(cx, on_change)
}
pub fn divider(theme: &Theme) -> Element {
Element::boxed(BoxStyle {
fill: Some(theme.palette.border),
radius: 0.0,
border: None,
})
.height(1.0)
}
pub fn swatch(color: Color, size: f64, radius: f64) -> Element {
Element::boxed(BoxStyle {
fill: Some(color),
radius,
border: None,
})
.width(size)
.height(size)
}
pub fn setting_row(theme: &Theme, accent: Color) -> Element {
row(vec![
swatch(accent, 24.0, theme.radius / 2.0),
spacer(),
button(theme),
])
.gap(theme.spacing.md)
.align(Align::Start, Align::Center)
.height(40.0)
}
pub fn radio<S>(
cx: &mut Cx<S>,
theme: &Theme,
selected: bool,
on_select: impl FnMut(&mut S) + 'static,
) -> Element {
let dot = if selected {
vec![
Element::boxed(BoxStyle {
fill: Some(theme.palette.primary),
radius: 5.0,
border: None,
})
.width(10.0)
.height(10.0),
]
} else {
Vec::new()
};
Element::stack(Axis::Horizontal, dot)
.fill(theme.palette.surface)
.border(theme.palette.border, 1.0)
.radius(11.0) .width(22.0)
.height(22.0)
.align(Align::Center, Align::Center)
.on_tap(cx, on_select)
}
pub fn tabs<S>(
cx: &mut Cx<S>,
theme: &Theme,
labels: &[&str],
selected: usize,
on_select: impl Fn(&mut S, usize) + Clone + 'static,
) -> Element {
let p = &theme.palette;
let mut cells = Vec::with_capacity(labels.len());
for (i, text) in labels.iter().enumerate() {
let active = i == selected;
let (fill, ink) = if active {
(p.primary, p.on_primary)
} else {
(p.surface, p.text)
};
let on = on_select.clone();
let cell = Element::stack(
Axis::Horizontal,
vec![Element::text(*text, theme.typography.body, ink)],
)
.fill(fill)
.border(p.border, 1.0)
.radius(theme.radius)
.padding(Insets::symmetric(theme.spacing.md, theme.spacing.sm))
.align(Align::Center, Align::Center)
.grow(1.0)
.on_tap(cx, move |s: &mut S| on(s, i));
cells.push(cell);
}
Element::stack(Axis::Horizontal, cells)
.gap(theme.spacing.xs)
.align(Align::Start, Align::Stretch)
}
pub fn progress_bar(theme: &Theme, fraction: f64) -> Element {
let f = fraction.clamp(0.0, 1.0);
let fill = Element::boxed(BoxStyle {
fill: Some(theme.palette.primary),
radius: 4.0,
border: None,
})
.grow(f);
Element::stack(Axis::Horizontal, vec![fill, spacer().grow(1.0 - f)])
.fill(theme.palette.border)
.radius(4.0)
.width(200.0)
.height(8.0)
}
pub fn spinner(theme: &Theme) -> Element {
Element::boxed(BoxStyle {
fill: None,
radius: 11.0,
border: Some((theme.palette.primary, 3.0)),
})
.width(22.0)
.height(22.0)
}
pub fn menu_item<S>(
cx: &mut Cx<S>,
theme: &Theme,
text: impl Into<String>,
on_select: impl FnMut(&mut S) + 'static,
) -> Element {
Element::stack(Axis::Horizontal, vec![label(theme, text)])
.radius(theme.radius / 2.0)
.padding(Insets::symmetric(theme.spacing.md, theme.spacing.sm))
.align(Align::Start, Align::Center)
.on_tap(cx, on_select)
}
pub fn menu(theme: &Theme, items: Vec<Element>) -> Element {
Element::stack(Axis::Vertical, items)
.fill(theme.palette.surface)
.border(theme.palette.border, 1.0)
.radius(theme.radius)
.padding(Insets::uniform(theme.spacing.xs))
.gap(2.0)
.align(Align::Start, Align::Stretch)
.width(180.0)
}
pub fn open_menu<S>(
cx: &mut Cx<S>,
theme: &Theme,
at: Point,
items: Vec<Element>,
on_dismiss: impl FnMut(&mut S) + 'static,
) {
let content = menu(theme, items);
let dismiss = cx.register(on_dismiss);
cx.overlay(OverlaySpec {
content,
anchor: Anchor::At(at),
modal: false,
dismiss: Some(dismiss),
});
}
pub fn open_dialog<S>(
cx: &mut Cx<S>,
theme: &Theme,
title: impl Into<String>,
body: Element,
actions: Vec<Element>,
on_dismiss: impl FnMut(&mut S) + 'static,
) {
let dismiss = cx.register(on_dismiss);
let content = panel(
theme,
vec![
heading(theme, title),
body,
row(actions)
.gap(theme.spacing.sm)
.align(Align::End, Align::Center),
],
)
.width(340.0);
cx.overlay(OverlaySpec {
content,
anchor: Anchor::Center,
modal: true,
dismiss: Some(dismiss),
});
}
pub fn tooltip(theme: &Theme, text: impl Into<String>) -> Element {
Element::stack(
Axis::Horizontal,
vec![Element::text(
text.into(),
theme.typography.caption,
theme.palette.on_primary,
)],
)
.fill(theme.palette.text)
.radius(theme.radius / 2.0)
.padding(Insets::symmetric(theme.spacing.sm, theme.spacing.xs))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{ElementKind, render_view};
use crate::geometry::Size;
#[test]
fn panel_is_a_decorated_vertical_stack() {
let p = panel(&Theme::light(), vec![divider(&Theme::light())]);
assert!(p.decoration.fill.is_some());
assert!(matches!(
p.kind,
ElementKind::Stack {
axis: Axis::Vertical,
..
}
));
}
#[test]
fn setting_row_paints_swatch_and_button() {
let theme = Theme::light();
let scene = render_view(
&setting_row(&theme, Color::rgb(200, 80, 80)),
Size::new(300.0, 40.0),
&theme,
None,
);
assert_eq!(scene.len(), 2);
}
#[test]
fn edit_buffer_inserts_and_moves_caret() {
let mut b = EditBuffer::new();
b.insert("ab");
assert_eq!((b.text(), b.caret()), ("ab", 2));
b.move_left();
assert_eq!(b.caret(), 1);
b.insert("X"); assert_eq!((b.text(), b.caret()), ("aXb", 2));
b.home();
assert_eq!(b.caret(), 0);
b.end();
assert_eq!(b.caret(), 3);
}
#[test]
fn edit_buffer_backspace_and_delete_at_caret() {
let mut b = EditBuffer::from_text("abc");
assert_eq!(b.caret(), 3); b.backspace();
assert_eq!((b.text(), b.caret()), ("ab", 2));
b.home();
b.delete();
assert_eq!((b.text(), b.caret()), ("b", 0));
b.backspace();
assert_eq!(b.text(), "b");
b.end();
b.delete();
assert_eq!(b.text(), "b");
}
#[test]
fn edit_buffer_respects_utf8_boundaries() {
let mut b = EditBuffer::from_text("é🦀");
assert_eq!(b.caret(), "é🦀".len());
b.move_left(); assert_eq!(b.caret(), "é".len());
b.move_left();
assert_eq!(b.caret(), 0);
b.delete(); assert_eq!(b.text(), "🦀");
}
#[test]
fn edit_buffer_apply_dispatches_keys() {
let mut b = EditBuffer::new();
for k in [
KeyInput::Text("hi".into()),
KeyInput::Left,
KeyInput::Text("X".into()),
] {
b.apply(&k);
}
assert_eq!((b.text(), b.caret()), ("hXi", 2));
}
#[test]
fn edit_buffer_shift_select_and_replace() {
let mut b = EditBuffer::from_text("hello");
b.home(); assert_eq!(b.selection(), None);
b.select_right();
b.select_right();
b.select_right(); assert_eq!(b.selection(), Some((0, 3)));
b.insert("X");
assert_eq!((b.text(), b.caret(), b.selection()), ("Xlo", 1, None));
}
#[test]
fn edit_buffer_select_all_then_backspace_clears() {
let mut b = EditBuffer::from_text("abc");
b.select_all();
assert_eq!(b.selection(), Some((0, 3)));
b.backspace(); assert!(b.is_empty());
assert_eq!(b.selection(), None);
}
#[test]
fn edit_buffer_plain_motion_collapses_selection() {
let mut b = EditBuffer::from_text("abcd");
b.home();
b.select_right();
b.select_right(); assert_eq!(b.selection(), Some((0, 2)));
b.move_left(); assert_eq!((b.caret(), b.selection()), (0, None));
}
#[test]
fn edit_buffer_select_respects_utf8() {
let mut b = EditBuffer::from_text("é🦀");
b.home();
b.select_right(); assert_eq!(b.selection(), Some((0, "é".len())));
b.select_right(); assert_eq!(b.selection(), Some((0, "é🦀".len())));
}
#[test]
fn edit_buffer_place_caret_and_extend() {
let mut b = EditBuffer::from_text("hello");
b.place_caret(2); assert_eq!((b.caret(), b.selection()), (2, None));
b.extend_to(4); assert_eq!(b.selection(), Some((2, 4)));
b.extend_to(0); assert_eq!(b.selection(), Some((0, 2)));
let mut u = EditBuffer::from_text("é🦀");
u.place_caret(999);
assert_eq!(u.caret(), "é🦀".len());
u.place_caret(1); assert_eq!(u.caret(), 0);
}
#[test]
fn edit_buffer_copy_paste_round_trips_through_the_clipboard() {
let mut b = EditBuffer::from_text("Clip");
b.apply(&KeyInput::SelectAll);
b.apply(&KeyInput::Copy); b.apply(&KeyInput::End); b.apply(&KeyInput::Paste); assert_eq!(b.text(), "ClipClip");
assert_eq!(crate::core::clipboard_text(), "Clip");
b.apply(&KeyInput::SelectAll);
b.apply(&KeyInput::Cut);
assert_eq!(b.text(), "");
assert_eq!(crate::core::clipboard_text(), "ClipClip");
}
#[test]
fn edit_buffer_enter_and_line_navigation() {
let mut b = EditBuffer::new();
b.apply(&KeyInput::Text("ab".into()));
b.apply(&KeyInput::Enter); b.apply(&KeyInput::Text("cde".into())); assert_eq!((b.text(), b.caret()), ("ab\ncde", 6));
b.up();
assert_eq!(b.caret(), 2);
b.home();
assert_eq!(b.caret(), 0);
b.end();
assert_eq!(b.caret(), 2); b.down();
assert_eq!(b.caret(), 5); }
#[test]
fn edit_buffer_shift_down_selects_across_lines() {
let mut b = EditBuffer::from_text("ab\ncd");
b.home(); b.up(); b.home(); b.apply(&KeyInput::SelectDown); let (s, e) = b.selection().expect("selection");
assert_eq!(s, 0);
assert!(e >= 3, "selection should reach into line 1, got {e}");
}
#[test]
fn text_editor_carries_caret_on_its_text_leaf() {
let theme = Theme::light();
let mut cx = Cx::new(&theme);
let buf = EditBuffer::from_text("hello");
let field = text_editor(&mut cx, &theme, &buf, |s: &mut EditBuffer| s);
assert!(field.focus.is_some());
assert!(field.text_pos.is_some());
let ElementKind::Stack { children, .. } = &field.kind else {
panic!("text_editor should be a stack");
};
assert_eq!(children[0].caret, Some(5));
}
}