iris: move text state into shared resources
This commit is contained in:
1 parent
7c23c5f146
commit
51719ed121
17 files changed
+626
-429
No files matched your search
@@ -21,9 +21,10 @@ exists only to give a row enough text to wrap across several lines at a \
|
||||
typical phone column width.";
|
||||
|
||||
fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
|
||||
let mut text = Text::new(format!("Message {i}: {BODY}"));
|
||||
text.wrap = true;
|
||||
let text = rsc.ui.widgets.add_strong(text).any();
|
||||
let text = wtext(format!("Message {i}: {BODY}"))
|
||||
.wrap(true)
|
||||
.add_strong(rsc)
|
||||
.any();
|
||||
|
||||
if image_every > 0 && i.is_multiple_of(image_every) {
|
||||
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)
|
||||
.map(|i| char::from(b'a' + (i % 26) as u8))
|
||||
.collect();
|
||||
let mut text = Text::new(content);
|
||||
text.wrap = true;
|
||||
let text = rsc.ui.widgets.add_strong(text);
|
||||
let text = wtext(content).wrap(true).add_strong(&mut rsc);
|
||||
let handle = text.weak();
|
||||
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::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
||||
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
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::{
|
||||
FontRef,
|
||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||
@@ -25,7 +38,6 @@ pub struct FontDiagnostics {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FontRegistrationError {
|
||||
AlreadyRegistered(String),
|
||||
TextAlreadyShaped,
|
||||
InvalidFont(String),
|
||||
}
|
||||
|
||||
@@ -35,10 +47,6 @@ impl fmt::Display for FontRegistrationError {
|
||||
Self::AlreadyRegistered(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) => {
|
||||
write!(
|
||||
f,
|
||||
@@ -57,10 +65,9 @@ pub struct TextData {
|
||||
scale_cx: ScaleContext,
|
||||
pub atlas: GlyphAtlas,
|
||||
/// Physical pixels per dp -- a second copy of
|
||||
/// `UiRenderState::density`, kept here too because `TextEditCtx::layout`
|
||||
/// (cursor movement and hit-testing, `widget/text/edit.rs`) shapes text
|
||||
/// from an event callback that has a `TextData` but no `Painter`, so it
|
||||
/// has nowhere else to read the display's density from. Both copies are
|
||||
/// `UiRenderState::density`, kept here too because cursor movement and
|
||||
/// hit-testing shape text from event callbacks that have no `Painter`,
|
||||
/// so they have nowhere else to read the display's density from. Both copies are
|
||||
/// set together, from the one place either backend learns the real
|
||||
/// value (`android::view::new_peer`); this is the same accepted
|
||||
/// duplication as `AndroidRenderer::content_scale`; a single source of
|
||||
@@ -69,7 +76,6 @@ pub struct TextData {
|
||||
pub density: f32,
|
||||
registered_families: HashMap<String, String>,
|
||||
next_registered_family: u64,
|
||||
shaping_started: bool,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
@@ -84,7 +90,6 @@ impl Default for TextData {
|
||||
density: 1.0,
|
||||
registered_families: HashMap::new(),
|
||||
next_registered_family: 0,
|
||||
shaping_started: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,9 +173,6 @@ impl TextData {
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), FontRegistrationError> {
|
||||
let family = family.as_ref().to_owned();
|
||||
if self.shaping_started {
|
||||
return Err(FontRegistrationError::TextAlreadyShaped);
|
||||
}
|
||||
if self.registered_families.contains_key(&family) {
|
||||
return Err(FontRegistrationError::AlreadyRegistered(family));
|
||||
}
|
||||
@@ -523,7 +525,6 @@ impl TextBuffer {
|
||||
width: Option<f32>,
|
||||
density: f32,
|
||||
) {
|
||||
data.shaping_started = true;
|
||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) {
|
||||
return;
|
||||
}
|
||||
@@ -574,6 +575,10 @@ impl TextBuffer {
|
||||
.align(Alignment::Start, AlignmentOptions::default());
|
||||
self.shaped = Some((attrs.clone(), width, density));
|
||||
}
|
||||
|
||||
fn invalidate(&mut self) {
|
||||
self.shaped = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_coords(coords: &[i16]) -> u64 {
|
||||
@@ -605,6 +610,261 @@ pub struct RenderedText {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -619,13 +879,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registering_after_shaping_is_reported() {
|
||||
let mut data = TextData::default();
|
||||
let mut buffer = TextBuffer::new("ordinary platform text");
|
||||
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
|
||||
assert_eq!(
|
||||
data.register_font("icons", b"not a font" as &'static [u8]),
|
||||
Err(FontRegistrationError::TextAlreadyShaped)
|
||||
fn dropped_handles_release_their_arena_entries() {
|
||||
let resources = Rc::new(RefCell::new(TextResources::new()));
|
||||
let text = TextResources::add(
|
||||
resources.clone(),
|
||||
TextBuffer::new("temporary"),
|
||||
TextAttrs::default(),
|
||||
);
|
||||
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::{
|
||||
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
Mask, MoveOffset, Paints, TextResources, Textures, WeakWidget, WidgetId, Widgets,
|
||||
util::TrackedArena,
|
||||
};
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -22,7 +23,7 @@ pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
pub paints: Paints,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub text: Rc<RefCell<TextResources>>,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
pub move_offsets: TrackedArena<MoveOffset, u32>,
|
||||
animating: Vec<WidgetId>,
|
||||
@@ -67,21 +68,32 @@ pub struct Ui {
|
||||
|
||||
impl Ui {
|
||||
/// Register application-owned font data for a semantic or named family.
|
||||
///
|
||||
/// This must happen before the first text shape. Existing text layouts
|
||||
/// cache their resolved faces, so accepting a later registration would
|
||||
/// leave already-shaped widgets displaying the old result.
|
||||
/// Existing text resources are invalidated and their active widgets are
|
||||
/// scheduled for layout again.
|
||||
#[track_caller]
|
||||
pub fn register_font(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> 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 {
|
||||
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.
|
||||
@@ -96,7 +108,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -172,5 +184,6 @@ pub trait UiRsc {
|
||||
}
|
||||
self.ui_mut().textures.free();
|
||||
self.ui_mut().paints.free_released();
|
||||
self.ui_mut().text.borrow_mut().free_released();
|
||||
}
|
||||
}
|
||||
+11
-15
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
|
||||
TextBuffer, TextData, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextHandle,
|
||||
TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
|
||||
WidgetId,
|
||||
render::{
|
||||
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
ui::render_state::Retained,
|
||||
util::Vec2,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub struct Painter<'a> {
|
||||
pub(super) render_state: &'a mut UiRenderState,
|
||||
@@ -413,21 +414,16 @@ impl<'a> Painter<'a> {
|
||||
self.own(h);
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
pub fn render_text(&mut self, text: &TextHandle, width: Option<f32>) -> RenderedText {
|
||||
let density = self.render_state.density;
|
||||
self.render_state.shape_count += 1;
|
||||
let ui: &mut UiData = self.rsc.ui_mut();
|
||||
ui.text
|
||||
.render(buffer, attrs, width, &mut ui.textures, density)
|
||||
let (rendered, prepared) = text.render(width, self.id, &mut ui.textures, density);
|
||||
self.render_state.shape_count += u64::from(prepared);
|
||||
rendered
|
||||
}
|
||||
|
||||
pub fn atlas_generation(&mut self) -> u64 {
|
||||
self.rsc.ui_mut().text.atlas.generation()
|
||||
fn atlas_generation(&self) -> u64 {
|
||||
self.rsc.ui().text.borrow().atlas.generation()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
&mut self.rsc.ui_mut().text
|
||||
pub fn text_resources(&mut self) -> Rc<RefCell<TextResources>> {
|
||||
self.rsc.ui().text.clone()
|
||||
}
|
||||
|
||||
pub fn child_layer(&mut self) {
|
||||
|
||||
@@ -71,6 +71,10 @@ impl<T> SlotVec<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
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> {
|
||||
|
||||
@@ -8,8 +8,8 @@ pub(crate) fn update_info<Rsc: UiRsc>(rsc: &mut Rsc, info: WeakWidget<Text>, vie
|
||||
rsc.widgets().len(),
|
||||
render_state.get().active_widgets(),
|
||||
);
|
||||
if new != *rsc.widgets()[info].content {
|
||||
*rsc.widgets_mut()[info].content = new;
|
||||
if new != rsc.widgets()[info].content() {
|
||||
rsc.widgets_mut()[info].set_text(new);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ where
|
||||
.attr::<Selectable>(())
|
||||
.on(Submit, move |ctx, rsc: &mut Rsc| {
|
||||
let w = ctx.widget;
|
||||
let content = w.edit(rsc).take();
|
||||
let content = w(rsc).take();
|
||||
let text = wtext(content)
|
||||
.editable(EditMode::MultiLine)
|
||||
.size(30)
|
||||
|
||||
@@ -59,7 +59,7 @@ the retained widget tree dirty:
|
||||
```rust
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
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;
|
||||
};
|
||||
let content = text.text();
|
||||
let sel_start = byte_to_utf16(content, sel.start) as i32;
|
||||
let sel_end = byte_to_utf16(content, sel.end) 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 compose_len = self.state.android_state().compose_len;
|
||||
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)
|
||||
} else {
|
||||
(-1, -1)
|
||||
@@ -89,16 +89,12 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
if let Some(focus) = self.focus() {
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range().unwrap_or(0..0);
|
||||
let start = byte_to_utf16(text.text(), sel.start) as i32;
|
||||
let end = byte_to_utf16(text.text(), sel.end) as i32;
|
||||
let content = text.text();
|
||||
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_end(&mut ctx.env, end);
|
||||
let caps = caps_mode(
|
||||
&mut ctx.env,
|
||||
text.text(),
|
||||
start as usize,
|
||||
CAP_MODE_SENTENCES,
|
||||
);
|
||||
let caps = caps_mode(&mut ctx.env, &content, start as usize, CAP_MODE_SENTENCES);
|
||||
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 text = focus.get(&self.rsc);
|
||||
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 = utf16_to_byte(text.text(), start_16);
|
||||
Some(Cow::Borrowed(&text.text()[start..sel.start]))
|
||||
let start = utf16_to_byte(&content, start_16);
|
||||
Some(Cow::Owned(content[start..sel.start].to_owned()))
|
||||
}
|
||||
|
||||
fn text_after_cursor<'slf>(
|
||||
@@ -131,11 +128,12 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
let focus = self.focus()?;
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range()?;
|
||||
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
||||
let start_16 = byte_to_utf16(text.text(), sel.end);
|
||||
let content = text.text();
|
||||
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 = utf16_to_byte(text.text(), end_16);
|
||||
Some(Cow::Borrowed(&text.text()[sel.end..end]))
|
||||
let end = utf16_to_byte(&content, end_16);
|
||||
Some(Cow::Owned(content[sel.end..end].to_owned()))
|
||||
}
|
||||
|
||||
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 {
|
||||
return 0;
|
||||
};
|
||||
let off = byte_to_utf16(text.text(), caret);
|
||||
caps_mode(&mut ctx.env, text.text(), off, req_modes)
|
||||
let content = text.text();
|
||||
let off = byte_to_utf16(&content, caret);
|
||||
caps_mode(&mut ctx.env, &content, off, req_modes)
|
||||
}
|
||||
|
||||
fn delete_surrounding_text(
|
||||
@@ -170,12 +169,13 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
};
|
||||
let content = text.text();
|
||||
let start_16 =
|
||||
byte_to_utf16(content, sel.start).saturating_sub(before_length.max(0) as usize);
|
||||
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 start = utf16_to_byte(content, start_16);
|
||||
let end = utf16_to_byte(content, end_16);
|
||||
focus.edit(&mut self.rsc).delete_byte_range(start, end);
|
||||
byte_to_utf16(&content, sel.start).saturating_sub(before_length.max(0) as usize);
|
||||
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 start = utf16_to_byte(&content, start_16);
|
||||
let end = utf16_to_byte(&content, end_16);
|
||||
drop(content);
|
||||
focus(&mut self.rsc).delete_byte_range(start, end);
|
||||
self.after_input(ctx);
|
||||
true
|
||||
}
|
||||
@@ -199,7 +199,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
return false;
|
||||
};
|
||||
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.after_input(ctx);
|
||||
true
|
||||
@@ -225,8 +225,9 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
};
|
||||
let text = focus.get(&self.rsc);
|
||||
let content = text.text();
|
||||
let byte = utf16_to_byte(content, end.max(0) as usize);
|
||||
focus.edit(&mut self.rsc).set_cursor_byte(byte);
|
||||
let byte = utf16_to_byte(&content, end.max(0) as usize);
|
||||
drop(content);
|
||||
focus(&mut self.rsc).set_cursor_byte(byte);
|
||||
let _ = start;
|
||||
self.after_input(ctx);
|
||||
true
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(super) fn on_key<'local, State: AndroidAppState>(
|
||||
let Some(focus) = state.android_state().focus else {
|
||||
return false;
|
||||
};
|
||||
let mut text = focus.edit(rsc);
|
||||
let text = focus(rsc);
|
||||
match key_code {
|
||||
Keycode::Del => text.backspace(false),
|
||||
Keycode::ForwardDel => text.delete(false),
|
||||
|
||||
+8
-8
@@ -217,7 +217,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
if old_focus != ui_state.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) {
|
||||
show_soft_input(&mut ctx.env, &ctx.view);
|
||||
@@ -619,7 +619,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
if !gain_focus {
|
||||
let ui_state = self.state.android_state_mut();
|
||||
if let Some(focus) = ui_state.focus.take() {
|
||||
focus.edit(&mut self.rsc).deselect();
|
||||
focus(&mut self.rsc).deselect();
|
||||
}
|
||||
}
|
||||
self.after_input(ctx);
|
||||
@@ -646,8 +646,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
log::info!(
|
||||
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
||||
glyphs_cached={} atlas_pages={}",
|
||||
self.rsc.ui().text.atlas.glyph_count(),
|
||||
self.rsc.ui().text.atlas.page_count(),
|
||||
self.rsc.ui().text.borrow().atlas.glyph_count(),
|
||||
self.rsc.ui().text.borrow().atlas.page_count(),
|
||||
);
|
||||
if already_live {
|
||||
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: \
|
||||
glyphs={} pages={}",
|
||||
renderer.adapter_backend,
|
||||
self.rsc.ui().text.atlas.glyph_count(),
|
||||
self.rsc.ui().text.atlas.page_count(),
|
||||
self.rsc.ui().text.borrow().atlas.glyph_count(),
|
||||
self.rsc.ui().text.borrow().atlas.page_count(),
|
||||
);
|
||||
self.rsc.ui_mut().textures.reupload();
|
||||
self.rsc.ui_mut().paints.reupload();
|
||||
@@ -716,8 +716,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
log::info!(
|
||||
"iris surface: surface_destroyed, tearing the renderer down \
|
||||
(glyphs_cached={} atlas_pages={})",
|
||||
self.rsc.ui().text.atlas.glyph_count(),
|
||||
self.rsc.ui().text.atlas.page_count(),
|
||||
self.rsc.ui().text.borrow().atlas.glyph_count(),
|
||||
self.rsc.ui().text.borrow().atlas.page_count(),
|
||||
);
|
||||
self.state.android_state_mut().renderer = None;
|
||||
}
|
||||
|
||||
+3
-3
@@ -250,7 +250,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
||||
if old != ui_state.focus
|
||||
&& let Some(old) = old
|
||||
{
|
||||
old.edit(rsc).deselect();
|
||||
old(rsc).deselect();
|
||||
}
|
||||
match &event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
@@ -315,7 +315,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
||||
&& let Some(sel) = ui_state.focus
|
||||
&& event.state.is_pressed()
|
||||
{
|
||||
let mut text = sel.edit(rsc);
|
||||
let text = sel(rsc);
|
||||
match text.apply_event(event, &ui_state.input.modifiers) {
|
||||
TextInputResult::Unfocus => {
|
||||
ui_state.focus = None;
|
||||
@@ -346,7 +346,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
|
||||
if !rsc.events.controllers.command_target_blocks_input()
|
||||
&& let Some(sel) = ui_state.focus
|
||||
{
|
||||
let mut text = sel.edit(rsc);
|
||||
let text = sel(rsc);
|
||||
match ime {
|
||||
Ime::Enabled | Ime::Disabled => (),
|
||||
Ime::Preedit(content, _pos) => {
|
||||
|
||||
+3
-5
@@ -467,10 +467,8 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
|
||||
|
||||
render.resize((1080.0, 2298.0));
|
||||
render.update(&root, &mut rsc);
|
||||
field
|
||||
.edit(&mut rsc)
|
||||
.select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
|
||||
field.edit(&mut rsc).insert("a");
|
||||
field(&mut rsc).select(vec2(40.0, 2250.0), vec2(1080.0, 2298.0), false, false);
|
||||
field(&mut rsc).insert("a");
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
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.update(&root, &mut rsc);
|
||||
field.edit(&mut rsc).insert("b");
|
||||
field(&mut rsc).insert("b");
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
let after_px = render.window_region(&field, &rsc).unwrap();
|
||||
|
||||
+14
-14
@@ -127,29 +127,29 @@ fn on_press(
|
||||
match sense {
|
||||
CursorSense::PressStart(_) => {
|
||||
let recent = state.recent_click();
|
||||
id.edit(rsc).text.press_origin = Some(pos);
|
||||
id.edit(rsc).select(pos, size, false, recent);
|
||||
id(rsc).press_origin = Some(pos);
|
||||
id(rsc).select(pos, size, false, recent);
|
||||
}
|
||||
CursorSense::Pressing(_) | CursorSense::PressEnd(_) => {
|
||||
let mut ctx = id.edit(rsc);
|
||||
let Some(origin) = ctx.text.press_origin else {
|
||||
let ctx = id(rsc);
|
||||
let Some(origin) = ctx.press_origin else {
|
||||
return;
|
||||
};
|
||||
let (dx, dy) = (pos.x - origin.x, pos.y - origin.y);
|
||||
if dy.abs() > DRAG_SLOP && dy.abs() >= dx.abs() {
|
||||
ctx.text.press_origin = None;
|
||||
ctx.press_origin = None;
|
||||
return;
|
||||
}
|
||||
let ended = matches!(sense, CursorSense::PressEnd(_));
|
||||
if ended {
|
||||
ctx.text.press_origin = None;
|
||||
ctx.press_origin = None;
|
||||
}
|
||||
ctx.select(pos, size, true, false);
|
||||
if ended && dx.abs() <= DRAG_SLOP && dy.abs() <= DRAG_SLOP {
|
||||
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;
|
||||
@@ -157,24 +157,24 @@ fn on_press(
|
||||
|
||||
match sense {
|
||||
CursorSense::PressStart(_) => {
|
||||
id.edit(rsc).text.press_origin = Some(pos);
|
||||
id(rsc).press_origin = Some(pos);
|
||||
}
|
||||
CursorSense::Pressing(_) => {
|
||||
let ctx = id.edit(rsc);
|
||||
if let Some(origin) = ctx.text.press_origin
|
||||
let ctx = id(rsc);
|
||||
if let Some(origin) = ctx.press_origin
|
||||
&& ((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
|
||||
// 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(_) => {
|
||||
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 {
|
||||
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.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);
|
||||
buf.set_spans(builder.spans);
|
||||
let hint = builder.hint.get(state);
|
||||
let mut text = Text {
|
||||
content: builder.content.into(),
|
||||
view: TextView::new(buf, builder.attrs, hint),
|
||||
};
|
||||
text.content.changed = false;
|
||||
text
|
||||
let resources = state.ui().text.clone();
|
||||
Text {
|
||||
view: TextView::new(TextResources::add(resources, buf, builder.attrs), hint),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +111,12 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
|
||||
) -> Self::Output {
|
||||
let mut buf = TextBuffer::new(&builder.content);
|
||||
buf.set_spans(builder.spans);
|
||||
let resources = state.ui().text.clone();
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
+132
-148
@@ -1,9 +1,10 @@
|
||||
use crate::prelude::*;
|
||||
use iris_core::{PaintId, TextData};
|
||||
use iris_core::PaintId;
|
||||
use parley::{Affinity, Layout, Selection};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::selection_layout;
|
||||
use std::{
|
||||
cell::Ref,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use winit::{
|
||||
event::KeyEvent,
|
||||
@@ -47,15 +48,15 @@ impl TextEdit {
|
||||
}
|
||||
|
||||
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
|
||||
/// 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
|
||||
/// knowing about that encoding.
|
||||
pub fn text(&self) -> &str {
|
||||
self.view.buf.text()
|
||||
pub fn text(&self) -> Ref<'_, str> {
|
||||
self.view.text.text()
|
||||
}
|
||||
|
||||
/// The selection as a byte range, collapsed to `caret..caret` when
|
||||
@@ -89,21 +90,15 @@ impl Widget for TextEdit {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextEditCtx<'a> {
|
||||
pub text: &'a mut TextEdit,
|
||||
pub data: &'a mut TextData,
|
||||
}
|
||||
|
||||
impl<'a> TextEditCtx<'a> {
|
||||
impl TextEdit {
|
||||
fn selection_ctx(&mut self) -> TextSelectionCtx<'_> {
|
||||
TextSelectionCtx {
|
||||
view: &mut self.text.view,
|
||||
data: self.data,
|
||||
view: &mut self.view,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&mut self) -> &Layout<iris_core::PaintId> {
|
||||
selection_layout(&mut self.text.view, self.data)
|
||||
fn layout(&self) -> Ref<'_, Layout<iris_core::PaintId>> {
|
||||
self.view.text.layout()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||
@@ -112,44 +107,44 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
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("");
|
||||
text
|
||||
}
|
||||
|
||||
pub fn set(&mut self, text: &str) {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.text.view.selection.deselect();
|
||||
self.view.text.set_text(text);
|
||||
self.view.selection.deselect();
|
||||
}
|
||||
|
||||
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.set_spans(spans);
|
||||
self.text.view.selection.deselect();
|
||||
self.view.text.set_text(text);
|
||||
self.view.text.set_spans(spans);
|
||||
self.view.selection.deselect();
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
let layout = self.layout();
|
||||
let sel = if !select && !sel.is_collapsed() {
|
||||
match motion {
|
||||
Motion::Left | Motion::LeftWord => {
|
||||
Selection::from(sel.text_range().start_cursor(layout))
|
||||
Selection::from(sel.text_range().start_cursor(&layout))
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
@@ -161,7 +156,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
fn string(&self, text: &str) -> String {
|
||||
if self.text.mode == EditMode::SingleLine {
|
||||
if self.mode == EditMode::SingleLine {
|
||||
text.replace('\n', "")
|
||||
} else {
|
||||
text.to_string()
|
||||
@@ -178,7 +173,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
return;
|
||||
}
|
||||
self.clear_span();
|
||||
let at = match self.text.view.selection.range {
|
||||
let at = match self.view.selection.range {
|
||||
Some(sel) => sel.focus().index(),
|
||||
// No caret means nowhere to put the text, so this drops the
|
||||
// keystroke -- which is invisible, and was the whole of the
|
||||
@@ -196,22 +191,24 @@ impl<'a> TextEditCtx<'a> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let at = at.min(self.text.view.buf.text().len());
|
||||
self.text.view.buf.edit().insert_str(at, text);
|
||||
self.text.view.buf.changed = true;
|
||||
let at = at.min(self.view.text.text().len());
|
||||
self.view
|
||||
.text
|
||||
.edit_text(|content| content.insert_str(at, text));
|
||||
self.set_caret(at + text.len());
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
if sel.is_collapsed() {
|
||||
return false;
|
||||
}
|
||||
let range = sel.text_range();
|
||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.view
|
||||
.text
|
||||
.edit_text(|content| content.replace_range(range.clone(), ""));
|
||||
self.set_caret(range.start);
|
||||
true
|
||||
}
|
||||
@@ -221,7 +218,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
pub fn newline(&mut self) {
|
||||
if self.text.mode == EditMode::MultiLine {
|
||||
if self.mode == EditMode::MultiLine {
|
||||
self.insert_str("\n");
|
||||
}
|
||||
}
|
||||
@@ -230,7 +227,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
}
|
||||
let Some(sel) = self.text.view.selection.range else {
|
||||
let Some(sel) = self.view.selection.range else {
|
||||
return;
|
||||
};
|
||||
let end = sel.focus().index();
|
||||
@@ -239,10 +236,11 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
let layout = self.layout();
|
||||
let start = if word {
|
||||
sel.focus().previous_logical_word(layout).index()
|
||||
sel.focus().previous_logical_word(&layout).index()
|
||||
} else {
|
||||
sel.focus().previous_visual(layout).index()
|
||||
sel.focus().previous_visual(&layout).index()
|
||||
};
|
||||
drop(layout);
|
||||
self.delete_range(start, end);
|
||||
}
|
||||
|
||||
@@ -250,30 +248,32 @@ impl<'a> TextEditCtx<'a> {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
}
|
||||
let Some(sel) = self.text.view.selection.range else {
|
||||
let Some(sel) = self.view.selection.range else {
|
||||
return;
|
||||
};
|
||||
let start = sel.focus().index();
|
||||
if start >= self.text.view.buf.text().len() {
|
||||
if start >= self.view.text.text().len() {
|
||||
return;
|
||||
}
|
||||
let layout = self.layout();
|
||||
let end = if word {
|
||||
sel.focus().next_logical_word(layout).index()
|
||||
sel.focus().next_logical_word(&layout).index()
|
||||
} else {
|
||||
sel.focus().next_visual(layout).index()
|
||||
sel.focus().next_visual(&layout).index()
|
||||
};
|
||||
drop(layout);
|
||||
self.delete_range(start, end);
|
||||
}
|
||||
|
||||
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));
|
||||
if start == end {
|
||||
return;
|
||||
}
|
||||
self.text.view.buf.edit().replace_range(start..end, "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.view
|
||||
.text
|
||||
.edit_text(|content| content.replace_range(start..end, ""));
|
||||
self.set_caret(start);
|
||||
}
|
||||
|
||||
@@ -326,20 +326,17 @@ impl<'a> TextEditCtx<'a> {
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||
let old = (
|
||||
self.text.view.buf.text().to_string(),
|
||||
self.text.view.selection.range,
|
||||
);
|
||||
let old = (self.view.text.text().to_string(), self.view.selection.range);
|
||||
let mut undo = false;
|
||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||
if undo {
|
||||
if let Some((old, selection)) = self.text.history.pop() {
|
||||
if let Some((old, selection)) = self.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.view.selection.range = selection;
|
||||
self.view.selection.range = selection;
|
||||
self.refresh();
|
||||
}
|
||||
} else if self.text.view.buf.text() != old.0 {
|
||||
self.text.history.push(old);
|
||||
} else if self.view.text.text().as_ref() != old.0 {
|
||||
self.history.push(old);
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -394,12 +391,12 @@ impl<'a> TextEditCtx<'a> {
|
||||
match text.as_str() {
|
||||
"v" => return TextInputResult::Paste,
|
||||
"c" => {
|
||||
if let Some(content) = self.text.selected_text() {
|
||||
if let Some(content) = self.selected_text() {
|
||||
return TextInputResult::Copy(content);
|
||||
}
|
||||
}
|
||||
"x" => {
|
||||
if let Some(content) = self.text.selected_text() {
|
||||
if let Some(content) = self.selected_text() {
|
||||
self.clear_span();
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use iris_core::{TextAttrs, TextBuffer};
|
||||
use iris_core::{TextAttrs, TextBuffer, TextResources};
|
||||
|
||||
fn edit(text: &str, mode: EditMode) -> (TextEdit, TextData) {
|
||||
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
|
||||
(TextEdit::new(view, mode), TextData::default())
|
||||
fn edit(text: &str, mode: EditMode) -> TextEdit {
|
||||
let resources = std::rc::Rc::new(std::cell::RefCell::new(TextResources::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> {
|
||||
TextEditCtx { text, data }
|
||||
fn ctx(text: &mut TextEdit) -> &mut TextEdit {
|
||||
text
|
||||
}
|
||||
|
||||
fn content(text: &TextEdit) -> String {
|
||||
text.buf.text().to_string()
|
||||
text.text().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_at_the_caret() {
|
||||
let (mut t, mut d) = edit("ac", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(1);
|
||||
ctx(&mut t, &mut d).insert("b");
|
||||
let mut t = edit("ac", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(1);
|
||||
ctx(&mut t).insert("b");
|
||||
assert_eq!(content(&t), "abc");
|
||||
assert_eq!(t.caret(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_removes_the_character_before_the_caret() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(2);
|
||||
ctx(&mut t, &mut d).backspace(false);
|
||||
let mut t = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(2);
|
||||
ctx(&mut t).backspace(false);
|
||||
assert_eq!(content(&t), "ac");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_at_the_start_does_nothing() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).backspace(false);
|
||||
let mut t = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(0);
|
||||
ctx(&mut t).backspace(false);
|
||||
assert_eq!(content(&t), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_removes_the_character_after_the_caret() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(1);
|
||||
ctx(&mut t, &mut d).delete(false);
|
||||
let mut t = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(1);
|
||||
ctx(&mut t).delete(false);
|
||||
assert_eq!(content(&t), "ac");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_at_the_end_does_nothing() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(3);
|
||||
ctx(&mut t, &mut d).delete(false);
|
||||
let mut t = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(3);
|
||||
ctx(&mut t).delete(false);
|
||||
assert_eq!(content(&t), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all_then_typing_replaces_everything() {
|
||||
let (mut t, mut d) = edit("hello", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
let mut t = edit("hello", EditMode::SingleLine);
|
||||
ctx(&mut t).select_all();
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_a_span_leaves_the_caret_at_its_start() {
|
||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
assert!(ctx(&mut t, &mut d).clear_span());
|
||||
let mut t = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t).select_all();
|
||||
assert!(ctx(&mut t).clear_span());
|
||||
assert_eq!(content(&t), "");
|
||||
assert_eq!(t.caret(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
|
||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
|
||||
let mut t = edit("", EditMode::MultiLine);
|
||||
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");
|
||||
ctx(&mut t, &mut d).insert("hi");
|
||||
ctx(&mut t).insert("hi");
|
||||
assert_eq!(content(&t), "hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
||||
let mut t = edit("abc", EditMode::MultiLine);
|
||||
ctx(&mut t).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
||||
assert_eq!(t.caret(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_without_a_previous_selection_selects_nothing() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
||||
let mut t = edit("abc", EditMode::MultiLine);
|
||||
ctx(&mut t).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
||||
assert!(t.selection_range().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_line_field_refuses_newlines() {
|
||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).insert("a\nb");
|
||||
let mut t = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(0);
|
||||
ctx(&mut t).insert("a\nb");
|
||||
assert_eq!(content(&t), "ab");
|
||||
ctx(&mut t, &mut d).newline();
|
||||
ctx(&mut t).newline();
|
||||
assert_eq!(content(&t), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_multi_line_field_keeps_newlines() {
|
||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).insert("a\nb");
|
||||
let mut t = edit("", EditMode::MultiLine);
|
||||
ctx(&mut t).set_caret(0);
|
||||
ctx(&mut t).insert("a\nb");
|
||||
assert_eq!(content(&t), "a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_empties_the_field_and_hands_back_what_was_there() {
|
||||
let (mut t, mut d) = edit("some text", EditMode::SingleLine);
|
||||
assert_eq!(ctx(&mut t, &mut d).take(), "some text");
|
||||
let mut t = edit("some text", EditMode::SingleLine);
|
||||
assert_eq!(ctx(&mut t).take(), "some text");
|
||||
assert_eq!(content(&t), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ime_preedit_replaces_its_own_previous_text() {
|
||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).replace(0, "n");
|
||||
let mut t = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(0);
|
||||
ctx(&mut t).replace(0, "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");
|
||||
ctx(&mut t, &mut d).replace(2, "に");
|
||||
ctx(&mut t).replace(2, "に");
|
||||
assert_eq!(content(&t), "に");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composing_advances_the_caret_with_the_growing_text() {
|
||||
let (mut t, mut d) = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).replace(0, "h");
|
||||
let mut t = edit("", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(0);
|
||||
ctx(&mut t).replace(0, "h");
|
||||
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!(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!(t.caret(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committing_composed_text_leaves_it_in_place_with_the_caret_after_it() {
|
||||
let (mut t, mut d) = edit("say ", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(4);
|
||||
ctx(&mut t, &mut d).replace(0, "hi");
|
||||
let mut t = edit("say ", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(4);
|
||||
ctx(&mut t).replace(0, "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!(t.caret(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_byte_range_removes_exactly_that_range() {
|
||||
let (mut t, mut d) = edit("hello world", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).delete_byte_range(5, 11);
|
||||
let mut t = edit("hello world", EditMode::SingleLine);
|
||||
ctx(&mut t).delete_byte_range(5, 11);
|
||||
assert_eq!(content(&t), "hello");
|
||||
assert_eq!(t.caret(), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_cursor_byte_collapses_to_a_caret_there() {
|
||||
let (mut t, mut d) = edit("hello world", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).set_cursor_byte(5);
|
||||
let mut t = edit("hello world", EditMode::SingleLine);
|
||||
ctx(&mut t).select_all();
|
||||
ctx(&mut t).set_cursor_byte(5);
|
||||
assert_eq!(t.selected_text(), None);
|
||||
assert_eq!(t.caret(), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_moves_the_caret_and_shift_extends_a_span() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||
let mut t = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(0);
|
||||
ctx(&mut t).motion(Motion::Right, false);
|
||||
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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unshifted_arrow_collapses_a_span_to_its_edge() {
|
||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).motion(Motion::Left, false);
|
||||
let mut t = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t).select_all();
|
||||
ctx(&mut t).motion(Motion::Left, false);
|
||||
assert_eq!(t.caret(), Some(0));
|
||||
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||
ctx(&mut t).select_all();
|
||||
ctx(&mut t).motion(Motion::Right, false);
|
||||
assert_eq!(t.caret(), Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multibyte_text_is_edited_by_byte_offset() {
|
||||
let (mut t, mut d) = edit("aé", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(3);
|
||||
ctx(&mut t, &mut d).backspace(false);
|
||||
let mut t = edit("aé", EditMode::SingleLine);
|
||||
ctx(&mut t).set_caret(3);
|
||||
ctx(&mut t).backspace(false);
|
||||
assert_eq!(content(&t), "a");
|
||||
}
|
||||
}
|
||||
+58
-107
@@ -4,87 +4,65 @@ mod selection;
|
||||
|
||||
pub use build::*;
|
||||
pub use edit::*;
|
||||
use iris_core::util::MutDetect;
|
||||
pub use selection::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub struct Text {
|
||||
pub content: MutDetect<String>,
|
||||
view: TextView,
|
||||
}
|
||||
|
||||
pub struct TextView {
|
||||
pub attrs: MutDetect<TextAttrs>,
|
||||
pub buf: MutDetect<TextBuffer>,
|
||||
tex: Option<RenderedText>,
|
||||
width: Option<f32>,
|
||||
text: TextHandle,
|
||||
pub hint: Option<StrongWidget>,
|
||||
selection: TextSelection,
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
fn is_blank(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
self.text.text().is_empty()
|
||||
}
|
||||
|
||||
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 {
|
||||
attrs: attrs.into(),
|
||||
buf: buf.into(),
|
||||
tex: None,
|
||||
width: None,
|
||||
text,
|
||||
hint,
|
||||
selection: TextSelection::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn region(&self) -> UiRegion {
|
||||
self.tex()
|
||||
let align = self.text.attrs().align;
|
||||
self.text
|
||||
.rendered()
|
||||
.map(|t| t.size)
|
||||
.unwrap_or(Vec2::ZERO)
|
||||
.align(self.align)
|
||||
.align(align)
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let generation = painter.atlas_generation();
|
||||
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);
|
||||
let tex = painter.render_text(&self.text, width);
|
||||
if crate::diagnostics::trace_enabled() {
|
||||
log::debug!(
|
||||
target: "iris::frame",
|
||||
"iris text render: chars={} width={width:?} glyphs={} size={:?}",
|
||||
self.buf.text().chars().count(),
|
||||
self.text.text().chars().count(),
|
||||
tex.glyphs.len(),
|
||||
tex.size,
|
||||
);
|
||||
}
|
||||
self.tex = Some(tex.clone());
|
||||
self.attrs.changed = false;
|
||||
self.buf.changed = false;
|
||||
tex
|
||||
}
|
||||
pub fn tex(&self) -> Option<&RenderedText> {
|
||||
self.tex.as_ref()
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let tex = self.render(painter);
|
||||
if self.is_blank()
|
||||
@@ -92,7 +70,7 @@ impl TextView {
|
||||
{
|
||||
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());
|
||||
painter.glyphs(&tex, within);
|
||||
Size::abs(tex.size)
|
||||
@@ -108,8 +86,10 @@ impl TextView {
|
||||
let Some(selection) = self.selection.range else {
|
||||
return used;
|
||||
};
|
||||
let layout = self.buf.layout();
|
||||
for (rect, _) in selection.geometry(layout) {
|
||||
let geometry = self
|
||||
.text
|
||||
.with_layout(|layout, _| selection.geometry(layout));
|
||||
for (rect, _) in geometry {
|
||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||
let paint = painter.paint(&PaintId::SKY);
|
||||
@@ -119,7 +99,9 @@ impl TextView {
|
||||
);
|
||||
}
|
||||
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 top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||
let paint = painter.paint(&PaintId::WHITE);
|
||||
@@ -132,28 +114,23 @@ impl TextView {
|
||||
}
|
||||
|
||||
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 {
|
||||
pub fn new(content: impl Into<String>) -> Self {
|
||||
let content: String = content.into();
|
||||
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());
|
||||
pub fn set_text(&mut self, content: impl Into<String>) {
|
||||
if self.view.text.set_text(content) {
|
||||
self.view.selection.deselect();
|
||||
}
|
||||
}
|
||||
|
||||
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>> {
|
||||
@@ -161,18 +138,22 @@ impl Text {
|
||||
}
|
||||
|
||||
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
|
||||
let content = content.into();
|
||||
*self.content = content.clone();
|
||||
self.content.changed = false;
|
||||
self.view.buf.set_text(content);
|
||||
self.view.buf.set_spans(spans);
|
||||
self.view.text.set_text(content);
|
||||
self.view.text.set_spans(spans);
|
||||
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 {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
self.update_buf();
|
||||
let size = if self.view.selection.range.is_some() {
|
||||
self.view.draw_selectable(painter, false)
|
||||
} else {
|
||||
@@ -188,34 +169,6 @@ impl Widget for Text {
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use crate::layout_tests::TestRsc;
|
||||
@@ -237,20 +190,20 @@ mod tests {
|
||||
let (mut rsc, _render, text, _root) = rendered_text("hello there");
|
||||
text.selection(&mut rsc).select_all();
|
||||
|
||||
let view = TextView::new(TextBuffer::new("hello there"), TextAttrs::default(), None);
|
||||
let mut edit = TextEdit::new(view, EditMode::MultiLine);
|
||||
let mut data = TextData::default();
|
||||
TextEditCtx {
|
||||
text: &mut edit,
|
||||
data: &mut data,
|
||||
}
|
||||
.select_all();
|
||||
let edit = wtext("hello there")
|
||||
.editable(EditMode::MultiLine)
|
||||
.add_strong(&mut rsc);
|
||||
let edit_id = edit.weak();
|
||||
edit_id(&mut rsc).select_all();
|
||||
|
||||
assert_eq!(
|
||||
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]
|
||||
@@ -262,7 +215,7 @@ mod tests {
|
||||
Some("before")
|
||||
);
|
||||
|
||||
*rsc.ui.widgets[text].content = "after".to_string();
|
||||
rsc.ui.widgets[text].set_text("after");
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
assert_eq!(rsc.ui.widgets[text].selected_text(), None);
|
||||
@@ -277,17 +230,15 @@ mod tests {
|
||||
render.update(&root, &mut rsc);
|
||||
let selected = render.active[&text.id()].primitives.len();
|
||||
|
||||
let view = TextView::new(TextBuffer::new("selected"), TextAttrs::default(), None);
|
||||
let edit = rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(TextEdit::new(view, EditMode::MultiLine));
|
||||
let edit = wtext("selected")
|
||||
.editable(EditMode::MultiLine)
|
||||
.add_strong(&mut rsc);
|
||||
let edit_id = edit.weak();
|
||||
let edit_root = edit.any();
|
||||
let mut edit_render = UiRenderState::new();
|
||||
edit_render.resize((800.0, 600.0));
|
||||
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);
|
||||
let editable = edit_render.active[&edit_id.id()].primitives.len();
|
||||
|
||||
@@ -314,16 +265,16 @@ mod tests {
|
||||
render.resize((800.0, 600.0));
|
||||
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");
|
||||
|
||||
rsc.ui.text.atlas.clear();
|
||||
assert_eq!(rsc.ui.text.atlas.glyph_count(), 0);
|
||||
rsc.ui.text.borrow_mut().atlas.clear();
|
||||
assert_eq!(rsc.ui.text.borrow().atlas.glyph_count(), 0);
|
||||
render.resize((800.0, 600.0));
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
assert_eq!(
|
||||
rsc.ui.text.atlas.glyph_count(),
|
||||
rsc.ui.text.borrow().atlas.glyph_count(),
|
||||
rasterised,
|
||||
"the second frame re-emitted its cached glyphs instead of \
|
||||
re-rendering them against the fresh atlas"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
use iris_core::{PaintId, TextData};
|
||||
use parley::{Affinity, Layout, Selection as ParleySelection};
|
||||
use parley::{Affinity, Selection as ParleySelection};
|
||||
use std::time::Instant;
|
||||
|
||||
/// 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
|
||||
/// iris's text resources. `TextEditCtx` delegates to this same context rather
|
||||
/// than maintaining an editable-only copy of the geometry and hit testing.
|
||||
/// Selection operations shared by ordinary and editable text, using the
|
||||
/// widget's arena-backed layout rather than maintaining an editable-only copy
|
||||
/// of the geometry and hit testing.
|
||||
pub struct TextSelectionCtx<'a> {
|
||||
pub(super) view: &'a mut TextView,
|
||||
pub(super) data: &'a mut TextData,
|
||||
}
|
||||
|
||||
impl TextSelectionCtx<'_> {
|
||||
pub(super) fn layout(&mut self) -> &Layout<PaintId> {
|
||||
selection_layout(self.view, self.data)
|
||||
}
|
||||
|
||||
pub(crate) fn refresh(&mut self) {
|
||||
if let Some(selection) = self.view.selection.range {
|
||||
let layout = self.layout();
|
||||
self.view.selection.range = Some(selection.refresh(layout));
|
||||
self.view.selection.range = Some(
|
||||
self.view
|
||||
.text
|
||||
.with_layout(|layout, _| selection.refresh(layout)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,21 +56,23 @@ impl TextSelectionCtx<'_> {
|
||||
/// the same widget-local coordinates as a `CursorSense` event.
|
||||
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
||||
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)
|
||||
.focus()
|
||||
.index()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
let len = self.view.buf.text().len();
|
||||
let len = self.view.text.text().len();
|
||||
if len == 0 {
|
||||
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 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) {
|
||||
@@ -81,8 +80,7 @@ impl TextSelectionCtx<'_> {
|
||||
let previous = self.view.selection.range;
|
||||
let previous_hit = self.view.selection.double_hit;
|
||||
|
||||
let outcome = {
|
||||
let layout = self.layout();
|
||||
let outcome = self.view.text.with_layout(|layout, _| {
|
||||
if drag {
|
||||
previous.map(|selection| {
|
||||
(
|
||||
@@ -109,7 +107,7 @@ impl TextSelectionCtx<'_> {
|
||||
(Some(hit), None)
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if let Some((range, double_hit)) = outcome {
|
||||
self.view.selection.range = range;
|
||||
@@ -122,39 +120,28 @@ impl TextSelectionCtx<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn set_caret(&mut self, index: usize) {
|
||||
let index = index.min(self.view.buf.text().len());
|
||||
let layout = self.layout();
|
||||
self.view.selection.range = Some(ParleySelection::from_byte_index(
|
||||
layout,
|
||||
index,
|
||||
Affinity::default(),
|
||||
));
|
||||
let index = index.min(self.view.text.text().len());
|
||||
self.view.selection.range = Some(self.view.text.with_layout(|layout, _| {
|
||||
ParleySelection::from_byte_index(layout, index, Affinity::default())
|
||||
}));
|
||||
}
|
||||
|
||||
fn select_between(&mut self, anchor: usize, focus: usize) {
|
||||
let len = self.view.buf.text().len();
|
||||
let layout = self.layout();
|
||||
let anchor = parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default());
|
||||
let focus = parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default());
|
||||
self.view.selection.range = Some(ParleySelection::new(anchor, focus));
|
||||
let len = self.view.text.text().len();
|
||||
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 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
|
||||
/// `TextEditCtx`. Gesture policy is intentionally not part of this trait; a
|
||||
/// selection controller and an editor's focus handler do different
|
||||
/// things with the same mechanics.
|
||||
/// `TextEdit`. Gesture policy is intentionally not part of this trait; a
|
||||
/// selection controller and an editor's focus handler do different things
|
||||
/// with the same mechanics.
|
||||
pub trait TextSelectable {
|
||||
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();
|
||||
TextSelectionCtx {
|
||||
view: &mut ui.widgets.get_mut(self).unwrap().view,
|
||||
data: &mut ui.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,10 +242,8 @@ impl SelectionController {
|
||||
.get_dyn_mut(id)?
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Text>()?;
|
||||
text.update_buf();
|
||||
let mut ctx = TextSelectionCtx {
|
||||
view: &mut text.view,
|
||||
data: &mut ui.text,
|
||||
};
|
||||
Some(f(&mut ctx))
|
||||
}
|
||||
@@ -334,7 +318,7 @@ impl SelectionController {
|
||||
for &text_id in &self.order[lo..=hi] {
|
||||
let forward = anchor_at <= focus_at;
|
||||
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 {
|
||||
(anchor_byte, focus_byte)
|
||||
} else if text_id == anchor {
|
||||
|
||||
Reference in new issue
Block a user