Skip to content

Commit

Permalink
TextReader/TextWriter WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
UkoeHB committed Oct 3, 2024
1 parent cd6dc92 commit 99b9678
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 283 deletions.
6 changes: 3 additions & 3 deletions crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ mod glyph;
mod pipeline;
mod text;
mod text2d;
mod text_blocks;
mod text_access;

pub use cosmic_text;

Expand All @@ -57,7 +57,7 @@ pub use glyph::*;
pub use pipeline::*;
pub use text::*;
pub use text2d::*;
pub use text_blocks::*;
pub use text_access::*;

/// The text prelude.
///
Expand Down Expand Up @@ -117,7 +117,7 @@ impl Plugin for TextPlugin {
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>()
.init_resource::<TextSpansScratch>()
.init_resource::<TextIterScratch>()
.add_systems(
PostUpdate,
(
Expand Down
16 changes: 11 additions & 5 deletions crates/bevy_text/src/text2d.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::pipeline::CosmicFontSystem;
use crate::{
ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBlock,
TextBlocks, TextBounds, TextError, TextLayoutInfo, TextPipeline, TextSpanReader, TextStyle,
TextBlocks, TextBounds, TextError, TextLayoutInfo, TextPipeline, TextSpanAccess, TextStyle,
YAxisOrientation,
};
use bevy_asset::Assets;
Expand Down Expand Up @@ -94,10 +94,13 @@ impl Text2d {
}
}

impl TextSpanReader for Text2d {
impl TextSpanAccess for Text2d {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}

impl From<&str> for Text2d {
Expand Down Expand Up @@ -150,10 +153,13 @@ world.spawn((
#[require(TextStyle, Visibility(visibility_hidden), Transform)]
pub struct TextSpan2d(pub String);

impl TextSpanReader for TextSpan2d {
impl TextSpanAccess for TextSpan2d {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}

fn visibility_hidden() -> Visibility {
Expand Down Expand Up @@ -382,7 +388,7 @@ mod tests {
use bevy_asset::{load_internal_binary_asset, Handle};
use bevy_ecs::{event::Events, schedule::IntoSystemConfigs};

use crate::{detect_text_needs_rerender, TextSpansScratch};
use crate::{detect_text_needs_rerender, TextIterScratch};

use super::*;

Expand All @@ -399,7 +405,7 @@ mod tests {
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>()
.init_resource::<TextSpansScratch>()
.init_resource::<TextIterScratch>()
.add_systems(
Update,
(
Expand Down
259 changes: 259 additions & 0 deletions crates/bevy_text/src/text_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
use bevy_ecs::{prelude::*, system::{Query, SystemParam}};
use bevy_hierarchy::Children;

use crate::TextStyle;

/// Helper trait for using the [`TextReader`] system param.
pub trait TextSpanAccess: Component {
/// Gets the text span's string.
fn read_span(&self) -> &str;
/// Gets mutable reference to the text span's string.
fn write_span(&mut self) -> &mut String;
}

#[derive(Resource, Default)]
pub(crate) struct TextIterScratch {
stack: Vec<(&'static Children, usize)>,
}

/// System parameter for reading text spans in a [`TextBlock`].
///
/// `R` is the root text component, and `S` is the text span component on children.
#[derive(SystemParam)]
pub struct TextReader<'w, 's, R: TextSpanAccess, S: TextSpanAccess> {
scratch: ResMut<'w, TextIterScratch>,
roots: Query<'w, 's, (&'static R, &'static TextStyle, Option<&'static Children>)>,
spans: Query<
'w,
's,
(
Entity,
&'static S,
&'static TextStyle,
Option<&'static Children>,
),
>,
}

impl<'w, 's, R: TextSpanAccess, S: TextSpanAccess> TextReader<'w, 's, R, S> {
/// Returns an iterator over text spans in a text block, starting with the root entity.
pub fn iter<'a>(&'a mut self, root_entity: Entity) -> TextSpanIter<'a, R, S> {
let stack = core::mem::take(&mut self.scratch.stack)
.into_iter()
.map(|_| -> (&Children, usize) { unreachable!() })
.collect();

TextSpanIter {
scratch: &mut self.scratch,
root_entity: Some(root_entity),
stack,
roots: &self.roots,
spans: &self.spans,
}
}

/// Gets a text span within a text block at a specific index in the flattened span list.
pub fn get_by_index<'a>(&'a mut self, root_entity: Entity, index: usize) -> Option<(&'a str, &'a TextStyle)> {
self.iter(root_entity).nth(index).map(|(_, _, text, style)| (text, style))
}
}

/// System parameter for reading and writing text spans in a [`TextBlock`].
///
/// `R` is the root text component, and `S` is the text span component on children.
#[derive(SystemParam)]
pub struct TextWriter<'w, 's, R: TextSpanAccess, S: TextSpanAccess> {
scratch: ResMut<'w, TextIterScratch>,
roots: Query<'w, 's, (&'static mut R, &'static mut TextStyle), Without<S>>,
spans: Query<
'w,
's,
(
Entity,
&'static mut S,
&'static mut TextStyle,
),
Without<R>
>,
children: Query<'w, 's, &'static Children>,
}

impl<'w, 's, R: TextSpanAccess, S: TextSpanAccess> TextWriter<'w, 's, R, S> {
/// Returns a mutatable iterator over text spans in a text block, starting with the root entity.
pub fn iter<'a>(&'a mut self, root_entity: Entity) -> TextSpanIterMut<'a, R, S> {
let stack = core::mem::take(&mut self.scratch.stack)
.into_iter()
.map(|_| -> (&Children, usize) { unreachable!() })
.collect();

TextSpanIterMut {
scratch: &mut self.scratch,
root_entity: Some(root_entity),
stack,
roots: &mut self.roots,
spans: &mut self.spans,
children: &self.children,
}
}

/// Gets a mutable reference to a text span within a text block at a specific index in the flattened span list.
pub fn get_by_index<'a>(&'a mut self, root_entity: Entity, index: usize) -> Option<(Mut<'a, String>, Mut<'a, TextStyle>)> {
self.iter(root_entity).nth(index).map(|(_, _, text, style)| (text, style))
}
}

/// Iterator returned by [`TextReader::iter`] and [`TextWriter::iter`].
///
/// Iterates all spans in a text block according to hierarchy traversal order.
/// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed.
// TODO: Use this iterator design in UiChildrenIter to reduce allocations.
pub struct TextSpanIter<'a, R: TextSpanAccess, S: TextSpanAccess> {
scratch: &'a mut TextIterScratch,
root_entity: Option<Entity>,
/// Stack of (children, next index into children).
stack: Vec<(&'a Children, usize)>,
roots: &'a Query<'a, 'a, (&'static R, &'static TextStyle, Option<&'static Children>)>,
spans: &'a Query<
'a,
'a,
(
Entity,
&'static S,
&'static TextStyle,
Option<&'static Children>,
),
>,
}

impl<'a, R: TextSpanAccess, S: TextSpanAccess> Iterator for TextSpanIter<'a, R, S> {
/// Item = (entity in text block, hierarchy depth in the block, span text, span style).
type Item = (Entity, usize, &'a str, &'a TextStyle);
fn next(&mut self) -> Option<Self::Item> {
// Root
if let Some(root_entity) = self.root_entity.take() {
if let Ok((text, style, maybe_children)) = self.roots.get(root_entity) {
if let Some(children) = maybe_children {
self.stack.push((children, 0));
}
return Some((root_entity, 0, text.read_span(), style));
} else {
return None;
}
}

// Span
loop {
let Some((children, idx)) = self.stack.last_mut() else {
return None;
};

loop {
let Some(child) = children.get(*idx) else {
break;
};

// Increment to prep the next entity in this stack level.
*idx += 1;

let Ok((entity, span, style, maybe_children)) = self.spans.get(*child) else {
continue;
};

let depth = self.stack.len();
if let Some(children) = maybe_children {
self.stack.push((children, 0));
}
return Some((entity, depth, span.read_span(), style));
}

// All children at this stack entry have been iterated.
self.stack.pop();
}
}
}

impl<'a, R: TextSpanAccess, S: TextSpanAccess> Drop for TextSpanIter<'a, R, S> {
fn drop(&mut self) {
// Return the internal stack.
let mut stack = std::mem::take(&mut self.stack);
stack.clear();
self.scratch.stack = stack
.into_iter()
.map(|_| -> (&'static Children, usize) { unreachable!() })
.collect();
}
}

/// Iterator returned by [`TextWriter::iter_mut`].
///
/// Iterates all spans in a text block according to hierarchy traversal order.
/// Does *not* flatten interspersed ghost nodes. Only contiguous spans are traversed.
// TODO: Use this iterator design in UiChildrenIter to reduce allocations.
pub struct TextSpanIterMut<'a, R: TextSpanAccess, S: TextSpanAccess> {
scratch: &'a mut TextIterScratch,
root_entity: Option<Entity>,
/// Stack of (children, next index into children).
stack: Vec<(&'a Children, usize)>,
roots: &'a mut Query<'a, 'a, (&'a mut R, &'a mut TextStyle), Without<S>>,
spans: &'a mut Query<'a,'a, (Entity, &'a mut S, &'a mut TextStyle), Without<R>>,
children: &'a Query<'a, 'a, &'a Children>,
}

impl<'a, R: TextSpanAccess, S: TextSpanAccess> Iterator for TextSpanIterMut<'a, R, S> {
/// Item = (entity in text block, hierarchy depth in the block, span text, span style).
type Item = (Entity, usize, Mut<'a, String>, Mut<'a, TextStyle>);
fn next(&mut self) -> Option<Self::Item> {
// Root
if let Some(root_entity) = self.root_entity.take() {
if let Ok((text, style)) = self.roots.get_mut(root_entity) {
if let Ok(children) = self.children.get(root_entity) {
self.stack.push((children, 0));
}
return Some((root_entity, 0, text.map_unchanged(|t| t.write_span()), style));
} else {
return None;
}
}

// Span
loop {
let Some((children, idx)) = self.stack.last_mut() else {
return None;
};

loop {
let Some(child) = children.get(*idx) else {
break;
};

// Increment to prep the next entity in this stack level.
*idx += 1;

let Ok((entity, span, style)) = self.spans.get_mut(*child) else {
continue;
};

let depth = self.stack.len();
if let Ok(children) = self.children.get(entity) {
self.stack.push((children, 0));
}
return Some((entity, depth, span.map_unchanged(|t| t.write_span()), style));
}

// All children at this stack entry have been iterated.
self.stack.pop();
}
}
}

impl<'a, R: TextSpanAccess, S: TextSpanAccess> Drop for TextSpanIterMut<'a, R, S> {
fn drop(&mut self) {
// Return the internal stack.
let mut stack = std::mem::take(&mut self.stack);
stack.clear();
self.scratch.stack = stack
.into_iter()
.map(|_| -> (&'static Children, usize) { unreachable!() })
.collect();
}
}
Loading

0 comments on commit 99b9678

Please sign in to comment.