Move text layout and rendering to Parley

This commit is contained in:
iris committed 2026-09-13 01:04:49 -04:00
1 parent fc1ba3cdc3
commit f659e319ff
19 files changed
+1333 -798

No files matched your search

-1
View File
@@ -1,6 +1,5 @@
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(gen_blocks)]
#![feature(associated_type_defaults)]
#![feature(unsize)]
#![feature(option_into_flat_iter)]
+5 -20
View File
@@ -1,5 +1,4 @@
use crate::prelude::*;
use cosmic_text::{Attrs, Family, Metrics};
use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
@@ -20,7 +19,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
self.attrs.color = color;
self
}
pub fn family(mut self, family: Family<'static>) -> Self {
pub fn family(mut self, family: Family) -> Self {
self.attrs.family = family;
self
}
@@ -82,19 +81,13 @@ impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
state: &mut Rsc,
builder: TextBuilder<Rsc, Self, H>,
) -> Self::Output {
let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let buf = TextBuffer::new(&builder.content);
let hint = builder.hint.get(state);
let font_system = &mut state.ui_mut().text.font_system;
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
let mut text = Text {
content: builder.content.into(),
view: TextView::new(buf, builder.attrs, hint),
};
text.content.changed = false;
builder.attrs.apply(font_system, &mut text.view.buf, None);
text
}
}
@@ -110,19 +103,11 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
state: &mut State,
builder: TextBuilder<State, Self, H>,
) -> Self::Output {
let buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit::new(
let buf = TextBuffer::new(&builder.content);
TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(state)),
builder.output.mode,
);
let font_system = &mut state.ui_mut().text.font_system;
text.buf
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
builder.attrs.apply(font_system, &mut text.buf, None);
text
)
}
}
+256 -394
View File
@@ -1,17 +1,30 @@
use crate::prelude::*;
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
use iris_core::{TextData, UiColor};
use parley::{Affinity, Layout, Selection};
use std::ops::{Deref, DerefMut};
use unicode_segmentation::UnicodeSegmentation;
use winit::{
event::KeyEvent,
keyboard::{Key, NamedKey},
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Motion {
Left,
Right,
LeftWord,
RightWord,
Up,
Down,
LineStart,
LineEnd,
}
pub struct TextEdit {
view: TextView,
selection: TextSelection,
history: Vec<(String, TextSelection)>,
double_hit: Option<Cursor>,
/// `None` represents unfocused, which Parley's `Selection` cannot express.
selection: Option<Selection>,
history: Vec<(String, Option<Selection>)>,
double_hit: Option<usize>,
pub mode: EditMode,
}
@@ -25,27 +38,19 @@ impl TextEdit {
pub fn new(view: TextView, mode: EditMode) -> Self {
Self {
view,
selection: Default::default(),
selection: None,
history: Default::default(),
double_hit: None,
mode,
}
}
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
let (start, end) = sort_cursors(start, end);
let mut iter = self.buf.lines.iter().skip(start.line);
let first = iter.next().unwrap();
if start.line == end.line {
first.text()[start.index..end.index].to_string()
} else {
let mut str = first.text()[start.index..].to_string();
for _ in (start.line + 1)..end.line {
str = str + "\n" + iter.next().unwrap().text();
}
let last = iter.next().unwrap();
str = str + "\n" + &last.text()[..end.index];
str
pub fn selected_text(&self) -> Option<String> {
let sel = self.selection?;
if sel.is_collapsed() {
return None;
}
Some(self.buf.text()[sel.text_range()].to_string())
}
}
@@ -57,39 +62,29 @@ impl Widget for TextEdit {
painter.layer = base;
let region = self.region();
let size = vec2(1, self.attrs.line_height);
match self.selection {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
if let Some(offset) = cursor_pos(cursor, &self.buf) {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(offset).within(&region),
);
}
}
TextSelection::Span { start, end } => {
let (start, end) = sort_cursors(start, end);
for (l, x, width) in iter_layout_lines(start, end, &self.buf) {
let top_left = vec2(x, self.attrs.line_height * l as f32);
painter.primitive_within(
RectPrimitive::color(Color::SKY),
size.with_x(width)
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
}
if let Some(end_offset) = cursor_pos(end, &self.buf) {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT)
.offset(end_offset)
.within(&region),
);
}
}
let Some(selection) = self.selection else {
return;
};
let layout = self.view.buf.layout();
// parley reports selection as boxes in layout space, so bidi and
// wrapped lines come out right without this code knowing about either.
for (rect, _) in selection.geometry(layout) {
let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within(
RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
}
let caret = selection.focus().geometry(layout, CARET_WIDTH);
let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
@@ -101,154 +96,60 @@ impl Widget for TextEdit {
}
}
/// provides top left + width
fn iter_layout_lines(
start: Cursor,
end: Cursor,
buf: &TextBuffer,
) -> impl Iterator<Item = (usize, f32, f32)> {
gen move {
let mut iter = buf.layout_runs().enumerate();
for (i, line) in iter.by_ref() {
if line.line_i == start.line
&& let Some(start_x) = index_x(&line, start.index)
{
if start.line == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, start_x, end_x - start_x);
return;
}
yield (i, start_x, line.line_w - start_x);
break;
}
}
for (i, line) in iter {
if line.line_i > end.line {
return;
}
if line.line_i == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, 0.0, end_x);
return;
}
yield (i, 0.0, line.line_w);
}
}
}
/// copied & modified from fn found in Editor in cosmic_text
/// returns x pos of a (non layout) index within an layout run
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
for glyph in run.glyphs.iter() {
if index == glyph.start {
return Some(glyph.x);
} else if index > glyph.start && index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < index {
before += 1;
}
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some(glyph.x + offset);
}
}
None
}
/// returns top of line segment where cursor should visually select
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some(vec2(run.line_w, run.line_top));
if let Some(pos) = index_x(&run, cursor.index) {
return Some(vec2(pos, run.line_top));
}
}
prev
}
const CARET_WIDTH: f32 = 1.0;
pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit,
pub font_system: &'a mut FontSystem,
pub data: &'a mut TextData,
}
impl<'a> TextEditCtx<'a> {
/// Returns a layout synchronized with the current text.
fn layout(&mut self) -> &Layout<UiColor> {
let attrs = self.text.view.attrs.clone();
let width = self.text.view.wrap_width();
self.text.view.buf.shape(self.data, &attrs, width);
self.text.view.buf.layout()
}
/// Keep the selection valid after the text underneath it changed.
fn refresh(&mut self) {
if let Some(sel) = self.text.selection {
let layout = self.layout();
self.text.selection = Some(sel.refresh(layout));
}
}
pub fn take(&mut self) -> String {
let text = self
.text
.buf
.lines
.drain(..)
.map(|l| l.into_text())
.collect::<Vec<_>>()
.join("\n");
self.text
.buf
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
self.text.selection.clear();
let text = self.text.view.buf.text().to_string();
self.set("");
text
}
pub fn set(&mut self, text: &str) {
let text = self.string(text);
self.text
.buf
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
self.text.view.buf.set_text(text);
self.text.view.buf.changed = true;
self.text.selection = None;
}
pub fn motion(&mut self, motion: Motion, select: bool) {
if let TextSelection::Pos(cursor) = self.text.selection
&& let Some(new) = self.buf_motion(cursor, motion)
{
if select {
self.text.selection = TextSelection::Span {
start: cursor,
end: new,
};
} else {
self.text.selection = TextSelection::Pos(new);
}
} else if let TextSelection::Span { start, end } = self.text.selection {
if select {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Span { start, end: cursor };
}
} else {
let (start, end) = sort_cursors(start, end);
let sel = &mut self.text.selection;
match motion {
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
_ => {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Pos(cursor);
}
}
}
}
}
let Some(sel) = self.text.selection else {
return;
};
let layout = self.layout();
let sel = apply_motion(sel, layout, motion, select);
self.text.selection = Some(sel);
}
/// Replace the `len` characters before the caret. This is the IME's
/// preedit path: it re-sends the whole composition each time.
pub fn replace(&mut self, len: usize, text: &str) {
let text = self.string(text);
for _ in 0..len {
self.delete(false);
self.backspace(false);
}
self.insert_inner(&text, false);
self.insert_str(&text);
}
fn string(&self, text: &str) -> String {
@@ -261,202 +162,184 @@ impl<'a> TextEditCtx<'a> {
pub fn insert(&mut self, text: &str) {
let text = self.string(text);
let mut lines = text.split('\n');
let Some(first) = lines.next() else {
self.insert_str(&text);
}
fn insert_str(&mut self, text: &str) {
if text.is_empty() {
return;
};
self.insert_inner(first, true);
for line in lines {
self.newline();
self.insert_inner(line, true);
}
}
pub fn clear_span(&mut self) -> bool {
if let TextSelection::Span { start, end } = self.text.selection {
self.delete_between(start, end);
let (start, _) = sort_cursors(start, end);
self.text.selection = TextSelection::Pos(start);
true
} else {
false
}
}
pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
let lines = &mut self.text.view.buf.lines;
let (start, end) = sort_cursors(start, end);
if start.line == end.line {
let line = &mut lines[start.line];
let text = line.text();
let text = text[..start.index].to_string() + &text[end.index..];
edit_line(line, text);
} else {
// start
let start_text = lines[start.line].text()[..start.index].to_string();
let end_text = &lines[end.line].text()[end.index..];
let text = start_text + end_text;
edit_line(&mut lines[start.line], text);
}
// between
let range = (start.line + 1)..=end.line;
if !range.is_empty() {
lines.splice(range, None);
}
}
fn insert_inner(&mut self, text: &str, mov: bool) {
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let line = &mut self.text.view.buf.lines[cursor.line];
let mut line_text = line.text().to_string();
line_text.insert_str(cursor.index, text);
edit_line(line, line_text);
if mov {
for _ in 0..text.chars().count() {
self.motion(Motion::Right, false);
}
}
let at = match self.text.selection {
Some(sel) => sel.focus().index(),
None => return,
};
let at = at.min(self.text.view.buf.text().len());
self.text.view.buf.edit().insert_str(at, text);
self.text.view.buf.changed = true;
self.set_caret(at + text.len());
}
/// True when there was a span to remove.
pub fn clear_span(&mut self) -> bool {
let Some(sel) = self.text.selection else {
return false;
};
if sel.is_collapsed() {
return false;
}
let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), "");
self.text.view.buf.changed = true;
self.set_caret(range.start);
true
}
fn set_caret(&mut self, index: usize) {
let index = index.min(self.text.view.buf.text().len());
let layout = self.layout();
self.text.selection = Some(Selection::from_byte_index(
layout,
index,
Affinity::default(),
));
}
pub fn newline(&mut self) {
if self.text.mode == EditMode::SingleLine {
return;
}
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
let new = line.split_off(cursor.index);
cursor.line += 1;
lines.insert(cursor.line, new);
cursor.index = 0;
if self.text.mode == EditMode::MultiLine {
self.insert_str("\n");
}
}
pub fn backspace(&mut self, word: bool) {
if !self.clear_span()
&& let TextSelection::Pos(cursor) = &mut self.text.selection
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
self.delete(word);
if self.clear_span() {
return;
}
let Some(sel) = self.text.selection else {
return;
};
let end = sel.focus().index();
if end == 0 {
return;
}
let start = {
let layout = self.layout();
if word {
sel.focus().previous_logical_word(layout).index()
} else {
let Some(cluster) = sel.focus().logical_clusters(layout)[0] else {
return;
};
let range = cluster.text_range();
if cluster.is_hard_line_break() || cluster.is_emoji() {
range.start
} else {
self.text.view.buf.text()[..range.end]
.char_indices()
.next_back()
.map_or(range.start, |(start, _)| start)
}
}
};
self.delete_range(start, end);
}
pub fn delete(&mut self, word: bool) {
if !self.clear_span()
&& let TextSelection::Pos(cursor) = &mut self.text.selection
{
if self.clear_span() {
return;
}
let Some(sel) = self.text.selection else {
return;
};
let start = sel.focus().index();
if start >= self.text.view.buf.text().len() {
return;
}
let end = {
let layout = self.layout();
if word {
let start = *cursor;
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
self.delete_between(start, end);
}
sel.focus().next_logical_word(layout).index()
} else {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
if cursor.index == line.text().len() {
if cursor.line == lines.len() - 1 {
return;
}
let add = lines.remove(cursor.line + 1).into_text();
let line = &mut lines[cursor.line];
let mut cur = line.text().to_string();
cur.push_str(&add);
edit_line(line, cur);
} else {
let mut text = line.text().to_string();
text.remove(cursor.index);
edit_line(line, text);
}
let clusters = sel.focus().logical_clusters(layout);
let Some(cluster) = clusters[1].as_ref() else {
return;
};
cluster.text_range().end
}
}
};
self.delete_range(start, end);
}
fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
self.text
.buf
.cursor_motion(self.font_system, cursor, None, motion)
.map(|r| r.0)
fn delete_range(&mut self, start: usize, end: usize) {
let len = self.text.view.buf.text().len();
let (start, end) = (start.min(end).min(len), start.max(end).min(len));
if start == end {
return;
}
self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start);
}
pub fn select_word_at(&mut self, cursor: Cursor) {
if let (Some(start), Some(end)) = (
self.buf_motion(cursor, Motion::LeftWord),
self.buf_motion(cursor, Motion::RightWord),
) {
self.text.selection = TextSelection::Span { start, end };
}
}
pub fn select_line_at(&mut self, cursor: Cursor) {
let end = self.text.buf.lines[cursor.line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(cursor.line, 0),
end: Cursor::new(cursor.line, end),
pub fn select_all(&mut self) {
let len = self.text.view.buf.text().len();
if len == 0 {
return;
}
let layout = self.layout();
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
self.text.selection = Some(Selection::new(anchor, focus));
}
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos - self.text.region().top_left().to_abs(size);
let hit = self.text.buf.hit(pos.x, pos.y);
let sel = &mut self.text.selection;
match sel {
TextSelection::None => {
if !drag && let Some(hit) = hit {
*sel = TextSelection::Pos(hit)
}
let prev_sel = self.text.selection;
let prev_hit = self.text.double_hit;
let outcome = {
let layout = self.layout();
if drag {
prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit))
} else {
let hit = Selection::from_point(layout, pos.x, pos.y);
let index = hit.focus().index();
// Successive clicks at one index select the word, then the line.
Some(if recent && prev_hit == Some(index) {
(Some(Selection::line_from_point(layout, pos.x, pos.y)), None)
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
(
Some(Selection::word_from_point(layout, pos.x, pos.y)),
Some(index),
)
} else {
(Some(hit), None)
})
}
TextSelection::Pos(pos) => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => (),
(Some(hit), false) => {
if recent && hit == *pos {
self.text.double_hit = Some(hit);
return self.select_word_at(hit);
} else {
*pos = hit
}
}
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
},
TextSelection::Span { start, end } => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => *sel = TextSelection::Pos(*start),
(Some(hit), false) => {
if recent
&& let Some(double) = self.text.double_hit
&& double == hit
{
return self.select_line_at(hit);
} else {
*sel = TextSelection::Pos(hit)
}
}
(Some(hit), true) => *end = hit,
},
}
if let TextSelection::Span { start, end } = sel
&& start == end
{
*sel = TextSelection::Pos(*start);
};
if let Some((selection, double_hit)) = outcome {
self.text.selection = selection;
self.text.double_hit = double_hit;
}
}
pub fn deselect(&mut self) {
self.text.selection = TextSelection::None;
self.text.selection = None;
self.text.double_hit = None;
}
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.content(), self.text.selection);
let old = (self.text.view.buf.text().to_string(), self.text.selection);
let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo && let Some((old, selection)) = self.text.history.pop() {
self.set(&old);
self.text.selection = selection;
} else if self.text.content() != old.0 {
if undo {
if let Some((old, selection)) = self.text.history.pop() {
self.set(&old);
self.text.selection = selection;
self.refresh();
}
} else if self.text.view.buf.text() != old.0 {
self.text.history.push(old);
}
res
@@ -481,21 +364,25 @@ impl<'a> TextEditCtx<'a> {
}
}
NamedKey::ArrowRight => {
if modifiers.control {
self.motion(Motion::RightWord, modifiers.shift)
let motion = if modifiers.control {
Motion::RightWord
} else {
self.motion(Motion::Right, modifiers.shift)
}
Motion::Right
};
self.motion(motion, modifiers.shift);
}
NamedKey::ArrowLeft => {
if modifiers.control {
self.motion(Motion::LeftWord, modifiers.shift)
let motion = if modifiers.control {
Motion::LeftWord
} else {
self.motion(Motion::Left, modifiers.shift)
}
Motion::Left
};
self.motion(motion, modifiers.shift);
}
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift),
NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift),
NamedKey::Escape => {
self.deselect();
return TextInputResult::Unfocus;
@@ -507,34 +394,18 @@ impl<'a> TextEditCtx<'a> {
match text.as_str() {
"v" => return TextInputResult::Paste,
"c" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
if let Some(content) = self.text.selected_text() {
return TextInputResult::Copy(content);
}
}
"x" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
if let Some(content) = self.text.selected_text() {
self.clear_span();
return TextInputResult::Copy(content);
}
}
"a" => {
if !self.text.buf.lines[0].text().is_empty()
|| self.text.buf.lines.len() > 1
{
let lines = &self.text.buf.lines;
let last_line = lines.len() - 1;
let last_idx = lines[last_line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(0, 0),
end: Cursor::new(last_line, last_idx),
};
}
}
"z" => {
*undo = true;
}
"a" => self.select_all(),
"z" => *undo = true,
_ => self.insert(text),
}
} else {
@@ -547,6 +418,24 @@ impl<'a> TextEditCtx<'a> {
}
}
fn apply_motion(
sel: Selection,
layout: &Layout<UiColor>,
motion: Motion,
extend: bool,
) -> Selection {
match motion {
Motion::Left => sel.previous_visual(layout, extend),
Motion::Right => sel.next_visual(layout, extend),
Motion::LeftWord => sel.previous_visual_word(layout, extend),
Motion::RightWord => sel.next_visual_word(layout, extend),
Motion::Up => sel.previous_line(layout, extend),
Motion::Down => sel.next_line(layout, extend),
Motion::LineStart => sel.line_start(layout, extend),
Motion::LineEnd => sel.line_end(layout, extend),
}
}
#[derive(Default)]
pub struct Modifiers {
pub shift: bool,
@@ -569,33 +458,6 @@ pub enum TextInputResult {
Paste,
}
#[derive(Debug, Default, Clone, Copy)]
pub enum TextSelection {
#[default]
None,
Pos(Cursor),
Span {
start: Cursor,
end: Cursor,
},
}
impl TextSelection {
pub fn clear(&mut self) {
match self {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
TextSelection::Span { start: _, end: _ } => {
*self = TextSelection::None;
}
}
}
}
impl TextInputResult {
pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus)
@@ -625,7 +487,7 @@ impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
let ui = ui.ui_mut();
TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(),
font_system: &mut ui.text.font_system,
data: &mut ui.text,
}
}
}
+27 -55
View File
@@ -6,11 +6,8 @@ pub use edit::*;
use iris_core::util::MutDetect;
use crate::prelude::*;
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
use std::ops::{Deref, DerefMut};
pub const SHAPING: Shaping = Shaping::Advanced;
pub struct Text {
pub content: MutDetect<String>,
view: TextView,
@@ -25,6 +22,17 @@ pub struct TextView {
pub hint: Option<StrongWidget>,
}
impl TextView {
fn is_blank(&self) -> bool {
self.buf.is_empty()
}
/// The width used by the cached layout.
pub fn wrap_width(&self) -> Option<f32> {
self.width
}
}
impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
Self {
@@ -45,15 +53,6 @@ impl TextView {
.align(self.align)
}
fn tex_region(&self, tex: &RenderedText) -> UiRegion {
let region = tex.size.align(self.align);
let dims = tex.handle.size();
let mut region = region.offset(tex.top_left_offset);
region.x.end = region.x.start + UiScalar::abs(dims.x);
region.y.end = region.y.start + UiScalar::abs(dims.y);
region
}
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
let width = if self.attrs.wrap {
Some(ctx.px_size().x)
@@ -68,10 +67,7 @@ impl TextView {
return tex.clone();
}
self.width = width;
let font_system = &mut ctx.text.font_system;
self.attrs.apply(font_system, &mut self.buf, width);
self.buf.shape_until_scroll(font_system, false);
let tex = ctx.draw_text(&mut self.buf, &self.attrs);
let tex = ctx.draw_text(&mut self.buf, &self.attrs, width);
self.tex = Some(tex.clone());
self.attrs.changed = false;
self.buf.changed = false;
@@ -81,9 +77,8 @@ impl TextView {
self.tex.as_ref()
}
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(hint) = &self.hint
&& let [line] = &self.buf.lines[..]
&& line.text().is_empty()
if self.is_blank()
&& let Some(hint) = &self.hint
{
ctx.width(hint)
} else {
@@ -91,9 +86,8 @@ impl TextView {
}
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(hint) = &self.hint
&& let [line] = &self.buf.lines[..]
&& line.text().is_empty()
if self.is_blank()
&& let Some(hint) = &self.hint
{
ctx.height(hint)
} else {
@@ -102,47 +96,35 @@ impl TextView {
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let tex = self.render(&mut painter.size_ctx());
let region = self.tex_region(&tex);
if let Some(hint) = &self.hint
&& let [line] = &self.buf.lines[..]
&& line.text().is_empty()
let region = tex.size.align(self.align);
if self.is_blank()
&& let Some(hint) = &self.hint
{
painter.widget(hint);
} else {
painter.texture_within(&tex.handle, region);
let within = region.within(&painter.region());
painter.glyphs(&tex, within);
}
region
}
pub fn content(&self) -> String {
self.buf
.lines
.iter()
.map(|l| l.text())
.collect::<Vec<_>>()
.join("\n")
self.buf.text().to_string()
}
}
impl Text {
pub fn new(content: impl Into<String>) -> Self {
let attrs = TextAttrs::default();
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
let content: String = content.into();
Self {
content: content.into().into(),
view: TextView::new(buf, attrs, None),
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None),
content: content.into(),
}
}
fn update_buf(&mut self, ctx: &mut SizeCtx) {
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
if self.content.changed {
self.content.changed = false;
self.view.buf.set_text(
&mut ctx.text.font_system,
&self.content,
&Attrs::new().family(self.view.attrs.family),
SHAPING,
None,
);
self.view.buf.set_text(self.content.as_str());
}
}
}
@@ -164,16 +146,6 @@ impl Widget for Text {
}
}
pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
let start = a.min(b);
let end = a.max(b);
(start, end)
}
pub fn edit_line(line: &mut BufferLine, text: String) {
line.set_text(text, line.ending(), line.attrs_list().clone());
}
impl Deref for Text {
type Target = TextAttrs;