use crate::{
domain::render_text::{RenderedVimTextSpan, VimTextSpanKind},
ecs::{
components::{buffer::ViewEntity, layout::TextSpanLayout},
schedules::EditorSet,
},
};
use bevy::prelude::{
App, Changed, Color, Commands, Component, DetectChanges, Entity, IntoScheduleConfigs, Plugin,
Query, Ref, TextBackgroundColor, TextColor, TextSpan, Update,
};
use crate::features::ui::style::{
cursor_color, cursor_text_color, scene_text_color, search_match_color, selection_color,
};
type TextLayoutFilter = Changed<TextSpanLayout>;
#[derive(Clone, Copy, Component, Debug, Eq, PartialEq)]
pub struct ManagedText {
pub target: ViewEntity,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct TextRenderPlugin;
impl Plugin for TextRenderPlugin {
fn build(&self, app: &mut App) {
let _app = app.add_systems(Update, sync_managed_text.in_set(EditorSet::Render));
}
}
pub fn sync_managed_text(
mut commands: Commands,
changed_layout_query: Query<&TextSpanLayout, TextLayoutFilter>,
all_layout_query: Query<&TextSpanLayout>,
text_query: Query<(Entity, Ref<ManagedText>)>,
) {
for (entity, managed_text) in &text_query {
if managed_text.is_changed() {
match all_layout_query.get(managed_text.target.get()) {
Ok(span_layout) => sync_text_spans(&mut commands, entity, span_layout),
Err(_error) => clear_text_spans(&mut commands, entity),
}
} else if let Ok(span_layout) = changed_layout_query.get(managed_text.target.get()) {
sync_text_spans(&mut commands, entity, span_layout);
} else if !all_layout_query.contains(managed_text.target.get()) {
clear_text_spans(&mut commands, entity);
}
}
}
fn sync_text_spans(commands: &mut Commands, entity: Entity, span_layout: &TextSpanLayout) {
clear_text_spans(commands, entity);
if span_layout.spans.is_empty() {
return;
}
let _managed_text = commands.entity(entity).with_children(|text| {
spawn_rendered_text_spans(text, &span_layout.spans);
});
}
fn clear_text_spans(commands: &mut Commands, entity: Entity) {
let _managed_text = commands
.entity(entity)
.despawn_related::<bevy::ecs::hierarchy::Children>();
}
fn spawn_rendered_text_spans(
text: &mut bevy::ecs::hierarchy::ChildSpawnerCommands,
spans: &[RenderedVimTextSpan],
) {
for span in spans {
let (text_color, background_color) = match span.kind {
VimTextSpanKind::Normal => (scene_text_color(), Color::NONE),
VimTextSpanKind::SearchMatch => (scene_text_color(), search_match_color()),
VimTextSpanKind::Selection => (scene_text_color(), selection_color()),
VimTextSpanKind::Cursor => (cursor_text_color(), cursor_color()),
};
let _span_entity = text.spawn((
TextSpan::new(span.text.clone()),
TextColor(text_color),
TextBackgroundColor(background_color),
));
}
}
#[cfg(test)]
mod tests {
use super::{ManagedText, TextRenderPlugin};
use crate::{
domain::render_text::{RenderedVimTextSpan, VimTextSpanKind},
ecs::components::{buffer::ViewEntity, layout::TextSpanLayout},
};
use bevy::prelude::{App, Entity};
fn child_entities(app: &bevy::prelude::App, entity: Entity) -> Vec<Entity> {
app.world()
.get::<bevy::ecs::hierarchy::Children>(entity)
.map(|children| children.iter().copied().collect())
.unwrap_or_default()
}
#[test]
fn render_sync_only_rebuilds_when_span_layout_changes() {
let mut app = App::new();
let _app = app.add_plugins(TextRenderPlugin);
let buffer = app
.world_mut()
.spawn((TextSpanLayout {
spans: vec![RenderedVimTextSpan {
text: String::from("hello"),
kind: VimTextSpanKind::Normal,
}],
},))
.id();
let managed_text = app
.world_mut()
.spawn(ManagedText {
target: ViewEntity(buffer),
})
.id();
app.update();
let first_children = child_entities(&app, managed_text);
assert_eq!(first_children.len(), 1);
app.update();
assert_eq!(child_entities(&app, managed_text), first_children);
app.world_mut()
.query::<&mut TextSpanLayout>()
.single_mut(app.world_mut())
.expect("one span layout should exist")
.spans
.push(RenderedVimTextSpan {
text: String::from("!"),
kind: VimTextSpanKind::Cursor,
});
app.update();
let changed_children = child_entities(&app, managed_text);
assert_eq!(changed_children.len(), 2);
assert_ne!(changed_children, first_children);
}
#[test]
fn render_sync_clears_children_when_layout_becomes_empty() {
let mut app = App::new();
let _app = app.add_plugins(TextRenderPlugin);
let buffer = app
.world_mut()
.spawn((TextSpanLayout {
spans: vec![RenderedVimTextSpan {
text: String::from("stale"),
kind: VimTextSpanKind::Normal,
}],
},))
.id();
let managed_text = app
.world_mut()
.spawn(ManagedText {
target: ViewEntity(buffer),
})
.id();
app.update();
assert_eq!(child_entities(&app, managed_text).len(), 1);
app.world_mut()
.get_mut::<TextSpanLayout>(buffer)
.expect("span layout should exist")
.spans
.clear();
app.update();
assert!(child_entities(&app, managed_text).is_empty());
}
#[test]
fn render_sync_clears_children_when_target_layout_is_missing() {
let mut app = App::new();
let _app = app.add_plugins(TextRenderPlugin);
let buffer = app
.world_mut()
.spawn((TextSpanLayout {
spans: vec![RenderedVimTextSpan {
text: String::from("target"),
kind: VimTextSpanKind::Normal,
}],
},))
.id();
let missing_target = app.world_mut().spawn_empty().id();
let managed_text = app
.world_mut()
.spawn(ManagedText {
target: ViewEntity(buffer),
})
.id();
app.update();
assert_eq!(child_entities(&app, managed_text).len(), 1);
app.world_mut()
.get_mut::<ManagedText>(managed_text)
.expect("managed text should exist")
.target = ViewEntity(missing_target);
app.update();
assert!(child_entities(&app, managed_text).is_empty());
}
#[test]
fn render_sync_uses_each_managed_text_target() {
let mut app = App::new();
let _app = app.add_plugins(TextRenderPlugin);
let first_buffer = app
.world_mut()
.spawn((TextSpanLayout {
spans: vec![RenderedVimTextSpan {
text: String::from("first"),
kind: VimTextSpanKind::Normal,
}],
},))
.id();
let second_buffer = app
.world_mut()
.spawn((TextSpanLayout {
spans: vec![
RenderedVimTextSpan {
text: String::from("sec"),
kind: VimTextSpanKind::Normal,
},
RenderedVimTextSpan {
text: String::from("ond"),
kind: VimTextSpanKind::Cursor,
},
],
},))
.id();
let first_text = app
.world_mut()
.spawn(ManagedText {
target: ViewEntity(first_buffer),
})
.id();
let second_text = app
.world_mut()
.spawn(ManagedText {
target: ViewEntity(second_buffer),
})
.id();
app.update();
assert_eq!(child_entities(&app, first_text).len(), 1);
assert_eq!(child_entities(&app, second_text).len(), 2);
}
#[test]
fn render_sync_populates_new_managed_text_for_stable_layout() {
let mut app = App::new();
let _app = app.add_plugins(TextRenderPlugin);
let buffer = app
.world_mut()
.spawn((TextSpanLayout {
spans: vec![RenderedVimTextSpan {
text: String::from("stable"),
kind: VimTextSpanKind::Normal,
}],
},))
.id();
app.update();
let managed_text = app
.world_mut()
.spawn(ManagedText {
target: ViewEntity(buffer),
})
.id();
app.update();
assert_eq!(child_entities(&app, managed_text).len(), 1);
}
}