iris: move text state into shared resources
This commit is contained in:
1 parent
4cbe7baea0
commit
a8093b002b
26 files changed
+681
-462
No files matched your search
@@ -56,7 +56,7 @@ impl DesktopAppState for Client {
|
|||||||
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
|
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
|
||||||
Ok(opened) => {
|
Ok(opened) => {
|
||||||
if let Some(message) = message_argv() {
|
if let Some(message) = message_argv() {
|
||||||
opened.screen.composer.field.edit(rsc).set(&message);
|
(opened.screen.composer.field)(rsc).set(&message);
|
||||||
}
|
}
|
||||||
if let Some(text) = typed_argv() {
|
if let Some(text) = typed_argv() {
|
||||||
let field = opened.screen.composer.field;
|
let field = opened.screen.composer.field;
|
||||||
@@ -66,8 +66,8 @@ impl DesktopAppState for Client {
|
|||||||
ctx.update(move |state: &mut Client, rsc| {
|
ctx.update(move |state: &mut Client, rsc| {
|
||||||
state.set_focus(Some(field));
|
state.set_focus(Some(field));
|
||||||
let end = rsc[field].text().len();
|
let end = rsc[field].text().len();
|
||||||
let mut edit = field.edit(rsc);
|
let edit = field(rsc);
|
||||||
if edit.text.caret().is_none() {
|
if edit.caret().is_none() {
|
||||||
edit.set_cursor_byte(end);
|
edit.set_cursor_byte(end);
|
||||||
}
|
}
|
||||||
edit.insert(&ch.to_string());
|
edit.insert(&ch.to_string());
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ impl BenchClient {
|
|||||||
.any();
|
.any();
|
||||||
ui_state.set_root(rsc, tree);
|
ui_state.set_root(rsc, tree);
|
||||||
|
|
||||||
let font = rsc.ui.text.font_diagnostics();
|
let font = rsc.ui.text.borrow_mut().font_diagnostics();
|
||||||
log::info!(
|
log::info!(
|
||||||
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
|
"iris fonts: {} families found, default={:?} mono={:?}, resolved regular={:?} \
|
||||||
bold={:?} italic={:?} mono={:?}",
|
bold={:?} italic={:?} mono={:?}",
|
||||||
@@ -400,7 +400,7 @@ impl BenchClient {
|
|||||||
|
|
||||||
fn show_diagnostics(&mut self, rsc: &mut Rsc) {
|
fn show_diagnostics(&mut self, rsc: &mut Rsc) {
|
||||||
let report = self.diagnostics_text(rsc);
|
let report = self.diagnostics_text(rsc);
|
||||||
self.report_display.edit(rsc).set(&report);
|
self.report_display(rsc).set(&report);
|
||||||
self.last_report = Some(report);
|
self.last_report = Some(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ impl BenchClient {
|
|||||||
/// the keyboard-open capture (which only logs it), so the two can
|
/// the keyboard-open capture (which only logs it), so the two can
|
||||||
/// never drift into reporting different things.
|
/// never drift into reporting different things.
|
||||||
fn diagnostics_text(&self, rsc: &mut Rsc) -> String {
|
fn diagnostics_text(&self, rsc: &mut Rsc) -> String {
|
||||||
let font = rsc.ui.text.font_diagnostics();
|
let font = rsc.ui.text.borrow_mut().font_diagnostics();
|
||||||
let frame_report = match self.android_state().frame_report.report() {
|
let frame_report = match self.android_state().frame_report.report() {
|
||||||
Some(stats) => format!("{stats}"),
|
Some(stats) => format!("{stats}"),
|
||||||
None => "no frames recorded yet".to_string(),
|
None => "no frames recorded yet".to_string(),
|
||||||
@@ -481,7 +481,7 @@ impl BenchClient {
|
|||||||
}
|
}
|
||||||
self.running = true;
|
self.running = true;
|
||||||
self.android_state_mut().frame_report.reset();
|
self.android_state_mut().frame_report.reset();
|
||||||
self.report_display.edit(rsc).set("Running benchmark...");
|
self.report_display(rsc).set("Running benchmark...");
|
||||||
|
|
||||||
let platform = self.platform.clone();
|
let platform = self.platform.clone();
|
||||||
let stream_tail = self.stream_tail.clone();
|
let stream_tail = self.stream_tail.clone();
|
||||||
@@ -620,7 +620,7 @@ impl BenchClient {
|
|||||||
{rss_line}\n{battery}"
|
{rss_line}\n{battery}"
|
||||||
);
|
);
|
||||||
log::info!("iris bench report: {report}");
|
log::info!("iris bench report: {report}");
|
||||||
state.report_display.edit(rsc).set(&report);
|
state.report_display(rsc).set(&report);
|
||||||
state.last_report = Some(report);
|
state.last_report = Some(report);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -776,7 +776,7 @@ async fn run_type_phase(
|
|||||||
let text = typed.clone();
|
let text = typed.clone();
|
||||||
ctx.update(move |state: &mut BenchClient, rsc| {
|
ctx.update(move |state: &mut BenchClient, rsc| {
|
||||||
if let Some(screen) = &state.screen {
|
if let Some(screen) = &state.screen {
|
||||||
screen.composer.field.edit(rsc).set(&text);
|
(screen.composer.field)(rsc).set(&text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
|
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
|
||||||
@@ -787,7 +787,7 @@ async fn run_type_phase(
|
|||||||
let text = typed.clone();
|
let text = typed.clone();
|
||||||
ctx.update(move |state: &mut BenchClient, rsc| {
|
ctx.update(move |state: &mut BenchClient, rsc| {
|
||||||
if let Some(screen) = &state.screen {
|
if let Some(screen) = &state.screen {
|
||||||
screen.composer.field.edit(rsc).set(&text);
|
(screen.composer.field)(rsc).set(&text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
|
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
|
||||||
|
|||||||
@@ -245,19 +245,19 @@ impl TranscriptClient {
|
|||||||
let in_progress = self
|
let in_progress = self
|
||||||
.screen
|
.screen
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|screen| screen.composer.field.edit(rsc).text.text().to_string())
|
.map(|screen| (screen.composer.field)(rsc).text().to_string())
|
||||||
.filter(|t| !t.is_empty());
|
.filter(|t| !t.is_empty());
|
||||||
|
|
||||||
let rows = group_tool_runs(&self.items);
|
let rows = group_tool_runs(&self.items);
|
||||||
let (screen, tree) = ui::build_tree(rsc, rows);
|
let (screen, tree) = ui::build_tree(rsc, rows);
|
||||||
|
|
||||||
if let Some(text) = in_progress {
|
if let Some(text) = in_progress {
|
||||||
screen.composer.field.edit(rsc).set(&text);
|
(screen.composer.field)(rsc).set(&text);
|
||||||
}
|
}
|
||||||
if let Some(session_id) = self.session_id.clone() {
|
if let Some(session_id) = self.session_id.clone() {
|
||||||
let field = screen.composer.field;
|
let field = screen.composer.field;
|
||||||
rsc.register_event(field, Submit, move |ctx, rsc| {
|
rsc.register_event(field, Submit, move |ctx, rsc| {
|
||||||
let text = field.edit(rsc).take();
|
let text = field(rsc).take();
|
||||||
let text = text.trim().to_string();
|
let text = text.trim().to_string();
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
ctx.state.send_message(session_id.clone(), text);
|
ctx.state.send_message(session_id.clone(), text);
|
||||||
|
|||||||
@@ -290,19 +290,19 @@ impl Client {
|
|||||||
let in_progress = self
|
let in_progress = self
|
||||||
.screen
|
.screen
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|screen| screen.composer.field.edit(rsc).text.text().to_string())
|
.map(|screen| (screen.composer.field)(rsc).text().to_string())
|
||||||
.filter(|t| !t.is_empty());
|
.filter(|t| !t.is_empty());
|
||||||
|
|
||||||
let rows = group_tool_runs(&self.items);
|
let rows = group_tool_runs(&self.items);
|
||||||
let (screen, tree) = crate::ui::build_tree(rsc, rows);
|
let (screen, tree) = crate::ui::build_tree(rsc, rows);
|
||||||
|
|
||||||
if let Some(text) = in_progress {
|
if let Some(text) = in_progress {
|
||||||
screen.composer.field.edit(rsc).set(&text);
|
(screen.composer.field)(rsc).set(&text);
|
||||||
}
|
}
|
||||||
if let Some(session_id) = self.selected.clone() {
|
if let Some(session_id) = self.selected.clone() {
|
||||||
let field = screen.composer.field;
|
let field = screen.composer.field;
|
||||||
rsc.register_event(field, Submit, move |ctx, rsc| {
|
rsc.register_event(field, Submit, move |ctx, rsc| {
|
||||||
let text = field.edit(rsc).take();
|
let text = field(rsc).take();
|
||||||
let text = text.trim().to_string();
|
let text = text.trim().to_string();
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
ctx.state.send_message(session_id.clone(), text);
|
ctx.state.send_message(session_id.clone(), text);
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ const APPROX_LINE_HEIGHT_DP: f32 = 24.0;
|
|||||||
const FIELD_PAD_DP: f32 = 12.0;
|
const FIELD_PAD_DP: f32 = 12.0;
|
||||||
|
|
||||||
/// `field` is exposed so the caller can read its content on submit
|
/// `field` is exposed so the caller can read its content on submit
|
||||||
/// (`field.edit(rsc).text()`) and clear it afterward
|
/// (`field(rsc).text()`) and clear it afterward (`field(rsc).set("")`).
|
||||||
/// (`field.edit(rsc).set("")`).
|
|
||||||
pub struct Composer {
|
pub struct Composer {
|
||||||
pub field: WeakWidget<TextEdit>,
|
pub field: WeakWidget<TextEdit>,
|
||||||
/// The bar's own outer padding -- only `bottom` is ever changed, by
|
/// The bar's own outer padding -- only `bottom` is ever changed, by
|
||||||
|
|||||||
@@ -532,6 +532,24 @@ mod apply_tests {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registering_a_font_after_drawing_invalidates_its_text_owner() {
|
||||||
|
let mut rsc = TestRsc {
|
||||||
|
ui: Ui::default(),
|
||||||
|
events: EventManager::default(),
|
||||||
|
};
|
||||||
|
rsc.ui.resize((800.0, 600.0));
|
||||||
|
let root = wtext("already shaped").add_strong(&mut rsc).any();
|
||||||
|
rsc.draw(&root);
|
||||||
|
assert!(!rsc.widgets().has_updates());
|
||||||
|
|
||||||
|
rsc.ui.register_font("late-icons", ICON_FONT).unwrap();
|
||||||
|
|
||||||
|
assert!(rsc.widgets().has_updates());
|
||||||
|
rsc.draw(&root);
|
||||||
|
assert!(!rsc.widgets().has_updates());
|
||||||
|
}
|
||||||
|
|
||||||
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
|
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
|
||||||
let mut rsc = TestRsc {
|
let mut rsc = TestRsc {
|
||||||
ui: Ui::default(),
|
ui: Ui::default(),
|
||||||
|
|||||||
@@ -197,14 +197,14 @@ fn a_space_in_the_composer_finishes_layout() {
|
|||||||
screen.composer.set_bottom_inset(&mut h.rsc, 1000.0);
|
screen.composer.set_bottom_inset(&mut h.rsc, 1000.0);
|
||||||
h.frame(PHONE_FRAME_MS * 2);
|
h.frame(PHONE_FRAME_MS * 2);
|
||||||
h.state.set_focus(Some(screen.composer.field));
|
h.state.set_focus(Some(screen.composer.field));
|
||||||
screen.composer.field.edit(&mut h.rsc).set_cursor_byte(0);
|
(screen.composer.field)(&mut h.rsc).set_cursor_byte(0);
|
||||||
|
|
||||||
for text in ["h", "i", " "] {
|
for text in ["h", "i", " "] {
|
||||||
screen.composer.field.edit(&mut h.rsc).insert(text);
|
(screen.composer.field)(&mut h.rsc).insert(text);
|
||||||
h.frame(PHONE_FRAME_MS);
|
h.frame(PHONE_FRAME_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(screen.composer.field.edit(&mut h.rsc).text.text(), "hi ");
|
assert_eq!(&*(screen.composer.field)(&mut h.rsc).text(), "hi ");
|
||||||
let region = h
|
let region = h
|
||||||
.rsc
|
.rsc
|
||||||
.ui
|
.ui
|
||||||
@@ -232,9 +232,9 @@ fn a_newline_leaves_the_caret_inside_the_composers_padding() {
|
|||||||
h.frame(PHONE_FRAME_MS * 2);
|
h.frame(PHONE_FRAME_MS * 2);
|
||||||
|
|
||||||
h.state.set_focus(Some(screen.composer.field));
|
h.state.set_focus(Some(screen.composer.field));
|
||||||
screen.composer.field.edit(&mut h.rsc).set_cursor_byte(0);
|
(screen.composer.field)(&mut h.rsc).set_cursor_byte(0);
|
||||||
for _ in 0..12 {
|
for _ in 0..12 {
|
||||||
screen.composer.field.edit(&mut h.rsc).insert("a\n");
|
(screen.composer.field)(&mut h.rsc).insert("a\n");
|
||||||
h.frame(PHONE_FRAME_MS);
|
h.frame(PHONE_FRAME_MS);
|
||||||
}
|
}
|
||||||
let render_handle = h.rsc.ui.render_state();
|
let render_handle = h.rsc.ui.render_state();
|
||||||
|
|||||||
+3
-3
@@ -279,9 +279,9 @@ rasteriser (`TextData::place`, which reads back whatever `shape` set) is
|
|||||||
the display's *physical* size, and the glyph atlas holds a bitmap at the
|
the display's *physical* size, and the glyph atlas holds a bitmap at the
|
||||||
resolution it is actually shown at. `GlyphKey.size` already keys on the
|
resolution it is actually shown at. `GlyphKey.size` already keys on the
|
||||||
resolved size, so a cache entry is naturally per-physical-size with no
|
resolved size, so a cache entry is naturally per-physical-size with no
|
||||||
further change. The one caller with no `Painter` to read density from
|
further change. The callers with no `Painter` to read density from (cursor
|
||||||
(`TextEditCtx::layout`, cursor movement and hit-testing) reads a second
|
movement and hit-testing through `TextHandle::layout`) read a second copy kept
|
||||||
copy kept directly on `TextData` (`TextData::density`) instead — an
|
directly on `TextData` (`TextData::density`) instead — an
|
||||||
accepted duplication rather than threading a `Painter` into every input
|
accepted duplication rather than threading a `Painter` into every input
|
||||||
handler for one field, the same tradeoff `AndroidRenderer::content_scale`
|
handler for one field, the same tradeoff `AndroidRenderer::content_scale`
|
||||||
already makes for the Diagnostics page.
|
already makes for the Diagnostics page.
|
||||||
|
|||||||
+12
-7
@@ -814,13 +814,18 @@ private implementations of that wake; applications do not define platform
|
|||||||
event types or manually request redraws.
|
event types or manually request redraws.
|
||||||
|
|
||||||
**Iris ships no fonts, and font families are application-named strings**
|
**Iris ships no fonts, and font families are application-named strings**
|
||||||
(2026-09-12). Applications register their own font bytes on `Ui` before the
|
(2026-09-12). Applications register their own font bytes on `Ui` and select
|
||||||
first text shape and select them by the same string used by text widgets. The
|
them by the same string used by text widgets. The public boundaries accept
|
||||||
public boundaries accept `AsRef<str>`, so an application can use bare strings
|
`AsRef<str>`, so an application can use bare strings or put its own semantic
|
||||||
or put its own semantic enum in front of them without Iris hardcoding the
|
enum in front of them without Iris hardcoding the roles. Text buffers, layouts,
|
||||||
roles. ai-app owns the `ai-app-icons` family, its Nerd Fonts subset, its
|
and prepared glyphs live in a per-`Ui` generational arena; a text widget holds
|
||||||
codepoints, its license and the script that rebuilds it; the CSS generic names
|
one compact handle into it, while the arena and its shaping state are shared by
|
||||||
`sans-serif` and `monospace` continue to resolve through the platform.
|
one `Rc<RefCell<_>>`. Registering another family invalidates the live arena
|
||||||
|
entries and dirties their active owner widgets, so registration is also valid
|
||||||
|
after the first shape. ai-app owns the `ai-app-icons` family, its Nerd Fonts
|
||||||
|
subset, its codepoints, its license and the script that rebuilds it; the CSS
|
||||||
|
generic names `sans-serif` and `monospace` continue to resolve through the
|
||||||
|
platform.
|
||||||
|
|
||||||
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
||||||
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ exists only to give a row enough text to wrap across several lines at a \
|
|||||||
typical phone column width.";
|
typical phone column width.";
|
||||||
|
|
||||||
fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
|
fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
|
||||||
let mut text = Text::new(format!("Message {i}: {BODY}"));
|
let text = wtext(format!("Message {i}: {BODY}"))
|
||||||
text.wrap = true;
|
.wrap(true)
|
||||||
let text = rsc.ui.widgets.add_strong(text).any();
|
.add_strong(rsc)
|
||||||
|
.any();
|
||||||
|
|
||||||
if image_every > 0 && i.is_multiple_of(image_every) {
|
if image_every > 0 && i.is_multiple_of(image_every) {
|
||||||
let img = image::DynamicImage::new_rgba8(64, 64);
|
let img = image::DynamicImage::new_rgba8(64, 64);
|
||||||
@@ -321,9 +322,7 @@ fn bench_redraw_big_text(chars: usize, redraws: usize) {
|
|||||||
let content: String = (0..chars)
|
let content: String = (0..chars)
|
||||||
.map(|i| char::from(b'a' + (i % 26) as u8))
|
.map(|i| char::from(b'a' + (i % 26) as u8))
|
||||||
.collect();
|
.collect();
|
||||||
let mut text = Text::new(content);
|
let text = wtext(content).wrap(true).add_strong(&mut rsc);
|
||||||
text.wrap = true;
|
|
||||||
let text = rsc.ui.widgets.add_strong(text);
|
|
||||||
let handle = text.weak();
|
let handle = text.weak();
|
||||||
let root = text.any();
|
let root = text.any();
|
||||||
|
|
||||||
|
|||||||
+289
-24
@@ -1,10 +1,23 @@
|
|||||||
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
|
use crate::{
|
||||||
|
Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId,
|
||||||
|
util::{SlotId, SlotVec, Vec2},
|
||||||
|
};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
||||||
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
fontique::{Blob, FontInfoOverride},
|
fontique::{Blob, FontInfoOverride},
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, fmt, ops::Range, sync::Arc};
|
use std::{
|
||||||
|
cell::{Ref, RefCell},
|
||||||
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
|
ops::{Deref, DerefMut, Range},
|
||||||
|
rc::Rc,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
mpsc::{Receiver, Sender, channel},
|
||||||
|
},
|
||||||
|
};
|
||||||
use swash::{
|
use swash::{
|
||||||
FontRef,
|
FontRef,
|
||||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||||
@@ -25,7 +38,6 @@ pub struct FontDiagnostics {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum FontRegistrationError {
|
pub enum FontRegistrationError {
|
||||||
AlreadyRegistered(String),
|
AlreadyRegistered(String),
|
||||||
TextAlreadyShaped,
|
|
||||||
InvalidFont(String),
|
InvalidFont(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +47,6 @@ impl fmt::Display for FontRegistrationError {
|
|||||||
Self::AlreadyRegistered(family) => {
|
Self::AlreadyRegistered(family) => {
|
||||||
write!(f, "font data is already registered for {family:?}")
|
write!(f, "font data is already registered for {family:?}")
|
||||||
}
|
}
|
||||||
Self::TextAlreadyShaped => write!(
|
|
||||||
f,
|
|
||||||
"cannot register font data after text has been shaped; register application fonts before the first draw"
|
|
||||||
),
|
|
||||||
Self::InvalidFont(family) => {
|
Self::InvalidFont(family) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@@ -57,10 +65,9 @@ pub struct TextData {
|
|||||||
scale_cx: ScaleContext,
|
scale_cx: ScaleContext,
|
||||||
pub atlas: GlyphAtlas,
|
pub atlas: GlyphAtlas,
|
||||||
/// Physical pixels per dp -- a second copy of
|
/// Physical pixels per dp -- a second copy of
|
||||||
/// `UiRenderState::density`, kept here too because `TextEditCtx::layout`
|
/// `UiRenderState::density`, kept here too because cursor movement and
|
||||||
/// (cursor movement and hit-testing, `widget/text/edit.rs`) shapes text
|
/// hit-testing shape text from event callbacks that have no `Painter`,
|
||||||
/// from an event callback that has a `TextData` but no `Painter`, so it
|
/// so they have nowhere else to read the display's density from. Both copies are
|
||||||
/// has nowhere else to read the display's density from. Both copies are
|
|
||||||
/// set together, from the one place either backend learns the real
|
/// set together, from the one place either backend learns the real
|
||||||
/// value (`android::view::new_peer`); this is the same accepted
|
/// value (`android::view::new_peer`); this is the same accepted
|
||||||
/// duplication as `AndroidRenderer::content_scale`; a single source of
|
/// duplication as `AndroidRenderer::content_scale`; a single source of
|
||||||
@@ -69,7 +76,6 @@ pub struct TextData {
|
|||||||
pub density: f32,
|
pub density: f32,
|
||||||
registered_families: HashMap<String, String>,
|
registered_families: HashMap<String, String>,
|
||||||
next_registered_family: u64,
|
next_registered_family: u64,
|
||||||
shaping_started: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextData {
|
impl Default for TextData {
|
||||||
@@ -84,7 +90,6 @@ impl Default for TextData {
|
|||||||
density: 1.0,
|
density: 1.0,
|
||||||
registered_families: HashMap::new(),
|
registered_families: HashMap::new(),
|
||||||
next_registered_family: 0,
|
next_registered_family: 0,
|
||||||
shaping_started: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,9 +173,6 @@ impl TextData {
|
|||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
) -> Result<(), FontRegistrationError> {
|
) -> Result<(), FontRegistrationError> {
|
||||||
let family = family.as_ref().to_owned();
|
let family = family.as_ref().to_owned();
|
||||||
if self.shaping_started {
|
|
||||||
return Err(FontRegistrationError::TextAlreadyShaped);
|
|
||||||
}
|
|
||||||
if self.registered_families.contains_key(&family) {
|
if self.registered_families.contains_key(&family) {
|
||||||
return Err(FontRegistrationError::AlreadyRegistered(family));
|
return Err(FontRegistrationError::AlreadyRegistered(family));
|
||||||
}
|
}
|
||||||
@@ -523,7 +525,6 @@ impl TextBuffer {
|
|||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
density: f32,
|
density: f32,
|
||||||
) {
|
) {
|
||||||
data.shaping_started = true;
|
|
||||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -574,6 +575,10 @@ impl TextBuffer {
|
|||||||
.align(Alignment::Start, AlignmentOptions::default());
|
.align(Alignment::Start, AlignmentOptions::default());
|
||||||
self.shaped = Some((attrs.clone(), width, density));
|
self.shaped = Some((attrs.clone(), width, density));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.shaped = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_coords(coords: &[i16]) -> u64 {
|
fn hash_coords(coords: &[i16]) -> u64 {
|
||||||
@@ -605,6 +610,261 @@ pub struct RenderedText {
|
|||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TextResource {
|
||||||
|
buffer: TextBuffer,
|
||||||
|
attrs: TextAttrs,
|
||||||
|
rendered: Option<RenderedText>,
|
||||||
|
width: Option<f32>,
|
||||||
|
density: f32,
|
||||||
|
owner: Option<WidgetId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextResource {
|
||||||
|
fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer,
|
||||||
|
attrs,
|
||||||
|
rendered: None,
|
||||||
|
width: None,
|
||||||
|
density: 0.0,
|
||||||
|
owner: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate(&mut self) {
|
||||||
|
self.buffer.invalidate();
|
||||||
|
self.rendered = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All text storage and shaping state for one [`crate::Ui`]. Widgets retain a
|
||||||
|
/// compact [`TextHandle`] into this arena rather than owning Parley layouts.
|
||||||
|
pub struct TextResources {
|
||||||
|
data: TextData,
|
||||||
|
entries: SlotVec<TextResource>,
|
||||||
|
send: Sender<SlotId>,
|
||||||
|
recv: Receiver<SlotId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextResources {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (send, recv) = channel();
|
||||||
|
Self {
|
||||||
|
data: TextData::default(),
|
||||||
|
entries: SlotVec::new(),
|
||||||
|
send,
|
||||||
|
recv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(resources: Rc<RefCell<Self>>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle {
|
||||||
|
let (id, send) = {
|
||||||
|
let mut resources = resources.borrow_mut();
|
||||||
|
resources.free_released();
|
||||||
|
let id = resources.entries.add(TextResource::new(buffer, attrs));
|
||||||
|
(id, resources.send.clone())
|
||||||
|
};
|
||||||
|
TextHandle {
|
||||||
|
id,
|
||||||
|
resources,
|
||||||
|
send,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry(&self, id: SlotId) -> &TextResource {
|
||||||
|
self.entries
|
||||||
|
.get(id)
|
||||||
|
.expect("text resource handle points at a released slot")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_mut(&mut self, id: SlotId) -> &mut TextResource {
|
||||||
|
self.entries
|
||||||
|
.get_mut(id)
|
||||||
|
.expect("text resource handle points at a released slot")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_released(&mut self) {
|
||||||
|
for id in self.recv.try_iter() {
|
||||||
|
self.entries.free(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalidate_all(&mut self) -> Vec<WidgetId> {
|
||||||
|
let mut owners = Vec::new();
|
||||||
|
for resource in self.entries.values_mut() {
|
||||||
|
resource.invalidate();
|
||||||
|
if let Some(owner) = resource.owner
|
||||||
|
&& !owners.contains(&owner)
|
||||||
|
{
|
||||||
|
owners.push(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owners
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(&mut self, id: SlotId) {
|
||||||
|
let density = self.data.density;
|
||||||
|
let Self { data, entries, .. } = self;
|
||||||
|
let resource = entries
|
||||||
|
.get_mut(id)
|
||||||
|
.expect("text resource handle points at a released slot");
|
||||||
|
resource
|
||||||
|
.buffer
|
||||||
|
.shape(data, &resource.attrs, resource.width, density);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
id: SlotId,
|
||||||
|
width: Option<f32>,
|
||||||
|
owner: WidgetId,
|
||||||
|
textures: &mut Textures,
|
||||||
|
density: f32,
|
||||||
|
) -> (RenderedText, bool) {
|
||||||
|
let atlas_generation = self.data.atlas.generation();
|
||||||
|
let resource = self.entry_mut(id);
|
||||||
|
resource.owner = Some(owner);
|
||||||
|
if resource.width == width
|
||||||
|
&& resource.density == density
|
||||||
|
&& let Some(rendered) = &resource.rendered
|
||||||
|
&& rendered.generation == atlas_generation
|
||||||
|
{
|
||||||
|
return (rendered.clone(), false);
|
||||||
|
}
|
||||||
|
resource.width = width;
|
||||||
|
resource.density = density;
|
||||||
|
let Self { data, entries, .. } = self;
|
||||||
|
let resource = entries
|
||||||
|
.get_mut(id)
|
||||||
|
.expect("text resource handle points at a released slot");
|
||||||
|
let rendered = data.render(
|
||||||
|
&mut resource.buffer,
|
||||||
|
&resource.attrs,
|
||||||
|
width,
|
||||||
|
textures,
|
||||||
|
density,
|
||||||
|
);
|
||||||
|
resource.rendered = Some(rendered.clone());
|
||||||
|
(rendered, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextResources {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TextResources {
|
||||||
|
type Target = TextData;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for TextResources {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget-owned reference to one entry in [`TextResources`]. The arena is
|
||||||
|
/// shared once per UI; constructing a handle does not allocate a resource of
|
||||||
|
/// its own.
|
||||||
|
pub struct TextHandle {
|
||||||
|
id: SlotId,
|
||||||
|
resources: Rc<RefCell<TextResources>>,
|
||||||
|
send: Sender<SlotId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextHandle {
|
||||||
|
pub fn text(&self) -> Ref<'_, str> {
|
||||||
|
Ref::map(self.resources.borrow(), |resources| {
|
||||||
|
resources.entry(self.id).buffer.text()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attrs(&self) -> Ref<'_, TextAttrs> {
|
||||||
|
Ref::map(self.resources.borrow(), |resources| {
|
||||||
|
&resources.entry(self.id).attrs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: impl Into<String>) -> bool {
|
||||||
|
let text = text.into();
|
||||||
|
let mut resources = self.resources.borrow_mut();
|
||||||
|
let resource = resources.entry_mut(self.id);
|
||||||
|
if resource.buffer.text() == text {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resource.buffer.set_text(text);
|
||||||
|
resource.rendered = None;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit_text<R>(&mut self, edit: impl FnOnce(&mut String) -> R) -> R {
|
||||||
|
let mut resources = self.resources.borrow_mut();
|
||||||
|
let resource = resources.entry_mut(self.id);
|
||||||
|
resource.rendered = None;
|
||||||
|
edit(resource.buffer.edit())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
|
||||||
|
let mut resources = self.resources.borrow_mut();
|
||||||
|
let resource = resources.entry_mut(self.id);
|
||||||
|
resource.buffer.set_spans(spans);
|
||||||
|
resource.rendered = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R {
|
||||||
|
let mut resources = self.resources.borrow_mut();
|
||||||
|
let resource = resources.entry_mut(self.id);
|
||||||
|
resource.invalidate();
|
||||||
|
update(&mut resource.attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rendered(&self) -> Option<RenderedText> {
|
||||||
|
self.resources.borrow().entry(self.id).rendered.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> Option<f32> {
|
||||||
|
self.resources.borrow().entry(self.id).width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_layout<R>(&self, f: impl FnOnce(&Layout<PaintId>, &str) -> R) -> R {
|
||||||
|
let mut resources = self.resources.borrow_mut();
|
||||||
|
resources.shape(self.id);
|
||||||
|
let resource = resources.entry(self.id);
|
||||||
|
f(resource.buffer.layout(), resource.buffer.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout(&self) -> Ref<'_, Layout<PaintId>> {
|
||||||
|
self.resources.borrow_mut().shape(self.id);
|
||||||
|
Ref::map(self.resources.borrow(), |resources| {
|
||||||
|
resources.entry(self.id).buffer.layout()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
width: Option<f32>,
|
||||||
|
owner: WidgetId,
|
||||||
|
textures: &mut Textures,
|
||||||
|
density: f32,
|
||||||
|
) -> (RenderedText, bool) {
|
||||||
|
self.resources
|
||||||
|
.borrow_mut()
|
||||||
|
.render(self.id, width, owner, textures, density)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TextHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.send.send(self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -619,13 +879,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registering_after_shaping_is_reported() {
|
fn dropped_handles_release_their_arena_entries() {
|
||||||
let mut data = TextData::default();
|
let resources = Rc::new(RefCell::new(TextResources::new()));
|
||||||
let mut buffer = TextBuffer::new("ordinary platform text");
|
let text = TextResources::add(
|
||||||
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
|
resources.clone(),
|
||||||
assert_eq!(
|
TextBuffer::new("temporary"),
|
||||||
data.register_font("icons", b"not a font" as &'static [u8]),
|
TextAttrs::default(),
|
||||||
Err(FontRegistrationError::TextAlreadyShaped)
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(resources.borrow().entries.len(), 1);
|
||||||
|
|
||||||
|
drop(text);
|
||||||
|
resources.borrow_mut().free_released();
|
||||||
|
|
||||||
|
assert!(resources.borrow().entries.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-9
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
Mask, MoveOffset, Paints, TextResources, Textures, WeakWidget, WidgetId, Widgets,
|
||||||
|
util::TrackedArena,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Ref, RefCell, RefMut},
|
cell::{Ref, RefCell, RefMut},
|
||||||
@@ -22,7 +23,7 @@ pub struct UiData {
|
|||||||
pub widgets: Widgets,
|
pub widgets: Widgets,
|
||||||
pub paints: Paints,
|
pub paints: Paints,
|
||||||
pub textures: Textures,
|
pub textures: Textures,
|
||||||
pub text: TextData,
|
pub text: Rc<RefCell<TextResources>>,
|
||||||
pub masks: TrackedArena<Mask, u32>,
|
pub masks: TrackedArena<Mask, u32>,
|
||||||
pub move_offsets: TrackedArena<MoveOffset, u32>,
|
pub move_offsets: TrackedArena<MoveOffset, u32>,
|
||||||
animating: Vec<WidgetId>,
|
animating: Vec<WidgetId>,
|
||||||
@@ -67,21 +68,32 @@ pub struct Ui {
|
|||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
/// Register application-owned font data for a semantic or named family.
|
/// Register application-owned font data for a semantic or named family.
|
||||||
///
|
/// Existing text resources are invalidated and their active widgets are
|
||||||
/// This must happen before the first text shape. Existing text layouts
|
/// scheduled for layout again.
|
||||||
/// cache their resolved faces, so accepting a later registration would
|
|
||||||
/// leave already-shaped widgets displaying the old result.
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn register_font(
|
pub fn register_font(
|
||||||
&mut self,
|
&mut self,
|
||||||
family: impl AsRef<str>,
|
family: impl AsRef<str>,
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
self.data.text.register_font(family, data)
|
let owners = {
|
||||||
|
let mut text = self.data.text.borrow_mut();
|
||||||
|
text.register_font(family, data)?;
|
||||||
|
text.invalidate_all()
|
||||||
|
};
|
||||||
|
let active: Vec<WidgetId> = {
|
||||||
|
let render = self.render_state.get();
|
||||||
|
owners
|
||||||
|
.into_iter()
|
||||||
|
.filter(|owner| render.active.contains_key(owner))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
self.data.widgets.needs_redraw.extend(active);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
pub fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
||||||
self.data.text.is_font_registered(family)
|
self.data.text.borrow().is_font_registered(family)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A read-only handle to the retained result of the last completed frame.
|
/// A read-only handle to the retained result of the last completed frame.
|
||||||
@@ -96,7 +108,7 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_density(&mut self, density: f32) {
|
pub fn set_density(&mut self, density: f32) {
|
||||||
self.data.text.density = density;
|
self.data.text.borrow_mut().density = density;
|
||||||
self.render_state.get_mut().set_density(density);
|
self.render_state.get_mut().set_density(density);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,5 +184,6 @@ pub trait UiRsc {
|
|||||||
}
|
}
|
||||||
self.ui_mut().textures.free();
|
self.ui_mut().textures.free();
|
||||||
self.ui_mut().paints.free_released();
|
self.ui_mut().paints.free_released();
|
||||||
|
self.ui_mut().text.borrow_mut().free_released();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-15
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextHandle,
|
||||||
TextBuffer, TextData, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||||
WidgetId,
|
WidgetId,
|
||||||
render::{
|
render::{
|
||||||
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
|
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
|
||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
ui::render_state::Retained,
|
ui::render_state::Retained,
|
||||||
util::Vec2,
|
util::Vec2,
|
||||||
};
|
};
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
pub struct Painter<'a> {
|
pub struct Painter<'a> {
|
||||||
pub(super) render_state: &'a mut UiRenderState,
|
pub(super) render_state: &'a mut UiRenderState,
|
||||||
@@ -413,21 +414,16 @@ impl<'a> Painter<'a> {
|
|||||||
self.own(h);
|
self.own(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_text(
|
pub fn render_text(&mut self, text: &TextHandle, width: Option<f32>) -> RenderedText {
|
||||||
&mut self,
|
|
||||||
buffer: &mut TextBuffer,
|
|
||||||
attrs: &TextAttrs,
|
|
||||||
width: Option<f32>,
|
|
||||||
) -> RenderedText {
|
|
||||||
let density = self.render_state.density;
|
let density = self.render_state.density;
|
||||||
self.render_state.shape_count += 1;
|
|
||||||
let ui: &mut UiData = self.rsc.ui_mut();
|
let ui: &mut UiData = self.rsc.ui_mut();
|
||||||
ui.text
|
let (rendered, prepared) = text.render(width, self.id, &mut ui.textures, density);
|
||||||
.render(buffer, attrs, width, &mut ui.textures, density)
|
self.render_state.shape_count += u64::from(prepared);
|
||||||
|
rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn atlas_generation(&mut self) -> u64 {
|
fn atlas_generation(&self) -> u64 {
|
||||||
self.rsc.ui_mut().text.atlas.generation()
|
self.rsc.ui().text.borrow().atlas.generation()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||||
@@ -493,8 +489,8 @@ impl<'a> Painter<'a> {
|
|||||||
self.region.size().to_abs(self.render_state.output_size)
|
self.region.size().to_abs(self.render_state.output_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
pub fn text_resources(&mut self) -> Rc<RefCell<TextResources>> {
|
||||||
&mut self.rsc.ui_mut().text
|
self.rsc.ui().text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn child_layer(&mut self) {
|
pub fn child_layer(&mut self) {
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ impl<T> SlotVec<T> {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.len() == 0
|
self.len() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
||||||
|
self.data.iter_mut().filter_map(|(_, value)| value.as_mut())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Default for SlotVec<T> {
|
impl<T> Default for SlotVec<T> {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ pub(crate) fn update_info<Rsc: UiRsc>(rsc: &mut Rsc, info: WeakWidget<Text>, vie
|
|||||||
rsc.widgets().len(),
|
rsc.widgets().len(),
|
||||||
render_state.get().active_widgets(),
|
render_state.get().active_widgets(),
|
||||||
);
|
);
|
||||||
if new != *rsc.widgets()[info].content {
|
if new != rsc.widgets()[info].content() {
|
||||||
*rsc.widgets_mut()[info].content = new;
|
rsc.widgets_mut()[info].set_text(new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ where
|
|||||||
.attr::<Selectable>(())
|
.attr::<Selectable>(())
|
||||||
.on(Submit, move |ctx, rsc: &mut Rsc| {
|
.on(Submit, move |ctx, rsc: &mut Rsc| {
|
||||||
let w = ctx.widget;
|
let w = ctx.widget;
|
||||||
let content = w.edit(rsc).take();
|
let content = w(rsc).take();
|
||||||
let text = wtext(content)
|
let text = wtext(content)
|
||||||
.editable(EditMode::MultiLine)
|
.editable(EditMode::MultiLine)
|
||||||
.size(30)
|
.size(30)
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ the retained widget tree dirty:
|
|||||||
```rust
|
```rust
|
||||||
rsc.spawn_task(async move |mut ctx| {
|
rsc.spawn_task(async move |mut ctx| {
|
||||||
let text = load_text().await;
|
let text = load_text().await;
|
||||||
ctx.update(move |_, rsc| label.edit(rsc).set(&text));
|
ctx.update(move |_, rsc| label(rsc).set(&text));
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+30
-29
@@ -43,11 +43,11 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let content = text.text();
|
let content = text.text();
|
||||||
let sel_start = byte_to_utf16(content, sel.start) as i32;
|
let sel_start = byte_to_utf16(&content, sel.start) as i32;
|
||||||
let sel_end = byte_to_utf16(content, sel.end) as i32;
|
let sel_end = byte_to_utf16(&content, sel.end) as i32;
|
||||||
let compose_len = self.state.android_state().compose_len;
|
let compose_len = self.state.android_state().compose_len;
|
||||||
let (comp_start, comp_end) = if compose_len > 0 {
|
let (comp_start, comp_end) = if compose_len > 0 {
|
||||||
let caret = byte_to_utf16(content, text.caret().unwrap_or(sel.end)) as i32;
|
let caret = byte_to_utf16(&content, text.caret().unwrap_or(sel.end)) as i32;
|
||||||
(caret - compose_len as i32, caret)
|
(caret - compose_len as i32, caret)
|
||||||
} else {
|
} else {
|
||||||
(-1, -1)
|
(-1, -1)
|
||||||
@@ -89,16 +89,12 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
if let Some(focus) = self.focus() {
|
if let Some(focus) = self.focus() {
|
||||||
let text = focus.get(&self.rsc);
|
let text = focus.get(&self.rsc);
|
||||||
let sel = text.selection_range().unwrap_or(0..0);
|
let sel = text.selection_range().unwrap_or(0..0);
|
||||||
let start = byte_to_utf16(text.text(), sel.start) as i32;
|
let content = text.text();
|
||||||
let end = byte_to_utf16(text.text(), sel.end) as i32;
|
let start = byte_to_utf16(&content, sel.start) as i32;
|
||||||
|
let end = byte_to_utf16(&content, sel.end) as i32;
|
||||||
out_attrs.set_initial_sel_start(&mut ctx.env, start);
|
out_attrs.set_initial_sel_start(&mut ctx.env, start);
|
||||||
out_attrs.set_initial_sel_end(&mut ctx.env, end);
|
out_attrs.set_initial_sel_end(&mut ctx.env, end);
|
||||||
let caps = caps_mode(
|
let caps = caps_mode(&mut ctx.env, &content, start as usize, CAP_MODE_SENTENCES);
|
||||||
&mut ctx.env,
|
|
||||||
text.text(),
|
|
||||||
start as usize,
|
|
||||||
CAP_MODE_SENTENCES,
|
|
||||||
);
|
|
||||||
out_attrs.set_initial_caps_mode(&mut ctx.env, caps);
|
out_attrs.set_initial_caps_mode(&mut ctx.env, caps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,10 +110,11 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
let focus = self.focus()?;
|
let focus = self.focus()?;
|
||||||
let text = focus.get(&self.rsc);
|
let text = focus.get(&self.rsc);
|
||||||
let sel = text.selection_range()?;
|
let sel = text.selection_range()?;
|
||||||
let end_16 = byte_to_utf16(text.text(), sel.start);
|
let content = text.text();
|
||||||
|
let end_16 = byte_to_utf16(&content, sel.start);
|
||||||
let start_16 = end_16.saturating_sub(n as usize);
|
let start_16 = end_16.saturating_sub(n as usize);
|
||||||
let start = utf16_to_byte(text.text(), start_16);
|
let start = utf16_to_byte(&content, start_16);
|
||||||
Some(Cow::Borrowed(&text.text()[start..sel.start]))
|
Some(Cow::Owned(content[start..sel.start].to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_after_cursor<'slf>(
|
fn text_after_cursor<'slf>(
|
||||||
@@ -131,11 +128,12 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
let focus = self.focus()?;
|
let focus = self.focus()?;
|
||||||
let text = focus.get(&self.rsc);
|
let text = focus.get(&self.rsc);
|
||||||
let sel = text.selection_range()?;
|
let sel = text.selection_range()?;
|
||||||
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
let content = text.text();
|
||||||
let start_16 = byte_to_utf16(text.text(), sel.end);
|
let len_16 = byte_to_utf16(&content, content.len());
|
||||||
|
let start_16 = byte_to_utf16(&content, sel.end);
|
||||||
let end_16 = (start_16 + n as usize).min(len_16);
|
let end_16 = (start_16 + n as usize).min(len_16);
|
||||||
let end = utf16_to_byte(text.text(), end_16);
|
let end = utf16_to_byte(&content, end_16);
|
||||||
Some(Cow::Borrowed(&text.text()[sel.end..end]))
|
Some(Cow::Owned(content[sel.end..end].to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
|
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
|
||||||
@@ -151,8 +149,9 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
let Some(caret) = text.caret() else {
|
let Some(caret) = text.caret() else {
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
let off = byte_to_utf16(text.text(), caret);
|
let content = text.text();
|
||||||
caps_mode(&mut ctx.env, text.text(), off, req_modes)
|
let off = byte_to_utf16(&content, caret);
|
||||||
|
caps_mode(&mut ctx.env, &content, off, req_modes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_surrounding_text(
|
fn delete_surrounding_text(
|
||||||
@@ -170,12 +169,13 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
};
|
};
|
||||||
let content = text.text();
|
let content = text.text();
|
||||||
let start_16 =
|
let start_16 =
|
||||||
byte_to_utf16(content, sel.start).saturating_sub(before_length.max(0) as usize);
|
byte_to_utf16(&content, sel.start).saturating_sub(before_length.max(0) as usize);
|
||||||
let len_16 = byte_to_utf16(content, content.len());
|
let len_16 = byte_to_utf16(&content, content.len());
|
||||||
let end_16 = (byte_to_utf16(content, sel.end) + after_length.max(0) as usize).min(len_16);
|
let end_16 = (byte_to_utf16(&content, sel.end) + after_length.max(0) as usize).min(len_16);
|
||||||
let start = utf16_to_byte(content, start_16);
|
let start = utf16_to_byte(&content, start_16);
|
||||||
let end = utf16_to_byte(content, end_16);
|
let end = utf16_to_byte(&content, end_16);
|
||||||
focus.edit(&mut self.rsc).delete_byte_range(start, end);
|
drop(content);
|
||||||
|
focus(&mut self.rsc).delete_byte_range(start, end);
|
||||||
self.after_input(ctx);
|
self.after_input(ctx);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let compose_len = self.state.android_state().compose_len;
|
let compose_len = self.state.android_state().compose_len;
|
||||||
focus.edit(&mut self.rsc).replace(compose_len, text);
|
focus(&mut self.rsc).replace(compose_len, text);
|
||||||
self.state.android_state_mut().compose_len = text.chars().count();
|
self.state.android_state_mut().compose_len = text.chars().count();
|
||||||
self.after_input(ctx);
|
self.after_input(ctx);
|
||||||
true
|
true
|
||||||
@@ -225,8 +225,9 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
|||||||
};
|
};
|
||||||
let text = focus.get(&self.rsc);
|
let text = focus.get(&self.rsc);
|
||||||
let content = text.text();
|
let content = text.text();
|
||||||
let byte = utf16_to_byte(content, end.max(0) as usize);
|
let byte = utf16_to_byte(&content, end.max(0) as usize);
|
||||||
focus.edit(&mut self.rsc).set_cursor_byte(byte);
|
drop(content);
|
||||||
|
focus(&mut self.rsc).set_cursor_byte(byte);
|
||||||
let _ = start;
|
let _ = start;
|
||||||
self.after_input(ctx);
|
self.after_input(ctx);
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub(super) fn on_key<'local, State: AndroidAppState>(
|
|||||||
let Some(focus) = state.android_state().focus else {
|
let Some(focus) = state.android_state().focus else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let mut text = focus.edit(rsc);
|
let text = focus(rsc);
|
||||||
match key_code {
|
match key_code {
|
||||||
Keycode::Del => text.backspace(false),
|
Keycode::Del => text.backspace(false),
|
||||||
Keycode::ForwardDel => text.delete(false),
|
Keycode::ForwardDel => text.delete(false),
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
|||||||
if old_focus != ui_state.focus
|
if old_focus != ui_state.focus
|
||||||
&& let Some(old) = old_focus
|
&& let Some(old) = old_focus
|
||||||
{
|
{
|
||||||
old.edit(&mut self.rsc).deselect();
|
old(&mut self.rsc).deselect();
|
||||||
}
|
}
|
||||||
if std::mem::take(&mut ui_state.pending_show_keyboard) {
|
if std::mem::take(&mut ui_state.pending_show_keyboard) {
|
||||||
show_soft_input(&mut ctx.env, &ctx.view);
|
show_soft_input(&mut ctx.env, &ctx.view);
|
||||||
@@ -619,7 +619,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
if !gain_focus {
|
if !gain_focus {
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
if let Some(focus) = ui_state.focus.take() {
|
if let Some(focus) = ui_state.focus.take() {
|
||||||
focus.edit(&mut self.rsc).deselect();
|
focus(&mut self.rsc).deselect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.after_input(ctx);
|
self.after_input(ctx);
|
||||||
@@ -646,8 +646,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
log::info!(
|
log::info!(
|
||||||
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
||||||
glyphs_cached={} atlas_pages={}",
|
glyphs_cached={} atlas_pages={}",
|
||||||
self.rsc.ui().text.atlas.glyph_count(),
|
self.rsc.ui().text.borrow().atlas.glyph_count(),
|
||||||
self.rsc.ui().text.atlas.page_count(),
|
self.rsc.ui().text.borrow().atlas.page_count(),
|
||||||
);
|
);
|
||||||
if already_live {
|
if already_live {
|
||||||
let ui_state = self.state.android_state_mut();
|
let ui_state = self.state.android_state_mut();
|
||||||
@@ -691,8 +691,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
"iris surface: new renderer built ({:?}), re-uploading textures: \
|
"iris surface: new renderer built ({:?}), re-uploading textures: \
|
||||||
glyphs={} pages={}",
|
glyphs={} pages={}",
|
||||||
renderer.adapter_backend,
|
renderer.adapter_backend,
|
||||||
self.rsc.ui().text.atlas.glyph_count(),
|
self.rsc.ui().text.borrow().atlas.glyph_count(),
|
||||||
self.rsc.ui().text.atlas.page_count(),
|
self.rsc.ui().text.borrow().atlas.page_count(),
|
||||||
);
|
);
|
||||||
self.rsc.ui_mut().textures.reupload();
|
self.rsc.ui_mut().textures.reupload();
|
||||||
self.rsc.ui_mut().paints.reupload();
|
self.rsc.ui_mut().paints.reupload();
|
||||||
@@ -716,8 +716,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|||||||
log::info!(
|
log::info!(
|
||||||
"iris surface: surface_destroyed, tearing the renderer down \
|
"iris surface: surface_destroyed, tearing the renderer down \
|
||||||
(glyphs_cached={} atlas_pages={})",
|
(glyphs_cached={} atlas_pages={})",
|
||||||
self.rsc.ui().text.atlas.glyph_count(),
|
self.rsc.ui().text.borrow().atlas.glyph_count(),
|
||||||
self.rsc.ui().text.atlas.page_count(),
|
self.rsc.ui().text.borrow().atlas.page_count(),
|
||||||
);
|
);
|
||||||
self.state.android_state_mut().renderer = None;
|
self.state.android_state_mut().renderer = None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
|||||||
if old != ui_state.focus
|
if old != ui_state.focus
|
||||||
&& let Some(old) = old
|
&& let Some(old) = old
|
||||||
{
|
{
|
||||||
old.edit(rsc).deselect();
|
old(rsc).deselect();
|
||||||
}
|
}
|
||||||
match &event {
|
match &event {
|
||||||
WindowEvent::CloseRequested => event_loop.exit(),
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
@@ -315,7 +315,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
|||||||
&& let Some(sel) = ui_state.focus
|
&& let Some(sel) = ui_state.focus
|
||||||
&& event.state.is_pressed()
|
&& event.state.is_pressed()
|
||||||
{
|
{
|
||||||
let mut text = sel.edit(rsc);
|
let text = sel(rsc);
|
||||||
match text.apply_event(event, &ui_state.input.modifiers) {
|
match text.apply_event(event, &ui_state.input.modifiers) {
|
||||||
TextInputResult::Unfocus => {
|
TextInputResult::Unfocus => {
|
||||||
ui_state.focus = None;
|
ui_state.focus = None;
|
||||||
@@ -346,7 +346,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
|||||||
if !rsc.events.controllers.command_target_blocks_input()
|
if !rsc.events.controllers.command_target_blocks_input()
|
||||||
&& let Some(sel) = ui_state.focus
|
&& let Some(sel) = ui_state.focus
|
||||||
{
|
{
|
||||||
let mut text = sel.edit(rsc);
|
let text = sel(rsc);
|
||||||
match ime {
|
match ime {
|
||||||
Ime::Enabled | Ime::Disabled => (),
|
Ime::Enabled | Ime::Disabled => (),
|
||||||
Ime::Preedit(content, _pos) => {
|
Ime::Preedit(content, _pos) => {
|
||||||
|
|||||||
@@ -467,10 +467,8 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
|||||||
|
|
||||||
render.resize((1080.0, 2298.0));
|
render.resize((1080.0, 2298.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
field
|
field(&mut rsc).select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
|
||||||
.edit(&mut rsc)
|
field(&mut rsc).insert("a");
|
||||||
.select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
|
|
||||||
field.edit(&mut rsc).insert("a");
|
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
let before_px = render.window_region(&field, &rsc).unwrap();
|
let before_px = render.window_region(&field, &rsc).unwrap();
|
||||||
@@ -485,7 +483,7 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
|||||||
|
|
||||||
render.resize((1080.0, 1478.0));
|
render.resize((1080.0, 1478.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
field.edit(&mut rsc).insert("b");
|
field(&mut rsc).insert("b");
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
let after_px = render.window_region(&field, &rsc).unwrap();
|
let after_px = render.window_region(&field, &rsc).unwrap();
|
||||||
|
|||||||
+14
-14
@@ -127,29 +127,29 @@ fn on_press(
|
|||||||
match sense {
|
match sense {
|
||||||
CursorSense::PressStart(_) => {
|
CursorSense::PressStart(_) => {
|
||||||
let recent = state.recent_click();
|
let recent = state.recent_click();
|
||||||
id.edit(rsc).text.press_origin = Some(pos);
|
id(rsc).press_origin = Some(pos);
|
||||||
id.edit(rsc).select(pos, size, false, recent);
|
id(rsc).select(pos, size, false, recent);
|
||||||
}
|
}
|
||||||
CursorSense::Pressing(_) | CursorSense::PressEnd(_) => {
|
CursorSense::Pressing(_) | CursorSense::PressEnd(_) => {
|
||||||
let mut ctx = id.edit(rsc);
|
let ctx = id(rsc);
|
||||||
let Some(origin) = ctx.text.press_origin else {
|
let Some(origin) = ctx.press_origin else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let (dx, dy) = (pos.x - origin.x, pos.y - origin.y);
|
let (dx, dy) = (pos.x - origin.x, pos.y - origin.y);
|
||||||
if dy.abs() > DRAG_SLOP && dy.abs() >= dx.abs() {
|
if dy.abs() > DRAG_SLOP && dy.abs() >= dx.abs() {
|
||||||
ctx.text.press_origin = None;
|
ctx.press_origin = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ended = matches!(sense, CursorSense::PressEnd(_));
|
let ended = matches!(sense, CursorSense::PressEnd(_));
|
||||||
if ended {
|
if ended {
|
||||||
ctx.text.press_origin = None;
|
ctx.press_origin = None;
|
||||||
}
|
}
|
||||||
ctx.select(pos, size, true, false);
|
ctx.select(pos, size, true, false);
|
||||||
if ended && dx.abs() <= DRAG_SLOP && dy.abs() <= DRAG_SLOP {
|
if ended && dx.abs() <= DRAG_SLOP && dy.abs() <= DRAG_SLOP {
|
||||||
state.focus_gained(render.window_region(&id, &*rsc));
|
state.focus_gained(render.window_region(&id, &*rsc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CursorSense::Cancel => id.edit(rsc).text.press_origin = None,
|
CursorSense::Cancel => id(rsc).press_origin = None,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -157,24 +157,24 @@ fn on_press(
|
|||||||
|
|
||||||
match sense {
|
match sense {
|
||||||
CursorSense::PressStart(_) => {
|
CursorSense::PressStart(_) => {
|
||||||
id.edit(rsc).text.press_origin = Some(pos);
|
id(rsc).press_origin = Some(pos);
|
||||||
}
|
}
|
||||||
CursorSense::Pressing(_) => {
|
CursorSense::Pressing(_) => {
|
||||||
let ctx = id.edit(rsc);
|
let ctx = id(rsc);
|
||||||
if let Some(origin) = ctx.text.press_origin
|
if let Some(origin) = ctx.press_origin
|
||||||
&& ((pos.x - origin.x).abs() > DRAG_SLOP || (pos.y - origin.y).abs() > DRAG_SLOP)
|
&& ((pos.x - origin.x).abs() > DRAG_SLOP || (pos.y - origin.y).abs() > DRAG_SLOP)
|
||||||
{
|
{
|
||||||
ctx.text.press_origin = None;
|
ctx.press_origin = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The gesture was taken by somebody else, so it is not a tap and
|
// The gesture was taken by somebody else, so it is not a tap and
|
||||||
// must not grant focus when it ends out of this widget's sight.
|
// must not grant focus when it ends out of this widget's sight.
|
||||||
CursorSense::Cancel => id.edit(rsc).text.press_origin = None,
|
CursorSense::Cancel => id(rsc).press_origin = None,
|
||||||
CursorSense::PressEnd(_) => {
|
CursorSense::PressEnd(_) => {
|
||||||
let was_tap = id.edit(rsc).text.press_origin.take().is_some();
|
let was_tap = id(rsc).press_origin.take().is_some();
|
||||||
if was_tap {
|
if was_tap {
|
||||||
let recent = state.recent_click();
|
let recent = state.recent_click();
|
||||||
id.edit(rsc).select(pos, size, false, recent);
|
id(rsc).select(pos, size, false, recent);
|
||||||
state.set_focus(Some(id));
|
state.set_focus(Some(id));
|
||||||
state.focus_gained(render.window_region(&id, &*rsc));
|
state.focus_gained(render.window_region(&id, &*rsc));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,12 +91,10 @@ impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
|
|||||||
let mut buf = TextBuffer::new(&builder.content);
|
let mut buf = TextBuffer::new(&builder.content);
|
||||||
buf.set_spans(builder.spans);
|
buf.set_spans(builder.spans);
|
||||||
let hint = builder.hint.get(state);
|
let hint = builder.hint.get(state);
|
||||||
let mut text = Text {
|
let resources = state.ui().text.clone();
|
||||||
content: builder.content.into(),
|
Text {
|
||||||
view: TextView::new(buf, builder.attrs, hint),
|
view: TextView::new(TextResources::add(resources, buf, builder.attrs), hint),
|
||||||
};
|
}
|
||||||
text.content.changed = false;
|
|
||||||
text
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +111,12 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
|
|||||||
) -> Self::Output {
|
) -> Self::Output {
|
||||||
let mut buf = TextBuffer::new(&builder.content);
|
let mut buf = TextBuffer::new(&builder.content);
|
||||||
buf.set_spans(builder.spans);
|
buf.set_spans(builder.spans);
|
||||||
|
let resources = state.ui().text.clone();
|
||||||
TextEdit::new(
|
TextEdit::new(
|
||||||
TextView::new(buf, builder.attrs, builder.hint.get(state)),
|
TextView::new(
|
||||||
|
TextResources::add(resources, buf, builder.attrs),
|
||||||
|
builder.hint.get(state),
|
||||||
|
),
|
||||||
builder.output.mode,
|
builder.output.mode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-148
@@ -1,9 +1,10 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use iris_core::{PaintId, TextData};
|
use iris_core::PaintId;
|
||||||
use parley::{Affinity, Layout, Selection};
|
use parley::{Affinity, Layout, Selection};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::{
|
||||||
|
cell::Ref,
|
||||||
use super::selection_layout;
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use winit::{
|
use winit::{
|
||||||
event::KeyEvent,
|
event::KeyEvent,
|
||||||
@@ -47,15 +48,15 @@ impl TextEdit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_text(&self) -> Option<String> {
|
pub fn selected_text(&self) -> Option<String> {
|
||||||
self.view.selection.selected_text(self.view.buf.text())
|
self.view.selection.selected_text(&self.view.text.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The field's content. Byte-indexed, like everything else here since
|
/// The field's content. Byte-indexed, like everything else here since
|
||||||
/// I1 moved to parley -- an IME bridge (`android/ime.rs`) converts to
|
/// I1 moved to parley -- an IME bridge (`android/ime.rs`) converts to
|
||||||
/// and from UTF-16 code units at its own edge rather than this type
|
/// and from UTF-16 code units at its own edge rather than this type
|
||||||
/// knowing about that encoding.
|
/// knowing about that encoding.
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> Ref<'_, str> {
|
||||||
self.view.buf.text()
|
self.view.text.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The selection as a byte range, collapsed to `caret..caret` when
|
/// The selection as a byte range, collapsed to `caret..caret` when
|
||||||
@@ -89,21 +90,15 @@ impl Widget for TextEdit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextEditCtx<'a> {
|
impl TextEdit {
|
||||||
pub text: &'a mut TextEdit,
|
|
||||||
pub data: &'a mut TextData,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TextEditCtx<'a> {
|
|
||||||
fn selection_ctx(&mut self) -> TextSelectionCtx<'_> {
|
fn selection_ctx(&mut self) -> TextSelectionCtx<'_> {
|
||||||
TextSelectionCtx {
|
TextSelectionCtx {
|
||||||
view: &mut self.text.view,
|
view: &mut self.view,
|
||||||
data: self.data,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(&mut self) -> &Layout<iris_core::PaintId> {
|
fn layout(&self) -> Ref<'_, Layout<iris_core::PaintId>> {
|
||||||
selection_layout(&mut self.text.view, self.data)
|
self.view.text.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||||
@@ -112,44 +107,44 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn take(&mut self) -> String {
|
pub fn take(&mut self) -> String {
|
||||||
let text = self.text.view.buf.text().to_string();
|
let text = self.view.text.text().to_string();
|
||||||
self.set("");
|
self.set("");
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, text: &str) {
|
pub fn set(&mut self, text: &str) {
|
||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.view.text.set_text(text);
|
||||||
self.text.view.buf.changed = true;
|
self.view.selection.deselect();
|
||||||
self.text.view.selection.deselect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
|
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
|
||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.view.text.set_text(text);
|
||||||
self.text.view.buf.set_spans(spans);
|
self.view.text.set_spans(spans);
|
||||||
self.text.view.selection.deselect();
|
self.view.selection.deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn motion(&mut self, motion: Motion, select: bool) {
|
pub fn motion(&mut self, motion: Motion, select: bool) {
|
||||||
let Some(sel) = self.text.view.selection.range else {
|
let Some(sel) = self.view.selection.range else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let layout = self.layout();
|
let layout = self.layout();
|
||||||
let sel = if !select && !sel.is_collapsed() {
|
let sel = if !select && !sel.is_collapsed() {
|
||||||
match motion {
|
match motion {
|
||||||
Motion::Left | Motion::LeftWord => {
|
Motion::Left | Motion::LeftWord => {
|
||||||
Selection::from(sel.text_range().start_cursor(layout))
|
Selection::from(sel.text_range().start_cursor(&layout))
|
||||||
}
|
}
|
||||||
Motion::Right | Motion::RightWord => {
|
Motion::Right | Motion::RightWord => {
|
||||||
Selection::from(sel.text_range().end_cursor(layout))
|
Selection::from(sel.text_range().end_cursor(&layout))
|
||||||
}
|
}
|
||||||
_ => apply_motion(sel, layout, motion, false),
|
_ => apply_motion(sel, &layout, motion, false),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
apply_motion(sel, layout, motion, select)
|
apply_motion(sel, &layout, motion, select)
|
||||||
};
|
};
|
||||||
self.text.view.selection.range = Some(sel);
|
drop(layout);
|
||||||
|
self.view.selection.range = Some(sel);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace(&mut self, len: usize, text: &str) {
|
pub fn replace(&mut self, len: usize, text: &str) {
|
||||||
@@ -161,7 +156,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn string(&self, text: &str) -> String {
|
fn string(&self, text: &str) -> String {
|
||||||
if self.text.mode == EditMode::SingleLine {
|
if self.mode == EditMode::SingleLine {
|
||||||
text.replace('\n', "")
|
text.replace('\n', "")
|
||||||
} else {
|
} else {
|
||||||
text.to_string()
|
text.to_string()
|
||||||
@@ -178,7 +173,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.clear_span();
|
self.clear_span();
|
||||||
let at = match self.text.view.selection.range {
|
let at = match self.view.selection.range {
|
||||||
Some(sel) => sel.focus().index(),
|
Some(sel) => sel.focus().index(),
|
||||||
// No caret means nowhere to put the text, so this drops the
|
// No caret means nowhere to put the text, so this drops the
|
||||||
// keystroke -- which is invisible, and was the whole of the
|
// keystroke -- which is invisible, and was the whole of the
|
||||||
@@ -196,22 +191,24 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let at = at.min(self.text.view.buf.text().len());
|
let at = at.min(self.view.text.text().len());
|
||||||
self.text.view.buf.edit().insert_str(at, text);
|
self.view
|
||||||
self.text.view.buf.changed = true;
|
.text
|
||||||
|
.edit_text(|content| content.insert_str(at, text));
|
||||||
self.set_caret(at + text.len());
|
self.set_caret(at + text.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_span(&mut self) -> bool {
|
pub fn clear_span(&mut self) -> bool {
|
||||||
let Some(sel) = self.text.view.selection.range else {
|
let Some(sel) = self.view.selection.range else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if sel.is_collapsed() {
|
if sel.is_collapsed() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let range = sel.text_range();
|
let range = sel.text_range();
|
||||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
self.view
|
||||||
self.text.view.buf.changed = true;
|
.text
|
||||||
|
.edit_text(|content| content.replace_range(range.clone(), ""));
|
||||||
self.set_caret(range.start);
|
self.set_caret(range.start);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -221,7 +218,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn newline(&mut self) {
|
pub fn newline(&mut self) {
|
||||||
if self.text.mode == EditMode::MultiLine {
|
if self.mode == EditMode::MultiLine {
|
||||||
self.insert_str("\n");
|
self.insert_str("\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +227,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
if self.clear_span() {
|
if self.clear_span() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(sel) = self.text.view.selection.range else {
|
let Some(sel) = self.view.selection.range else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let end = sel.focus().index();
|
let end = sel.focus().index();
|
||||||
@@ -239,10 +236,11 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
let layout = self.layout();
|
let layout = self.layout();
|
||||||
let start = if word {
|
let start = if word {
|
||||||
sel.focus().previous_logical_word(layout).index()
|
sel.focus().previous_logical_word(&layout).index()
|
||||||
} else {
|
} else {
|
||||||
sel.focus().previous_visual(layout).index()
|
sel.focus().previous_visual(&layout).index()
|
||||||
};
|
};
|
||||||
|
drop(layout);
|
||||||
self.delete_range(start, end);
|
self.delete_range(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,30 +248,32 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
if self.clear_span() {
|
if self.clear_span() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(sel) = self.text.view.selection.range else {
|
let Some(sel) = self.view.selection.range else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let start = sel.focus().index();
|
let start = sel.focus().index();
|
||||||
if start >= self.text.view.buf.text().len() {
|
if start >= self.view.text.text().len() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let layout = self.layout();
|
let layout = self.layout();
|
||||||
let end = if word {
|
let end = if word {
|
||||||
sel.focus().next_logical_word(layout).index()
|
sel.focus().next_logical_word(&layout).index()
|
||||||
} else {
|
} else {
|
||||||
sel.focus().next_visual(layout).index()
|
sel.focus().next_visual(&layout).index()
|
||||||
};
|
};
|
||||||
|
drop(layout);
|
||||||
self.delete_range(start, end);
|
self.delete_range(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_range(&mut self, start: usize, end: usize) {
|
fn delete_range(&mut self, start: usize, end: usize) {
|
||||||
let len = self.text.view.buf.text().len();
|
let len = self.view.text.text().len();
|
||||||
let (start, end) = (start.min(end).min(len), start.max(end).min(len));
|
let (start, end) = (start.min(end).min(len), start.max(end).min(len));
|
||||||
if start == end {
|
if start == end {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.text.view.buf.edit().replace_range(start..end, "");
|
self.view
|
||||||
self.text.view.buf.changed = true;
|
.text
|
||||||
|
.edit_text(|content| content.replace_range(start..end, ""));
|
||||||
self.set_caret(start);
|
self.set_caret(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,20 +326,17 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||||
let old = (
|
let old = (self.view.text.text().to_string(), self.view.selection.range);
|
||||||
self.text.view.buf.text().to_string(),
|
|
||||||
self.text.view.selection.range,
|
|
||||||
);
|
|
||||||
let mut undo = false;
|
let mut undo = false;
|
||||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||||
if undo {
|
if undo {
|
||||||
if let Some((old, selection)) = self.text.history.pop() {
|
if let Some((old, selection)) = self.history.pop() {
|
||||||
self.set(&old);
|
self.set(&old);
|
||||||
self.text.view.selection.range = selection;
|
self.view.selection.range = selection;
|
||||||
self.refresh();
|
self.refresh();
|
||||||
}
|
}
|
||||||
} else if self.text.view.buf.text() != old.0 {
|
} else if self.view.text.text().as_ref() != old.0 {
|
||||||
self.text.history.push(old);
|
self.history.push(old);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
@@ -394,12 +391,12 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
match text.as_str() {
|
match text.as_str() {
|
||||||
"v" => return TextInputResult::Paste,
|
"v" => return TextInputResult::Paste,
|
||||||
"c" => {
|
"c" => {
|
||||||
if let Some(content) = self.text.selected_text() {
|
if let Some(content) = self.selected_text() {
|
||||||
return TextInputResult::Copy(content);
|
return TextInputResult::Copy(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"x" => {
|
"x" => {
|
||||||
if let Some(content) = self.text.selected_text() {
|
if let Some(content) = self.selected_text() {
|
||||||
self.clear_span();
|
self.clear_span();
|
||||||
return TextInputResult::Copy(content);
|
return TextInputResult::Copy(content);
|
||||||
}
|
}
|
||||||
@@ -492,226 +489,213 @@ impl DerefMut for TextEdit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait TextEditable {
|
|
||||||
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
|
|
||||||
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a> {
|
|
||||||
let ui: &mut UiData = ui.ui_mut();
|
|
||||||
TextEditCtx {
|
|
||||||
text: ui.widgets.get_mut(self).unwrap(),
|
|
||||||
data: &mut ui.text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use iris_core::{TextAttrs, TextBuffer};
|
use iris_core::{TextAttrs, TextBuffer, TextResources};
|
||||||
|
|
||||||
fn edit(text: &str, mode: EditMode) -> (TextEdit, TextData) {
|
fn edit(text: &str, mode: EditMode) -> TextEdit {
|
||||||
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
|
let resources = std::rc::Rc::new(std::cell::RefCell::new(TextResources::default()));
|
||||||
(TextEdit::new(view, mode), TextData::default())
|
let handle = TextResources::add(resources, TextBuffer::new(text), TextAttrs::default());
|
||||||
|
TextEdit::new(TextView::new(handle, None), mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ctx<'a>(text: &'a mut TextEdit, data: &'a mut TextData) -> TextEditCtx<'a> {
|
fn ctx(text: &mut TextEdit) -> &mut TextEdit {
|
||||||
TextEditCtx { text, data }
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(text: &TextEdit) -> String {
|
fn content(text: &TextEdit) -> String {
|
||||||
text.buf.text().to_string()
|
text.text().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_at_the_caret() {
|
fn insert_at_the_caret() {
|
||||||
let (mut t, mut d) = edit("ac", EditMode::SingleLine);
|
let mut t = edit("ac", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(1);
|
ctx(&mut t).set_caret(1);
|
||||||
ctx(&mut t, &mut d).insert("b");
|
ctx(&mut t).insert("b");
|
||||||
assert_eq!(content(&t), "abc");
|
assert_eq!(content(&t), "abc");
|
||||||
assert_eq!(t.caret(), Some(2));
|
assert_eq!(t.caret(), Some(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn backspace_removes_the_character_before_the_caret() {
|
fn backspace_removes_the_character_before_the_caret() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
let mut t = edit("abc", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(2);
|
ctx(&mut t).set_caret(2);
|
||||||
ctx(&mut t, &mut d).backspace(false);
|
ctx(&mut t).backspace(false);
|
||||||
assert_eq!(content(&t), "ac");
|
assert_eq!(content(&t), "ac");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn backspace_at_the_start_does_nothing() {
|
fn backspace_at_the_start_does_nothing() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
let mut t = edit("abc", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t).set_caret(0);
|
||||||
ctx(&mut t, &mut d).backspace(false);
|
ctx(&mut t).backspace(false);
|
||||||
assert_eq!(content(&t), "abc");
|
assert_eq!(content(&t), "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_removes_the_character_after_the_caret() {
|
fn delete_removes_the_character_after_the_caret() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
let mut t = edit("abc", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(1);
|
ctx(&mut t).set_caret(1);
|
||||||
ctx(&mut t, &mut d).delete(false);
|
ctx(&mut t).delete(false);
|
||||||
assert_eq!(content(&t), "ac");
|
assert_eq!(content(&t), "ac");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_at_the_end_does_nothing() {
|
fn delete_at_the_end_does_nothing() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
let mut t = edit("abc", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(3);
|
ctx(&mut t).set_caret(3);
|
||||||
ctx(&mut t, &mut d).delete(false);
|
ctx(&mut t).delete(false);
|
||||||
assert_eq!(content(&t), "abc");
|
assert_eq!(content(&t), "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_all_then_typing_replaces_everything() {
|
fn select_all_then_typing_replaces_everything() {
|
||||||
let (mut t, mut d) = edit("hello", EditMode::SingleLine);
|
let mut t = edit("hello", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t).select_all();
|
||||||
assert_eq!(t.selected_text().as_deref(), Some("hello"));
|
assert_eq!(t.selected_text().as_deref(), Some("hello"));
|
||||||
ctx(&mut t, &mut d).insert("x");
|
ctx(&mut t).insert("x");
|
||||||
assert_eq!(content(&t), "x");
|
assert_eq!(content(&t), "x");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clearing_a_span_leaves_the_caret_at_its_start() {
|
fn clearing_a_span_leaves_the_caret_at_its_start() {
|
||||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
let mut t = edit("abcdef", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t).select_all();
|
||||||
assert!(ctx(&mut t, &mut d).clear_span());
|
assert!(ctx(&mut t).clear_span());
|
||||||
assert_eq!(content(&t), "");
|
assert_eq!(content(&t), "");
|
||||||
assert_eq!(t.caret(), Some(0));
|
assert_eq!(t.caret(), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
|
fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
|
||||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
let mut t = edit("", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
|
ctx(&mut t).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
|
||||||
assert!(t.caret().is_some(), "a tap must leave a caret behind");
|
assert!(t.caret().is_some(), "a tap must leave a caret behind");
|
||||||
ctx(&mut t, &mut d).insert("hi");
|
ctx(&mut t).insert("hi");
|
||||||
assert_eq!(content(&t), "hi");
|
assert_eq!(content(&t), "hi");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
|
fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
let mut t = edit("abc", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
ctx(&mut t).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
||||||
assert_eq!(t.caret(), Some(3));
|
assert_eq!(t.caret(), Some(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dragging_without_a_previous_selection_selects_nothing() {
|
fn dragging_without_a_previous_selection_selects_nothing() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
let mut t = edit("abc", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
ctx(&mut t).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
||||||
assert!(t.selection_range().is_none());
|
assert!(t.selection_range().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_single_line_field_refuses_newlines() {
|
fn a_single_line_field_refuses_newlines() {
|
||||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
let mut t = edit("", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t).set_caret(0);
|
||||||
ctx(&mut t, &mut d).insert("a\nb");
|
ctx(&mut t).insert("a\nb");
|
||||||
assert_eq!(content(&t), "ab");
|
assert_eq!(content(&t), "ab");
|
||||||
ctx(&mut t, &mut d).newline();
|
ctx(&mut t).newline();
|
||||||
assert_eq!(content(&t), "ab");
|
assert_eq!(content(&t), "ab");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_multi_line_field_keeps_newlines() {
|
fn a_multi_line_field_keeps_newlines() {
|
||||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
let mut t = edit("", EditMode::MultiLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t).set_caret(0);
|
||||||
ctx(&mut t, &mut d).insert("a\nb");
|
ctx(&mut t).insert("a\nb");
|
||||||
assert_eq!(content(&t), "a\nb");
|
assert_eq!(content(&t), "a\nb");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn take_empties_the_field_and_hands_back_what_was_there() {
|
fn take_empties_the_field_and_hands_back_what_was_there() {
|
||||||
let (mut t, mut d) = edit("some text", EditMode::SingleLine);
|
let mut t = edit("some text", EditMode::SingleLine);
|
||||||
assert_eq!(ctx(&mut t, &mut d).take(), "some text");
|
assert_eq!(ctx(&mut t).take(), "some text");
|
||||||
assert_eq!(content(&t), "");
|
assert_eq!(content(&t), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ime_preedit_replaces_its_own_previous_text() {
|
fn ime_preedit_replaces_its_own_previous_text() {
|
||||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
let mut t = edit("", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t).set_caret(0);
|
||||||
ctx(&mut t, &mut d).replace(0, "n");
|
ctx(&mut t).replace(0, "n");
|
||||||
assert_eq!(content(&t), "n");
|
assert_eq!(content(&t), "n");
|
||||||
ctx(&mut t, &mut d).replace(1, "ni");
|
ctx(&mut t).replace(1, "ni");
|
||||||
assert_eq!(content(&t), "ni");
|
assert_eq!(content(&t), "ni");
|
||||||
ctx(&mut t, &mut d).replace(2, "に");
|
ctx(&mut t).replace(2, "に");
|
||||||
assert_eq!(content(&t), "に");
|
assert_eq!(content(&t), "に");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn composing_advances_the_caret_with_the_growing_text() {
|
fn composing_advances_the_caret_with_the_growing_text() {
|
||||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
let mut t = edit("", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t).set_caret(0);
|
||||||
ctx(&mut t, &mut d).replace(0, "h");
|
ctx(&mut t).replace(0, "h");
|
||||||
assert_eq!(t.caret(), Some(1));
|
assert_eq!(t.caret(), Some(1));
|
||||||
ctx(&mut t, &mut d).replace(1, "hi");
|
ctx(&mut t).replace(1, "hi");
|
||||||
assert_eq!(content(&t), "hi");
|
assert_eq!(content(&t), "hi");
|
||||||
assert_eq!(t.caret(), Some(2));
|
assert_eq!(t.caret(), Some(2));
|
||||||
ctx(&mut t, &mut d).replace(2, "hit");
|
ctx(&mut t).replace(2, "hit");
|
||||||
assert_eq!(content(&t), "hit");
|
assert_eq!(content(&t), "hit");
|
||||||
assert_eq!(t.caret(), Some(3));
|
assert_eq!(t.caret(), Some(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn committing_composed_text_leaves_it_in_place_with_the_caret_after_it() {
|
fn committing_composed_text_leaves_it_in_place_with_the_caret_after_it() {
|
||||||
let (mut t, mut d) = edit("say ", EditMode::SingleLine);
|
let mut t = edit("say ", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(4);
|
ctx(&mut t).set_caret(4);
|
||||||
ctx(&mut t, &mut d).replace(0, "hi");
|
ctx(&mut t).replace(0, "hi");
|
||||||
assert_eq!(content(&t), "say hi");
|
assert_eq!(content(&t), "say hi");
|
||||||
ctx(&mut t, &mut d).replace(0, " ");
|
ctx(&mut t).replace(0, " ");
|
||||||
assert_eq!(content(&t), "say hi ");
|
assert_eq!(content(&t), "say hi ");
|
||||||
assert_eq!(t.caret(), Some(7));
|
assert_eq!(t.caret(), Some(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_byte_range_removes_exactly_that_range() {
|
fn delete_byte_range_removes_exactly_that_range() {
|
||||||
let (mut t, mut d) = edit("hello world", EditMode::SingleLine);
|
let mut t = edit("hello world", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).delete_byte_range(5, 11);
|
ctx(&mut t).delete_byte_range(5, 11);
|
||||||
assert_eq!(content(&t), "hello");
|
assert_eq!(content(&t), "hello");
|
||||||
assert_eq!(t.caret(), Some(5));
|
assert_eq!(t.caret(), Some(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_cursor_byte_collapses_to_a_caret_there() {
|
fn set_cursor_byte_collapses_to_a_caret_there() {
|
||||||
let (mut t, mut d) = edit("hello world", EditMode::SingleLine);
|
let mut t = edit("hello world", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t).select_all();
|
||||||
ctx(&mut t, &mut d).set_cursor_byte(5);
|
ctx(&mut t).set_cursor_byte(5);
|
||||||
assert_eq!(t.selected_text(), None);
|
assert_eq!(t.selected_text(), None);
|
||||||
assert_eq!(t.caret(), Some(5));
|
assert_eq!(t.caret(), Some(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn motion_moves_the_caret_and_shift_extends_a_span() {
|
fn motion_moves_the_caret_and_shift_extends_a_span() {
|
||||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
let mut t = edit("abc", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(0);
|
ctx(&mut t).set_caret(0);
|
||||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
ctx(&mut t).motion(Motion::Right, false);
|
||||||
assert_eq!(t.caret(), Some(1));
|
assert_eq!(t.caret(), Some(1));
|
||||||
ctx(&mut t, &mut d).motion(Motion::Right, true);
|
ctx(&mut t).motion(Motion::Right, true);
|
||||||
assert_eq!(t.selected_text().as_deref(), Some("b"));
|
assert_eq!(t.selected_text().as_deref(), Some("b"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_unshifted_arrow_collapses_a_span_to_its_edge() {
|
fn an_unshifted_arrow_collapses_a_span_to_its_edge() {
|
||||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
let mut t = edit("abcdef", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t).select_all();
|
||||||
ctx(&mut t, &mut d).motion(Motion::Left, false);
|
ctx(&mut t).motion(Motion::Left, false);
|
||||||
assert_eq!(t.caret(), Some(0));
|
assert_eq!(t.caret(), Some(0));
|
||||||
|
|
||||||
ctx(&mut t, &mut d).select_all();
|
ctx(&mut t).select_all();
|
||||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
ctx(&mut t).motion(Motion::Right, false);
|
||||||
assert_eq!(t.caret(), Some(6));
|
assert_eq!(t.caret(), Some(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multibyte_text_is_edited_by_byte_offset() {
|
fn multibyte_text_is_edited_by_byte_offset() {
|
||||||
let (mut t, mut d) = edit("aé", EditMode::SingleLine);
|
let mut t = edit("aé", EditMode::SingleLine);
|
||||||
ctx(&mut t, &mut d).set_caret(3);
|
ctx(&mut t).set_caret(3);
|
||||||
ctx(&mut t, &mut d).backspace(false);
|
ctx(&mut t).backspace(false);
|
||||||
assert_eq!(content(&t), "a");
|
assert_eq!(content(&t), "a");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+58
-107
@@ -4,87 +4,65 @@ mod selection;
|
|||||||
|
|
||||||
pub use build::*;
|
pub use build::*;
|
||||||
pub use edit::*;
|
pub use edit::*;
|
||||||
use iris_core::util::MutDetect;
|
|
||||||
pub use selection::*;
|
pub use selection::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
pub content: MutDetect<String>,
|
|
||||||
view: TextView,
|
view: TextView,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
pub attrs: MutDetect<TextAttrs>,
|
text: TextHandle,
|
||||||
pub buf: MutDetect<TextBuffer>,
|
|
||||||
tex: Option<RenderedText>,
|
|
||||||
width: Option<f32>,
|
|
||||||
pub hint: Option<StrongWidget>,
|
pub hint: Option<StrongWidget>,
|
||||||
selection: TextSelection,
|
selection: TextSelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextView {
|
impl TextView {
|
||||||
fn is_blank(&self) -> bool {
|
fn is_blank(&self) -> bool {
|
||||||
self.buf.is_empty()
|
self.text.text().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap_width(&self) -> Option<f32> {
|
pub fn wrap_width(&self) -> Option<f32> {
|
||||||
self.width
|
self.text.width()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
pub fn new(text: TextHandle, hint: Option<StrongWidget>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attrs: attrs.into(),
|
text,
|
||||||
buf: buf.into(),
|
|
||||||
tex: None,
|
|
||||||
width: None,
|
|
||||||
hint,
|
hint,
|
||||||
selection: TextSelection::default(),
|
selection: TextSelection::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
self.tex()
|
let align = self.text.attrs().align;
|
||||||
|
self.text
|
||||||
|
.rendered()
|
||||||
.map(|t| t.size)
|
.map(|t| t.size)
|
||||||
.unwrap_or(Vec2::ZERO)
|
.unwrap_or(Vec2::ZERO)
|
||||||
.align(self.align)
|
.align(align)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, painter: &mut Painter) -> RenderedText {
|
fn render(&mut self, painter: &mut Painter) -> RenderedText {
|
||||||
let width = if self.attrs.wrap {
|
let width = if self.text.attrs().wrap {
|
||||||
Some(painter.px_size().x)
|
Some(painter.px_size().x)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let generation = painter.atlas_generation();
|
let tex = painter.render_text(&self.text, width);
|
||||||
if width == self.width
|
|
||||||
&& let Some(tex) = &self.tex
|
|
||||||
&& tex.generation == generation
|
|
||||||
&& !self.attrs.changed
|
|
||||||
&& !self.buf.changed
|
|
||||||
{
|
|
||||||
return tex.clone();
|
|
||||||
}
|
|
||||||
self.width = width;
|
|
||||||
let tex = painter.render_text(&mut self.buf, &self.attrs, width);
|
|
||||||
if crate::diagnostics::trace_enabled() {
|
if crate::diagnostics::trace_enabled() {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
target: "iris::frame",
|
target: "iris::frame",
|
||||||
"iris text render: chars={} width={width:?} glyphs={} size={:?}",
|
"iris text render: chars={} width={width:?} glyphs={} size={:?}",
|
||||||
self.buf.text().chars().count(),
|
self.text.text().chars().count(),
|
||||||
tex.glyphs.len(),
|
tex.glyphs.len(),
|
||||||
tex.size,
|
tex.size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.tex = Some(tex.clone());
|
|
||||||
self.attrs.changed = false;
|
|
||||||
self.buf.changed = false;
|
|
||||||
tex
|
tex
|
||||||
}
|
}
|
||||||
pub fn tex(&self) -> Option<&RenderedText> {
|
|
||||||
self.tex.as_ref()
|
|
||||||
}
|
|
||||||
pub fn draw(&mut self, painter: &mut Painter) -> Size {
|
pub fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let tex = self.render(painter);
|
let tex = self.render(painter);
|
||||||
if self.is_blank()
|
if self.is_blank()
|
||||||
@@ -92,7 +70,7 @@ impl TextView {
|
|||||||
{
|
{
|
||||||
return painter.widget(hint).size();
|
return painter.widget(hint).size();
|
||||||
}
|
}
|
||||||
let region = tex.size.align(self.align);
|
let region = tex.size.align(self.text.attrs().align);
|
||||||
let within = region.within(&painter.region());
|
let within = region.within(&painter.region());
|
||||||
painter.glyphs(&tex, within);
|
painter.glyphs(&tex, within);
|
||||||
Size::abs(tex.size)
|
Size::abs(tex.size)
|
||||||
@@ -108,8 +86,10 @@ impl TextView {
|
|||||||
let Some(selection) = self.selection.range else {
|
let Some(selection) = self.selection.range else {
|
||||||
return used;
|
return used;
|
||||||
};
|
};
|
||||||
let layout = self.buf.layout();
|
let geometry = self
|
||||||
for (rect, _) in selection.geometry(layout) {
|
.text
|
||||||
|
.with_layout(|layout, _| selection.geometry(layout));
|
||||||
|
for (rect, _) in geometry {
|
||||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||||
let paint = painter.paint(&PaintId::SKY);
|
let paint = painter.paint(&PaintId::SKY);
|
||||||
@@ -119,7 +99,9 @@ impl TextView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if caret {
|
if caret {
|
||||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
let caret = self
|
||||||
|
.text
|
||||||
|
.with_layout(|layout, _| selection.focus().geometry(layout, CARET_WIDTH));
|
||||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||||
let paint = painter.paint(&PaintId::WHITE);
|
let paint = painter.paint(&PaintId::WHITE);
|
||||||
@@ -132,28 +114,23 @@ impl TextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(&self) -> String {
|
pub fn content(&self) -> String {
|
||||||
self.buf.text().to_string()
|
self.text.text().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R {
|
||||||
|
self.text.update_attrs(update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Text {
|
impl Text {
|
||||||
pub fn new(content: impl Into<String>) -> Self {
|
pub fn set_text(&mut self, content: impl Into<String>) {
|
||||||
let content: String = content.into();
|
if self.view.text.set_text(content) {
|
||||||
Self {
|
|
||||||
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None),
|
|
||||||
content: content.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn update_buf(&mut self) {
|
|
||||||
if self.content.changed {
|
|
||||||
self.content.changed = false;
|
|
||||||
self.view.buf.set_text(self.content.as_str());
|
|
||||||
self.view.selection.deselect();
|
self.view.selection.deselect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_text(&self) -> Option<String> {
|
pub fn selected_text(&self) -> Option<String> {
|
||||||
self.view.selection.selected_text(self.view.buf.text())
|
self.view.selection.selected_text(&self.view.text.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||||
@@ -161,18 +138,22 @@ impl Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
|
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
|
||||||
let content = content.into();
|
self.view.text.set_text(content);
|
||||||
*self.content = content.clone();
|
self.view.text.set_spans(spans);
|
||||||
self.content.changed = false;
|
|
||||||
self.view.buf.set_text(content);
|
|
||||||
self.view.buf.set_spans(spans);
|
|
||||||
self.view.selection.deselect();
|
self.view.selection.deselect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> String {
|
||||||
|
self.view.content()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R {
|
||||||
|
self.view.update_attrs(update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Text {
|
impl Widget for Text {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
self.update_buf();
|
|
||||||
let size = if self.view.selection.range.is_some() {
|
let size = if self.view.selection.range.is_some() {
|
||||||
self.view.draw_selectable(painter, false)
|
self.view.draw_selectable(painter, false)
|
||||||
} else {
|
} else {
|
||||||
@@ -188,34 +169,6 @@ impl Widget for Text {
|
|||||||
|
|
||||||
pub(super) const CARET_WIDTH: f32 = 1.0;
|
pub(super) const CARET_WIDTH: f32 = 1.0;
|
||||||
|
|
||||||
impl Deref for Text {
|
|
||||||
type Target = TextAttrs;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Text {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for TextView {
|
|
||||||
type Target = TextAttrs;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.attrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for TextView {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.attrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::layout_tests::TestRsc;
|
use crate::layout_tests::TestRsc;
|
||||||
@@ -237,20 +190,20 @@ mod tests {
|
|||||||
let (mut rsc, _render, text, _root) = rendered_text("hello there");
|
let (mut rsc, _render, text, _root) = rendered_text("hello there");
|
||||||
text.selection(&mut rsc).select_all();
|
text.selection(&mut rsc).select_all();
|
||||||
|
|
||||||
let view = TextView::new(TextBuffer::new("hello there"), TextAttrs::default(), None);
|
let edit = wtext("hello there")
|
||||||
let mut edit = TextEdit::new(view, EditMode::MultiLine);
|
.editable(EditMode::MultiLine)
|
||||||
let mut data = TextData::default();
|
.add_strong(&mut rsc);
|
||||||
TextEditCtx {
|
let edit_id = edit.weak();
|
||||||
text: &mut edit,
|
edit_id(&mut rsc).select_all();
|
||||||
data: &mut data,
|
|
||||||
}
|
|
||||||
.select_all();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rsc.ui.widgets[text].selection_range(),
|
rsc.ui.widgets[text].selection_range(),
|
||||||
edit.selection_range()
|
rsc.ui.widgets[edit_id].selection_range()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rsc.ui.widgets[text].selected_text(),
|
||||||
|
rsc.ui.widgets[edit_id].selected_text()
|
||||||
);
|
);
|
||||||
assert_eq!(rsc.ui.widgets[text].selected_text(), edit.selected_text());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -262,7 +215,7 @@ mod tests {
|
|||||||
Some("before")
|
Some("before")
|
||||||
);
|
);
|
||||||
|
|
||||||
*rsc.ui.widgets[text].content = "after".to_string();
|
rsc.ui.widgets[text].set_text("after");
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
assert_eq!(rsc.ui.widgets[text].selected_text(), None);
|
assert_eq!(rsc.ui.widgets[text].selected_text(), None);
|
||||||
@@ -277,17 +230,15 @@ mod tests {
|
|||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
let selected = render.active[&text.id()].primitives.len();
|
let selected = render.active[&text.id()].primitives.len();
|
||||||
|
|
||||||
let view = TextView::new(TextBuffer::new("selected"), TextAttrs::default(), None);
|
let edit = wtext("selected")
|
||||||
let edit = rsc
|
.editable(EditMode::MultiLine)
|
||||||
.ui
|
.add_strong(&mut rsc);
|
||||||
.widgets
|
|
||||||
.add_strong(TextEdit::new(view, EditMode::MultiLine));
|
|
||||||
let edit_id = edit.weak();
|
let edit_id = edit.weak();
|
||||||
let edit_root = edit.any();
|
let edit_root = edit.any();
|
||||||
let mut edit_render = UiRenderState::new();
|
let mut edit_render = UiRenderState::new();
|
||||||
edit_render.resize((800.0, 600.0));
|
edit_render.resize((800.0, 600.0));
|
||||||
edit_render.update(&edit_root, &mut rsc);
|
edit_render.update(&edit_root, &mut rsc);
|
||||||
edit_id.edit(&mut rsc).select_all();
|
edit_id(&mut rsc).select_all();
|
||||||
edit_render.update(&edit_root, &mut rsc);
|
edit_render.update(&edit_root, &mut rsc);
|
||||||
let editable = edit_render.active[&edit_id.id()].primitives.len();
|
let editable = edit_render.active[&edit_id.id()].primitives.len();
|
||||||
|
|
||||||
@@ -314,16 +265,16 @@ mod tests {
|
|||||||
render.resize((800.0, 600.0));
|
render.resize((800.0, 600.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
let rasterised = rsc.ui.text.atlas.glyph_count();
|
let rasterised = rsc.ui.text.borrow().atlas.glyph_count();
|
||||||
assert!(rasterised > 0, "the first frame rasterised no glyphs");
|
assert!(rasterised > 0, "the first frame rasterised no glyphs");
|
||||||
|
|
||||||
rsc.ui.text.atlas.clear();
|
rsc.ui.text.borrow_mut().atlas.clear();
|
||||||
assert_eq!(rsc.ui.text.atlas.glyph_count(), 0);
|
assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 0);
|
||||||
render.resize((800.0, 600.0));
|
render.resize((800.0, 600.0));
|
||||||
render.update(&root, &mut rsc);
|
render.update(&root, &mut rsc);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rsc.ui.text.atlas.glyph_count(),
|
rsc.ui.text.borrow().atlas.glyph_count(),
|
||||||
rasterised,
|
rasterised,
|
||||||
"the second frame re-emitted its cached glyphs instead of \
|
"the second frame re-emitted its cached glyphs instead of \
|
||||||
re-rendering them against the fresh atlas"
|
re-rendering them against the fresh atlas"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use iris_core::{PaintId, TextData};
|
use parley::{Affinity, Selection as ParleySelection};
|
||||||
use parley::{Affinity, Layout, Selection as ParleySelection};
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
/// The selection state shared by display text and editable text. Editing,
|
/// The selection state shared by display text and editable text. Editing,
|
||||||
@@ -35,23 +34,21 @@ impl TextSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selection operations that need both a text widget's shaped buffer and
|
/// Selection operations shared by ordinary and editable text, using the
|
||||||
/// iris's text resources. `TextEditCtx` delegates to this same context rather
|
/// widget's arena-backed layout rather than maintaining an editable-only copy
|
||||||
/// than maintaining an editable-only copy of the geometry and hit testing.
|
/// of the geometry and hit testing.
|
||||||
pub struct TextSelectionCtx<'a> {
|
pub struct TextSelectionCtx<'a> {
|
||||||
pub(super) view: &'a mut TextView,
|
pub(super) view: &'a mut TextView,
|
||||||
pub(super) data: &'a mut TextData,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextSelectionCtx<'_> {
|
impl TextSelectionCtx<'_> {
|
||||||
pub(super) fn layout(&mut self) -> &Layout<PaintId> {
|
|
||||||
selection_layout(self.view, self.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn refresh(&mut self) {
|
pub(crate) fn refresh(&mut self) {
|
||||||
if let Some(selection) = self.view.selection.range {
|
if let Some(selection) = self.view.selection.range {
|
||||||
let layout = self.layout();
|
self.view.selection.range = Some(
|
||||||
self.view.selection.range = Some(selection.refresh(layout));
|
self.view
|
||||||
|
.text
|
||||||
|
.with_layout(|layout, _| selection.refresh(layout)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +56,23 @@ impl TextSelectionCtx<'_> {
|
|||||||
/// the same widget-local coordinates as a `CursorSense` event.
|
/// the same widget-local coordinates as a `CursorSense` event.
|
||||||
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
||||||
let pos = pos - self.view.region().top_left().to_abs(size);
|
let pos = pos - self.view.region().top_left().to_abs(size);
|
||||||
let layout = self.layout();
|
self.view.text.with_layout(|layout, _| {
|
||||||
ParleySelection::from_point(layout, pos.x, pos.y)
|
ParleySelection::from_point(layout, pos.x, pos.y)
|
||||||
.focus()
|
.focus()
|
||||||
.index()
|
.index()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_all(&mut self) {
|
pub fn select_all(&mut self) {
|
||||||
let len = self.view.buf.text().len();
|
let len = self.view.text.text().len();
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let layout = self.layout();
|
self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
|
||||||
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
||||||
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
||||||
self.view.selection.range = Some(ParleySelection::new(anchor, focus));
|
ParleySelection::new(anchor, focus)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||||
@@ -81,8 +80,7 @@ impl TextSelectionCtx<'_> {
|
|||||||
let previous = self.view.selection.range;
|
let previous = self.view.selection.range;
|
||||||
let previous_hit = self.view.selection.double_hit;
|
let previous_hit = self.view.selection.double_hit;
|
||||||
|
|
||||||
let outcome = {
|
let outcome = self.view.text.with_layout(|layout, _| {
|
||||||
let layout = self.layout();
|
|
||||||
if drag {
|
if drag {
|
||||||
previous.map(|selection| {
|
previous.map(|selection| {
|
||||||
(
|
(
|
||||||
@@ -109,7 +107,7 @@ impl TextSelectionCtx<'_> {
|
|||||||
(Some(hit), None)
|
(Some(hit), None)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
if let Some((range, double_hit)) = outcome {
|
if let Some((range, double_hit)) = outcome {
|
||||||
self.view.selection.range = range;
|
self.view.selection.range = range;
|
||||||
@@ -122,39 +120,28 @@ impl TextSelectionCtx<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_caret(&mut self, index: usize) {
|
pub(crate) fn set_caret(&mut self, index: usize) {
|
||||||
let index = index.min(self.view.buf.text().len());
|
let index = index.min(self.view.text.text().len());
|
||||||
let layout = self.layout();
|
self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
|
||||||
self.view.selection.range = Some(ParleySelection::from_byte_index(
|
ParleySelection::from_byte_index(layout, index, Affinity::default())
|
||||||
layout,
|
}));
|
||||||
index,
|
|
||||||
Affinity::default(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_between(&mut self, anchor: usize, focus: usize) {
|
fn select_between(&mut self, anchor: usize, focus: usize) {
|
||||||
let len = self.view.buf.text().len();
|
let len = self.view.text.text().len();
|
||||||
let layout = self.layout();
|
self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
|
||||||
let anchor = parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default());
|
let anchor =
|
||||||
let focus = parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default());
|
parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default());
|
||||||
self.view.selection.range = Some(ParleySelection::new(anchor, focus));
|
let focus =
|
||||||
|
parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default());
|
||||||
|
ParleySelection::new(anchor, focus)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn selection_layout<'a>(
|
|
||||||
view: &'a mut TextView,
|
|
||||||
data: &mut TextData,
|
|
||||||
) -> &'a Layout<PaintId> {
|
|
||||||
let attrs = view.attrs.clone();
|
|
||||||
let width = view.wrap_width();
|
|
||||||
let density = data.density;
|
|
||||||
view.buf.shape(data, &attrs, width, density);
|
|
||||||
view.buf.layout()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gives an ordinary `Text` handle access to the same selection operations as
|
/// Gives an ordinary `Text` handle access to the same selection operations as
|
||||||
/// `TextEditCtx`. Gesture policy is intentionally not part of this trait; a
|
/// `TextEdit`. Gesture policy is intentionally not part of this trait; a
|
||||||
/// selection controller and an editor's focus handler do different
|
/// selection controller and an editor's focus handler do different things
|
||||||
/// things with the same mechanics.
|
/// with the same mechanics.
|
||||||
pub trait TextSelectable {
|
pub trait TextSelectable {
|
||||||
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a>;
|
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a>;
|
||||||
}
|
}
|
||||||
@@ -164,7 +151,6 @@ impl<I: IdLike<Widget = Text>> TextSelectable for I {
|
|||||||
let ui: &mut UiData = ui.ui_mut();
|
let ui: &mut UiData = ui.ui_mut();
|
||||||
TextSelectionCtx {
|
TextSelectionCtx {
|
||||||
view: &mut ui.widgets.get_mut(self).unwrap().view,
|
view: &mut ui.widgets.get_mut(self).unwrap().view,
|
||||||
data: &mut ui.text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,10 +242,8 @@ impl SelectionController {
|
|||||||
.get_dyn_mut(id)?
|
.get_dyn_mut(id)?
|
||||||
.as_any_mut()
|
.as_any_mut()
|
||||||
.downcast_mut::<Text>()?;
|
.downcast_mut::<Text>()?;
|
||||||
text.update_buf();
|
|
||||||
let mut ctx = TextSelectionCtx {
|
let mut ctx = TextSelectionCtx {
|
||||||
view: &mut text.view,
|
view: &mut text.view,
|
||||||
data: &mut ui.text,
|
|
||||||
};
|
};
|
||||||
Some(f(&mut ctx))
|
Some(f(&mut ctx))
|
||||||
}
|
}
|
||||||
@@ -334,7 +318,7 @@ impl SelectionController {
|
|||||||
for &text_id in &self.order[lo..=hi] {
|
for &text_id in &self.order[lo..=hi] {
|
||||||
let forward = anchor_at <= focus_at;
|
let forward = anchor_at <= focus_at;
|
||||||
Self::with_text(rsc, text_id, |text| {
|
Self::with_text(rsc, text_id, |text| {
|
||||||
let len = text.view.buf.text().len();
|
let len = text.view.text.text().len();
|
||||||
let (start, end) = if text_id == anchor && text_id == id {
|
let (start, end) = if text_id == anchor && text_id == id {
|
||||||
(anchor_byte, focus_byte)
|
(anchor_byte, focus_byte)
|
||||||
} else if text_id == anchor {
|
} else if text_id == anchor {
|
||||||
|
|||||||
Reference in new issue
Block a user