use std::collections::HashMap;
use std::ffi::c_void;
use bevy::prelude::*;
use noesis_runtime::text_inlines::{
Bold, Hyperlink, Inline, InlineCollection, InlineUIContainer, Italic, LineBreak, Run, Span,
Underline,
};
use noesis_runtime::view::FrameworkElement;
pub use noesis_runtime::text_inlines::TextDecorations;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InlineSpec {
Run(String),
LineBreak,
Bold(Vec<InlineSpec>),
Italic(Vec<InlineSpec>),
Underline(Vec<InlineSpec>),
Span(Vec<InlineSpec>),
Hyperlink {
uri: Option<String>,
children: Vec<InlineSpec>,
},
Decorated {
decoration: TextDecorations,
children: Vec<InlineSpec>,
},
UiContainer {
child_xaml: String,
},
}
impl InlineSpec {
#[must_use]
pub fn run(text: impl Into<String>) -> Self {
Self::Run(text.into())
}
#[must_use]
pub fn line_break() -> Self {
Self::LineBreak
}
#[must_use]
pub fn bold(children: impl IntoIterator<Item = InlineSpec>) -> Self {
Self::Bold(children.into_iter().collect())
}
#[must_use]
pub fn italic(children: impl IntoIterator<Item = InlineSpec>) -> Self {
Self::Italic(children.into_iter().collect())
}
#[must_use]
pub fn underline(children: impl IntoIterator<Item = InlineSpec>) -> Self {
Self::Underline(children.into_iter().collect())
}
#[must_use]
pub fn span(children: impl IntoIterator<Item = InlineSpec>) -> Self {
Self::Span(children.into_iter().collect())
}
#[must_use]
pub fn hyperlink(
uri: impl Into<String>,
children: impl IntoIterator<Item = InlineSpec>,
) -> Self {
Self::Hyperlink {
uri: Some(uri.into()),
children: children.into_iter().collect(),
}
}
#[must_use]
pub fn decorated(
decoration: TextDecorations,
children: impl IntoIterator<Item = InlineSpec>,
) -> Self {
Self::Decorated {
decoration,
children: children.into_iter().collect(),
}
}
#[must_use]
pub fn ui_container(child_xaml: impl Into<String>) -> Self {
Self::UiContainer {
child_xaml: child_xaml.into(),
}
}
}
pub(crate) enum BuiltInline {
Run(Run),
LineBreak(LineBreak),
Bold(Bold, Vec<BuiltInline>),
Italic(Italic, Vec<BuiltInline>),
Underline(Underline, Vec<BuiltInline>),
Span(Span, Vec<BuiltInline>),
Hyperlink(Hyperlink, Vec<BuiltInline>),
Decorated(Span, Vec<BuiltInline>),
UiContainer(InlineUIContainer, Option<FrameworkElement>),
}
impl BuiltInline {
fn raw(&self) -> *mut c_void {
match self {
BuiltInline::Run(h) => h.raw(),
BuiltInline::LineBreak(h) => h.raw(),
BuiltInline::Bold(h, _) => h.raw(),
BuiltInline::Italic(h, _) => h.raw(),
BuiltInline::Underline(h, _) => h.raw(),
BuiltInline::Span(h, _) => h.raw(),
BuiltInline::Hyperlink(h, _) => h.raw(),
BuiltInline::Decorated(h, _) => h.raw(),
BuiltInline::UiContainer(h, _) => h.raw(),
}
}
fn add_to(&self, collection: &mut InlineCollection) {
let _ = match self {
BuiltInline::Run(h) => collection.add(h),
BuiltInline::LineBreak(h) => collection.add(h),
BuiltInline::Bold(h, _) => collection.add(h),
BuiltInline::Italic(h, _) => collection.add(h),
BuiltInline::Underline(h, _) => collection.add(h),
BuiltInline::Span(h, _) => collection.add(h),
BuiltInline::Hyperlink(h, _) => collection.add(h),
BuiltInline::Decorated(h, _) => collection.add(h),
BuiltInline::UiContainer(h, _) => collection.add(h),
};
}
}
pub(crate) fn build_into(
collection: &mut InlineCollection,
specs: &[InlineSpec],
) -> Vec<BuiltInline> {
let mut built = Vec::with_capacity(specs.len());
for spec in specs {
let node = build_one(spec);
node.add_to(collection);
built.push(node);
}
built
}
macro_rules! build_span_like {
($variant:ident, $ctor:expr, $children:expr) => {{
let handle = $ctor;
let kids = match handle.inlines() {
Some(mut col) => build_into(&mut col, $children),
None => Vec::new(),
};
BuiltInline::$variant(handle, kids)
}};
}
fn build_one(spec: &InlineSpec) -> BuiltInline {
match spec {
InlineSpec::Run(text) => BuiltInline::Run(Run::new(text)),
InlineSpec::LineBreak => BuiltInline::LineBreak(LineBreak::new()),
InlineSpec::Bold(children) => build_span_like!(Bold, Bold::new(), children),
InlineSpec::Italic(children) => build_span_like!(Italic, Italic::new(), children),
InlineSpec::Underline(children) => build_span_like!(Underline, Underline::new(), children),
InlineSpec::Span(children) => build_span_like!(Span, Span::new(), children),
InlineSpec::Hyperlink { uri, children } => {
let mut handle = Hyperlink::new();
if let Some(uri) = uri {
let _ = handle.set_navigate_uri(uri);
}
let kids = match handle.inlines() {
Some(mut col) => build_into(&mut col, children),
None => Vec::new(),
};
BuiltInline::Hyperlink(handle, kids)
}
InlineSpec::Decorated {
decoration,
children,
} => {
let handle = Span::new();
let _ = handle.set_text_decorations(*decoration);
let kids = match handle.inlines() {
Some(mut col) => build_into(&mut col, children),
None => Vec::new(),
};
BuiltInline::Decorated(handle, kids)
}
InlineSpec::UiContainer { child_xaml } => {
let mut handle = InlineUIContainer::new();
let child = FrameworkElement::parse(child_xaml);
match &child {
Some(element) => {
if !handle.set_child(element) {
warn!("NoesisInlines: InlineUIContainer child is not a UIElement; skipped");
}
}
None => warn!(
"NoesisInlines: InlineUIContainer child XAML failed to parse: {child_xaml:?}",
),
}
BuiltInline::UiContainer(handle, child)
}
}
}
fn flatten_into(tree: &[BuiltInline], out: &mut String) {
for node in tree {
match node {
BuiltInline::Run(h) => {
if let Some(text) = h.text() {
out.push_str(&text);
}
}
BuiltInline::LineBreak(_) | BuiltInline::UiContainer(_, _) => {}
BuiltInline::Bold(_, kids)
| BuiltInline::Italic(_, kids)
| BuiltInline::Underline(_, kids)
| BuiltInline::Span(_, kids)
| BuiltInline::Hyperlink(_, kids)
| BuiltInline::Decorated(_, kids) => flatten_into(kids, out),
}
}
}
fn collect_uris(tree: &[BuiltInline], out: &mut Vec<String>) {
for node in tree {
match node {
BuiltInline::Hyperlink(h, kids) => {
if let Some(uri) = h.navigate_uri() {
out.push(uri);
}
collect_uris(kids, out);
}
BuiltInline::Bold(_, kids)
| BuiltInline::Italic(_, kids)
| BuiltInline::Underline(_, kids)
| BuiltInline::Span(_, kids)
| BuiltInline::Decorated(_, kids) => collect_uris(kids, out),
BuiltInline::Run(_) | BuiltInline::LineBreak(_) | BuiltInline::UiContainer(_, _) => {}
}
}
}
fn collect_decorations(tree: &[BuiltInline], out: &mut Vec<TextDecorations>) {
for node in tree {
match node {
BuiltInline::Decorated(h, kids) => {
if let Some(d) = h.text_decorations() {
out.push(d);
}
collect_decorations(kids, out);
}
BuiltInline::Bold(_, kids)
| BuiltInline::Italic(_, kids)
| BuiltInline::Underline(_, kids)
| BuiltInline::Span(_, kids)
| BuiltInline::Hyperlink(_, kids) => collect_decorations(kids, out),
BuiltInline::Run(_) | BuiltInline::LineBreak(_) | BuiltInline::UiContainer(_, _) => {}
}
}
}
fn count_hosted_ui(tree: &[BuiltInline], out: &mut usize) {
for node in tree {
match node {
BuiltInline::UiContainer(container, child) => {
let live = container.child_raw();
if !live.is_null() && child.as_ref().is_some_and(|c| c.raw() == live) {
*out += 1;
}
}
BuiltInline::Bold(_, kids)
| BuiltInline::Italic(_, kids)
| BuiltInline::Underline(_, kids)
| BuiltInline::Span(_, kids)
| BuiltInline::Hyperlink(_, kids)
| BuiltInline::Decorated(_, kids) => count_hosted_ui(kids, out),
BuiltInline::Run(_) | BuiltInline::LineBreak(_) => {}
}
}
}
pub(crate) fn readback(tree: &[BuiltInline], collection: &InlineCollection) -> InlinesReadback {
let count = collection.count();
let mut text = String::new();
flatten_into(tree, &mut text);
let mut hyperlink_uris = Vec::new();
collect_uris(tree, &mut hyperlink_uris);
let mut decorations = Vec::new();
collect_decorations(tree, &mut decorations);
let mut hosted_ui = 0;
count_hosted_ui(tree, &mut hosted_ui);
let matched = tree.len() == count
&& tree
.iter()
.enumerate()
.all(|(i, node)| collection.get_raw(i) == node.raw());
InlinesReadback {
count,
text,
matched,
hyperlink_uris,
decorations,
hosted_ui,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InlinesReadback {
pub count: usize,
pub text: String,
pub matched: bool,
pub hyperlink_uris: Vec<String>,
pub decorations: Vec<TextDecorations>,
pub hosted_ui: usize,
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisInlines {
pub set: HashMap<String, Vec<InlineSpec>>,
pub watch: Vec<String>,
}
impl NoesisInlines {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set(
mut self,
name: impl Into<String>,
inlines: impl IntoIterator<Item = InlineSpec>,
) -> Self {
self.set.insert(name.into(), inlines.into_iter().collect());
self
}
#[must_use]
pub fn watching(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
for name in names {
self.observe(name);
}
self
}
pub fn write(
&mut self,
name: impl Into<String>,
inlines: impl IntoIterator<Item = InlineSpec>,
) {
self.set.insert(name.into(), inlines.into_iter().collect());
}
pub fn observe(&mut self, name: impl Into<String>) {
let name = name.into();
if !self.watch.contains(&name) {
self.watch.push(name);
}
}
}
#[derive(Message, Debug, Clone)]
pub struct NoesisInlinesChanged {
pub view: Entity,
pub name: String,
pub value: InlinesReadback,
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_inlines_bridge(
views: Query<(Entity, Ref<NoesisInlines>)>,
state: Option<NonSendMut<NoesisRenderState>>,
mut changed: MessageWriter<NoesisInlinesChanged>,
) {
let Some(mut state) = state else {
return;
};
for (entity, inlines) in &views {
if inlines.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_inlines_for(entity, &inlines.set);
}
for (name, value) in state.poll_inlines_reads_for(entity, &inlines.watch) {
changed.write(NoesisInlinesChanged {
view: entity,
name,
value,
});
}
}
}
pub struct NoesisInlinesPlugin;
impl Plugin for NoesisInlinesPlugin {
fn build(&self, app: &mut App) {
app.add_message::<NoesisInlinesChanged>()
.add_systems(PostUpdate, sync_inlines_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_set_and_watch() {
let c = NoesisInlines::new()
.set("Body", [InlineSpec::run("Hi"), InlineSpec::line_break()])
.set("Title", [InlineSpec::bold([InlineSpec::run("X")])])
.watching(["Body", "Title"]);
assert_eq!(
c.set.get("Body"),
Some(&vec![InlineSpec::Run("Hi".into()), InlineSpec::LineBreak]),
);
assert_eq!(
c.set.get("Title"),
Some(&vec![InlineSpec::Bold(vec![InlineSpec::Run("X".into())])]),
);
assert_eq!(c.watch, vec!["Body".to_string(), "Title".to_string()]);
}
#[test]
fn decorated_and_ui_container_specs() {
assert_eq!(
InlineSpec::decorated(TextDecorations::Strikethrough, [InlineSpec::run("x")]),
InlineSpec::Decorated {
decoration: TextDecorations::Strikethrough,
children: vec![InlineSpec::Run("x".into())],
},
);
assert_eq!(
InlineSpec::ui_container("<Rectangle/>"),
InlineSpec::UiContainer {
child_xaml: "<Rectangle/>".into(),
},
);
}
#[test]
fn hyperlink_spec_carries_uri() {
let h = InlineSpec::hyperlink("https://noesisengine.com/", [InlineSpec::run("click")]);
assert_eq!(
h,
InlineSpec::Hyperlink {
uri: Some("https://noesisengine.com/".into()),
children: vec![InlineSpec::Run("click".into())],
}
);
}
}