use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use bevy::core_pipeline::upscaling::upscaling;
use bevy::core_pipeline::{Core2d, Core2dSystems, Core3d, Core3dSystems};
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::system::ScheduleSystem;
use bevy::prelude::*;
use bevy_render::{
Render, RenderApp, RenderSystems,
extract_component::{ExtractComponent, ExtractComponentPlugin},
renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery},
view::ViewTarget,
};
use noesis_runtime::animation::{Animation, DoubleAnimation, Timeline};
use noesis_runtime::commands::Command;
use noesis_runtime::events::{
ClickSubscription, EventArgs, EventSubscription, KeyDownSubscription, RoutedEvent,
subscribe_click, subscribe_event, subscribe_keydown,
};
use noesis_runtime::input::KeyBinding;
use noesis_runtime::transforms::{CompositeTransform, CompositeTransform3D, MatrixTransform3D};
use noesis_runtime::view::{FrameworkElement, Key, View};
use crate::binding::{BindingEntry, BuiltBinding};
use crate::commands::{CommandEntry, CommandsDef, SharedCommandQueue};
use crate::events::{SharedClickQueue, SharedKeyDownQueue};
use crate::font::{BevyFontProvider, FontRegistry, SharedFontMap};
use crate::image::{BevyTextureProvider, ImageRegistry, SharedImageMap};
use crate::items::{CollectionViewOp, ItemValue, ItemsBinding, ObjectSource};
use crate::plain_vm::{PlainType, PlainValue, PlainVmEntry, SetSink, unbox};
use crate::render_device::WgpuRenderDevice;
use crate::routed_events::{RoutedEventSnapshot, SharedRoutedEventQueue};
use crate::viewmodel::{AttachTarget, SharedVmChangedQueue, ViewModelDef, VmEntry, VmValue};
use crate::xaml::{BevyXamlProvider, SharedXamlMap, XamlRegistry};
use noesis_runtime::element_tree::panel_children;
use noesis_runtime::plain_vm::{PlainInstance, PlainValueRef, PlainVmBuilder, PlainVmClass};
thread_local! {
static FFI_HOPS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[inline]
pub(crate) fn record_ffi_hop() {
FFI_HOPS.with(|c| c.set(c.get().wrapping_add(1)));
}
#[inline]
#[must_use]
pub(crate) fn ffi_hops() -> u64 {
FFI_HOPS.with(std::cell::Cell::get)
}
#[derive(Resource, Default)]
pub(crate) struct NoesisApplyTimer {
started: Option<std::time::Instant>,
pub(crate) last: std::time::Duration,
}
const INTERMEDIATE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
fn flags_from(config: &NoesisView) -> u32 {
let mut f = 0;
if config.ppaa {
f |= noesis_runtime::view::RenderFlag::Ppaa as u32;
}
f
}
const INTERMEDIATE_SAMPLE_FORMAT_SRGB: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
fn is_linear_float(format: wgpu::TextureFormat) -> bool {
matches!(
format,
wgpu::TextureFormat::Rgba16Float
| wgpu::TextureFormat::Rgba32Float
| wgpu::TextureFormat::Rg11b10Ufloat
)
}
struct Intermediate {
#[allow(dead_code)]
texture: wgpu::Texture,
view: wgpu::TextureView,
sample_view: wgpu::TextureView,
}
fn create_intermediate(device: &wgpu::Device, size: UVec2) -> Intermediate {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("noesis intermediate"),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: INTERMEDIATE_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[INTERMEDIATE_SAMPLE_FORMAT_SRGB],
});
let render_view = tex.create_view(&wgpu::TextureViewDescriptor {
label: Some("noesis intermediate (render, Unorm)"),
format: Some(INTERMEDIATE_FORMAT),
..Default::default()
});
let sample_view = tex.create_view(&wgpu::TextureViewDescriptor {
label: Some("noesis intermediate (sample, UnormSrgb)"),
format: Some(INTERMEDIATE_SAMPLE_FORMAT_SRGB),
..Default::default()
});
Intermediate {
texture: tex,
view: render_view,
sample_view,
}
}
fn build_noesis_style(
spec: &crate::styles::StyleSpec,
name: &str,
uri: &str,
) -> Option<noesis_runtime::styles::Style> {
use noesis_runtime::binding::Binding;
use noesis_runtime::styles::{DataTrigger, MultiTrigger, Style, Trigger};
let mut style = Style::new();
if !style.set_target_type(&spec.target_type) {
warn!(
"NoesisStyles: unknown TargetType {:?} for {name:?} in scene {uri:?}",
spec.target_type,
);
return None;
}
if let Some(base_spec) = &spec.based_on {
if let Some(base) = build_noesis_style(base_spec, name, uri) {
style.set_based_on(&base);
} else {
warn!(
"NoesisStyles: BasedOn style (TargetType {:?}) skipped for {name:?} in scene {uri:?}",
base_spec.target_type,
);
}
}
for (property, value) in &spec.setters {
if !style.add_setter(property, &value.to_boxed()) {
warn!(
"NoesisStyles: setter {property:?} unresolved on {:?} ({name:?})",
spec.target_type,
);
}
}
for trig in &spec.triggers {
let mut trigger = Trigger::new();
if !trigger.set_property(&spec.target_type, &trig.property) {
warn!(
"NoesisStyles: trigger property {:?} unresolved on {:?} ({name:?})",
trig.property, spec.target_type,
);
continue;
}
if !trigger.set_value(&trig.value.to_boxed()) {
warn!(
"NoesisStyles: trigger value for {:?} rejected ({name:?})",
trig.property
);
}
for (property, value) in &trig.setters {
if !trigger.add_setter(&spec.target_type, property, &value.to_boxed()) {
warn!(
"NoesisStyles: trigger setter {property:?} unresolved on {:?} ({name:?})",
spec.target_type,
);
}
}
let _ = style.add_trigger(&trigger);
}
for dt in &spec.data_triggers {
let mut trigger = DataTrigger::new();
let mut binding = Binding::new(&dt.binding_path);
if dt.relative_source_self {
binding = binding.relative_source_self();
}
let _ = trigger.set_binding(&binding);
if !trigger.set_value(&dt.value.to_boxed()) {
warn!(
"NoesisStyles: data-trigger value for {:?} rejected ({name:?})",
dt.binding_path
);
}
for (property, value) in &dt.setters {
if !trigger.add_setter(&spec.target_type, property, &value.to_boxed()) {
warn!(
"NoesisStyles: data-trigger setter {property:?} unresolved on {:?} ({name:?})",
spec.target_type,
);
}
}
let _ = style.add_trigger(&trigger);
}
for mt in &spec.multi_triggers {
let mut trigger = MultiTrigger::new();
for (property, value) in &mt.conditions {
if !trigger.add_condition(&spec.target_type, property, &value.to_boxed()) {
warn!(
"NoesisStyles: multi-trigger condition {property:?} unresolved on {:?} ({name:?})",
spec.target_type,
);
}
}
for (property, value) in &mt.setters {
if !trigger.add_setter(&spec.target_type, property, &value.to_boxed()) {
warn!(
"NoesisStyles: multi-trigger setter {property:?} unresolved on {:?} ({name:?})",
spec.target_type,
);
}
}
let _ = style.add_trigger(&trigger);
}
Some(style)
}
#[derive(Component, Clone, Debug)]
#[require(
crate::animation::NoesisAnimation,
crate::binding::NoesisBinding,
crate::brushes::NoesisBrushes,
crate::dp::NoesisDp,
crate::events::NoesisClickWatch,
crate::events::NoesisKeyDownWatch,
crate::focus::NoesisFocus,
crate::focus_input::NoesisFocusControl,
crate::geometry::NoesisGeometry,
crate::imaging::NoesisImaging,
crate::inlines::NoesisInlines,
crate::items::NoesisItems,
crate::layout::NoesisLayout,
crate::routed_events::NoesisEventWatch,
crate::shapes::NoesisShapes,
crate::styles::NoesisStyles,
crate::svg::NoesisSvg,
crate::text::NoesisText,
crate::transforms::NoesisTransform,
crate::transforms3d::NoesisTransform3D,
crate::typography::NoesisTypography,
crate::visibility::NoesisVisibility,
crate::visual_state::NoesisVisualState
)]
pub struct NoesisView {
pub xaml_uri: String,
pub size: UVec2,
pub scale: f32,
pub wait_for_fonts: Vec<String>,
pub wait_for_font_files: Vec<(String, String)>,
pub wait_for_images: Vec<String>,
pub ppaa: bool,
pub application_resources: Vec<String>,
pub font_fallbacks: Vec<String>,
}
impl Default for NoesisView {
fn default() -> Self {
Self {
xaml_uri: String::new(),
size: UVec2::new(512, 512),
scale: 1.0,
wait_for_fonts: Vec::new(),
wait_for_font_files: Vec::new(),
wait_for_images: Vec::new(),
ppaa: true,
application_resources: Vec::new(),
font_fallbacks: Vec::new(),
}
}
}
struct AppResourcesSnapshot {
entries: HashMap<String, crate::resources::ResourceEntry>,
merged_xaml: Vec<String>,
chain_uris: Vec<String>,
}
pub(crate) struct NoesisRenderState {
device: wgpu::Device,
shared_map: SharedXamlMap,
shared_fonts: SharedFontMap,
shared_images: SharedImageMap,
registered_device: Option<noesis_runtime::render_device::Registered>,
registered_provider: Option<noesis_runtime::xaml_provider::Registered>,
registered_fonts: Option<noesis_runtime::font_provider::Registered>,
registered_textures: Option<noesis_runtime::texture_provider::Registered>,
scenes: HashMap<Entity, SceneInstance>,
published_intermediates: HashSet<Entity>,
scenes_built_this_frame: HashSet<Entity>,
panels_mounted_this_frame: HashSet<Entity>,
pointer_over_ui: bool,
fallbacks_installed: bool,
registered_faces: HashSet<(String, String)>,
installed_app_resources: Option<AppResourcesSnapshot>,
clock_origin: std::time::Instant,
last_keydown_swallow: HashMap<(Entity, String), (Vec<Key>, Entity)>,
last_click_target: HashMap<(Entity, String), Entity>,
last_event_config: HashMap<(Entity, String, &'static str), (bool, bool, Entity)>,
bake_rig: Option<BakeRig>,
view_models: HashMap<Entity, VmEntry>,
items_sources: HashMap<(Entity, String), ItemsBinding>,
plain_vms: HashMap<(Entity, std::any::TypeId), PlainVmEntry>,
command_hosts: HashMap<Entity, CommandEntry>,
warned_host_build_failures: HashSet<(Entity, &'static str)>,
warned_dc_collisions: HashSet<(Entity, AttachTarget)>,
binding_entries: HashMap<(Entity, String, String), BindingEntry>,
panels: HashMap<Entity, PanelEntry>,
failed_fragments: HashSet<(Entity, String)>,
lists: HashMap<(Entity, String), crate::list::ListBinding>,
list_owners: HashMap<Entity, (Entity, String)>,
integration_guards: Vec<Box<dyn std::any::Any + Send>>,
}
struct SceneInstance {
view: View,
renderer_initialized: bool,
intermediates: [Intermediate; 2],
write_index: usize,
size: UVec2,
built_for_uri: String,
built_bytes: Arc<Vec<u8>>,
applied_flags: u32,
applied_scale: f32,
click_subs: HashMap<String, ClickSubscription>,
keydown_subs: HashMap<String, KeyDownSubscription>,
event_subs: HashMap<(String, &'static str), EventSubscription>,
row_click_subs: HashMap<String, EventSubscription>,
text_snapshots: HashMap<String, String>,
dp_snapshots: HashMap<(String, String), crate::dp::DpValue>,
transform_handles: HashMap<String, CompositeTransform>,
transform_snapshots: HashMap<String, crate::transforms::TransformSpec>,
transform3d_handles: HashMap<String, CompositeTransform3D>,
transform3d_snapshots: HashMap<String, crate::transforms3d::Transform3DSpec>,
matrix_transform3d_handles: HashMap<String, MatrixTransform3D>,
matrix_transform3d_snapshots: HashMap<String, [f32; 12]>,
brush_snapshots: HashMap<(String, String), crate::brushes::BrushReadback>,
typo_snapshots:
HashMap<(String, crate::typography::TypographyField), crate::typography::TypographyValue>,
input_bindings: HashMap<(String, i32, i32), InstalledKeyBinding>,
predict_snapshots: HashMap<(String, i32, Option<String>), (bool, Option<String>, bool)>,
image_snapshots: HashMap<String, crate::imaging::ImageReadback>,
inline_handles: HashMap<String, Vec<crate::inlines::BuiltInline>>,
inlines_snapshots: HashMap<String, crate::inlines::InlinesReadback>,
}
struct InstalledKeyBinding {
#[allow(dead_code)] command: Command,
binding: KeyBinding,
}
static PANEL_CLASS_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
struct PanelEntry {
click_subs: HashMap<String, ClickSubscription>,
keydown_subs: HashMap<String, KeyDownSubscription>,
event_subs: HashMap<(String, &'static str), EventSubscription>,
fragment: FrameworkElement,
instance: PlainInstance,
_class: PlainVmClass,
prop_names: Vec<String>,
set_sink: SetSink,
host: Entity,
host_name: String,
mounted_for_uri: Option<String>,
text_snapshots: HashMap<String, String>,
}
impl PanelEntry {
fn build(
uri: &str,
host: Entity,
host_name: &str,
props: &[(String, PlainType)],
) -> Option<Self> {
let mut fragment = FrameworkElement::load(uri)?;
let seq = PANEL_CLASS_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let class_name = format!("DmPanel.{seq}");
let prop_names: Vec<String> = props.iter().map(|(n, _)| n.clone()).collect();
let kinds: Vec<PlainType> = props.iter().map(|(_, k)| *k).collect();
let set_sink: SetSink = Arc::new(std::sync::Mutex::new(Vec::new()));
let mut builder = PlainVmBuilder::new(&class_name);
for (name, kind) in props {
builder.add_property(name, *kind);
}
let sink_for_handler = Arc::clone(&set_sink);
let class = builder
.on_set(move |idx: u32, value: &PlainValueRef| {
let kind = kinds
.get(idx as usize)
.copied()
.unwrap_or(PlainType::BaseComponent);
let owned = unbox(kind, value);
if let Ok(mut queue) = sink_for_handler.lock() {
queue.push((idx, owned));
}
})
.register()?;
let instance = class.create_instance()?;
if !instance.set_data_context(&mut fragment) {
warn!("UiPanel: set_data_context returned false for fragment {uri:?}");
}
Some(Self {
click_subs: HashMap::new(),
keydown_subs: HashMap::new(),
event_subs: HashMap::new(),
fragment,
instance,
_class: class,
prop_names,
set_sink,
host,
host_name: host_name.to_owned(),
mounted_for_uri: None,
text_snapshots: HashMap::new(),
})
}
fn apply_pushes(&self, pushes: &[(u32, PlainValue)]) {
for (gi, value) in pushes {
if let Some(name) = self.prop_names.get(*gi as usize) {
let _ = self.instance.set_and_notify(*gi, name, value.clone());
}
}
}
fn drain_writebacks(&self) -> Vec<(u32, PlainValue)> {
let mut guard = self.set_sink.lock().expect("panel set sink poisoned");
if guard.is_empty() {
Vec::new()
} else {
std::mem::take(&mut *guard)
}
}
fn unmount(&self, scenes: &HashMap<Entity, SceneInstance>) {
let Some(scene) = scenes.get(&self.host) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
let Some(host_el) = resolve_named(&content, &self.host_name) else {
return;
};
let Some(mut children) = panel_children(&host_el) else {
return;
};
let target = self.fragment.raw();
for i in 0..children.count() {
if std::ptr::eq(children.get_raw(i), target) {
let _ = children.remove_at(i);
break;
}
}
}
}
struct BakeRig {
view: View,
renderer_initialized: bool,
built_for_uri: String,
size: UVec2,
}
fn resolve_named(root: &FrameworkElement, path: &str) -> Option<FrameworkElement> {
record_ffi_hop();
resolve_scope_path(root, path, |host, name| host.find_name(name))
}
fn resolve_scope_path<T>(
root: &T,
path: &str,
find: impl Fn(&T, &str) -> Option<T> + Copy,
) -> Option<T> {
match path.split_once('/') {
None => find(root, path),
Some((host, rest)) => resolve_scope_path(&find(root, host)?, rest, find),
}
}
impl NoesisRenderState {
pub(crate) fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
let shared_map = SharedXamlMap::default();
let shared_fonts = SharedFontMap::default();
let shared_images = SharedImageMap::default();
let wgpu_rd = WgpuRenderDevice::new(device.clone(), queue);
let registered_device = noesis_runtime::render_device::register(wgpu_rd);
let xaml_prov = BevyXamlProvider::from_shared(shared_map.clone());
let registered_provider = noesis_runtime::xaml_provider::set_xaml_provider(xaml_prov);
let font_prov = BevyFontProvider::from_shared(shared_fonts.clone());
let registered_fonts = noesis_runtime::font_provider::set_font_provider(font_prov);
let texture_prov = BevyTextureProvider::from_shared(shared_images.clone());
let registered_textures =
noesis_runtime::texture_provider::set_texture_provider(texture_prov);
Self {
device,
shared_map,
shared_fonts,
shared_images,
registered_device: Some(registered_device),
registered_provider: Some(registered_provider),
registered_fonts: Some(registered_fonts),
registered_textures: Some(registered_textures),
scenes: HashMap::new(),
published_intermediates: HashSet::new(),
scenes_built_this_frame: HashSet::new(),
panels_mounted_this_frame: HashSet::new(),
pointer_over_ui: false,
fallbacks_installed: false,
registered_faces: HashSet::new(),
installed_app_resources: None,
clock_origin: std::time::Instant::now(),
last_keydown_swallow: HashMap::new(),
last_click_target: HashMap::new(),
last_event_config: HashMap::new(),
bake_rig: None,
view_models: HashMap::new(),
items_sources: HashMap::new(),
plain_vms: HashMap::new(),
command_hosts: HashMap::new(),
warned_host_build_failures: HashSet::new(),
warned_dc_collisions: HashSet::new(),
binding_entries: HashMap::new(),
panels: HashMap::new(),
failed_fragments: HashSet::new(),
lists: HashMap::new(),
list_owners: HashMap::new(),
integration_guards: Vec::new(),
}
}
pub(crate) fn own_integration_guards(&mut self, guards: Vec<Box<dyn std::any::Any + Send>>) {
self.integration_guards = guards;
}
pub(crate) fn ensure_view_model(
&mut self,
entity: Entity,
def: &ViewModelDef,
changed: &SharedVmChangedQueue,
) {
if let Some(existing) = self.view_models.get(&entity) {
if existing.matches(def) {
return;
}
self.reap_view_model_for(entity);
}
match VmEntry::build(entity, def, changed) {
Some(entry) => {
self.view_models.insert(entity, entry);
self.warned_host_build_failures
.remove(&(entity, "NoesisVm"));
self.warn_datacontext_collisions(entity);
}
None => {
if self.warned_host_build_failures.insert((entity, "NoesisVm")) {
warn!(
"NoesisViewModel: failed to register/instantiate class {:?} (duplicate name?)",
def.class_name(),
);
}
}
}
}
pub(crate) fn apply_view_model_writes_for(
&mut self,
entity: Entity,
writes: &[(String, VmValue)],
) {
let Some(entry) = self.view_models.get(&entity) else {
return;
};
for (prop, value) in writes {
if !entry.write(prop, value) {
warn!("NoesisViewModel: view {entity:?} has no property {prop:?}");
}
}
}
pub(crate) fn attach_view_models(&mut self) {
for (&entity, entry) in &mut self.view_models {
let Some(scene) = self.scenes.get(&entity) else {
continue;
};
let Some(content) = scene.view.content() else {
continue;
};
let uri = &scene.built_for_uri;
if !entry.needs_attach(uri) {
continue;
}
let target = match entry.target() {
AttachTarget::Root => scene.view.content(),
AttachTarget::Named(name) => resolve_named(&content, name),
};
let Some(mut element) = target else {
warn!(
"NoesisViewModel: attach target for view {:?} not found in scene {:?}",
entity, scene.built_for_uri,
);
continue;
};
if element.set_data_context(entry.instance()) {
entry.mark_attached(uri);
} else {
warn!("NoesisViewModel: set_data_context returned false for view {entity:?}");
}
}
}
pub(crate) fn ensure_commands(
&mut self,
entity: Entity,
def: &CommandsDef,
queue: &SharedCommandQueue,
) {
if let Some(existing) = self.command_hosts.get(&entity) {
if existing.matches(def) {
return;
}
self.reap_commands_for(entity);
}
match CommandEntry::build(entity, def, queue) {
Some(entry) => {
self.command_hosts.insert(entity, entry);
self.warned_host_build_failures
.remove(&(entity, "NoesisCommands"));
self.warn_datacontext_collisions(entity);
}
None => {
if self
.warned_host_build_failures
.insert((entity, "NoesisCommands"))
{
warn!(
"NoesisCommands: failed to register/instantiate class {:?} (duplicate name?)",
def.class_name(),
);
}
}
}
}
pub(crate) fn apply_command_enables_for(&mut self, entity: Entity, enables: &[(String, bool)]) {
let Some(entry) = self.command_hosts.get(&entity) else {
return;
};
for (name, value) in enables {
if !entry.set_enabled(name, *value) {
warn!("NoesisCommands: view {entity:?} has no command {name:?}");
}
}
}
pub(crate) fn attach_commands(&mut self) {
for (&entity, entry) in &mut self.command_hosts {
let Some(scene) = self.scenes.get(&entity) else {
continue;
};
let Some(content) = scene.view.content() else {
continue;
};
let uri = &scene.built_for_uri;
if !entry.needs_attach(uri) {
continue;
}
let target = match entry.target() {
AttachTarget::Root => scene.view.content(),
AttachTarget::Named(name) => resolve_named(&content, name),
};
let Some(mut element) = target else {
warn!(
"NoesisCommands: attach target for view {:?} not found in scene {:?}",
entity, scene.built_for_uri,
);
continue;
};
if element.set_data_context(entry.instance()) {
entry.mark_attached(uri);
} else {
warn!("NoesisCommands: set_data_context returned false for view {entity:?}");
}
}
}
fn warn_datacontext_collisions(&mut self, entity: Entity) {
let mut by_target: HashMap<&AttachTarget, Vec<String>> = HashMap::new();
if let Some(entry) = self.view_models.get(&entity) {
by_target
.entry(entry.target())
.or_default()
.push("NoesisVm".to_owned());
}
if let Some(entry) = self.command_hosts.get(&entity) {
by_target
.entry(entry.target())
.or_default()
.push("NoesisCommands".to_owned());
}
for ((ent, _), entry) in &self.plain_vms {
if *ent == entity {
by_target
.entry(entry.target())
.or_default()
.push(format!("plain view model {:?}", entry.type_name()));
}
}
for (target, mut sources) in by_target {
if sources.len() < 2 {
continue;
}
if !self.warned_dc_collisions.insert((entity, target.clone())) {
continue;
}
sources.sort();
warn!(
"DataContext collision on view {entity:?} {}: {} all attach as the same \
element's DataContext, so only the last one applied wins and the rest are \
silently inert — give them distinct attach targets (attach_to(x_name))",
target.describe(),
sources.join(", "),
);
}
}
pub(crate) fn apply_items_for(
&mut self,
entity: Entity,
sources: &HashMap<String, Vec<ItemValue>>,
objects: &HashMap<String, ObjectSource>,
select: &HashMap<String, i32>,
navigate: &HashMap<String, CollectionViewOp>,
changed: bool,
) {
if changed {
self.items_sources.retain(|(ent, name), _| {
*ent != entity || sources.contains_key(name) || objects.contains_key(name)
});
for (name, items) in sources {
if objects.contains_key(name) {
warn!(
"NoesisItems: x:Name {name:?} is in both sources and objects; \
using the object items and ignoring the primitive source",
);
continue;
}
let binding = self
.items_sources
.entry((entity, name.clone()))
.or_default();
binding.set_typed(items);
binding.set_desired_select(select.get(name).copied());
binding.set_desired_nav(navigate.get(name).copied());
}
for (name, source) in objects {
let binding = self
.items_sources
.entry((entity, name.clone()))
.or_default();
binding.set_objects(source);
binding.set_desired_select(select.get(name).copied());
binding.set_desired_nav(navigate.get(name).copied());
}
}
let Some(scene) = self.scenes.get(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
let uri = scene.built_for_uri.clone();
for ((ent, name), binding) in &mut self.items_sources {
if *ent != entity {
continue;
}
let Some(mut element) = resolve_named(&content, name) else {
if binding.needs_bind(&uri) {
warn!(
"NoesisItems: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
}
continue;
};
if binding.needs_bind(&uri) {
record_ffi_hop();
if element.set_items_source(binding.collection()) {
binding.mark_bound(&uri);
} else {
warn!("NoesisItems: element {name:?} is not an ItemsControl; skipped");
continue;
}
}
binding.drive_selection(&mut element);
binding.drive_navigation();
}
}
pub(crate) fn poll_items_for(
&mut self,
entity: Entity,
) -> Vec<(String, usize, i32, i32, Option<ItemValue>)> {
let mut out = Vec::new();
let Some(scene) = self.scenes.get(&entity) else {
return out;
};
let Some(content) = scene.view.content() else {
return out;
};
for ((ent, name), binding) in &mut self.items_sources {
if *ent != entity {
continue;
}
let Some(element) = resolve_named(&content, name) else {
continue;
};
if let Some((count, selected_index, current_position, current)) =
binding.read_changed(&element)
{
out.push((
name.clone(),
count,
selected_index,
current_position,
current,
));
}
}
out
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_list_for(
&mut self,
list_ent: Entity,
view: Entity,
name: &str,
class: &str,
schema: &[(&'static str, crate::plain_vm::PlainType)],
desired: &[crate::list::DesiredRow],
desired_selected: Option<Entity>,
click_queue: &SharedClickQueue,
) -> (crate::list::ListOps, crate::list::SelectionOutcome) {
let key = (view, name.to_owned());
self.list_owners.insert(list_ent, key.clone());
let ops = {
let binding = self.lists.entry(key.clone()).or_default();
binding.reconcile_into(class, schema, desired)
};
let mount = self
.scenes
.get(&view)
.and_then(|s| s.view.content().map(|c| (c, s.built_for_uri.clone())));
if let Some((content, uri)) = mount
&& self.lists.get(&key).is_some_and(|b| b.needs_bind(&uri))
{
match resolve_named(&content, name) {
Some(mut element) => {
record_ffi_hop();
let bound = {
let binding = self.lists.get_mut(&key).expect("checked above");
let ok = element.set_items_source(binding.collection());
if ok {
binding.mark_bound(&uri);
binding.set_control(element.clone_ref());
}
ok
};
if bound {
self.install_row_click_sub(view, name, &element, click_queue);
} else {
warn!("UiList: element {name:?} is not an ItemsControl; skipped");
}
}
None => warn!("UiList: x:Name {name:?} not found in scene {uri:?}"),
}
}
let selection = self
.lists
.get_mut(&key)
.map_or(crate::list::SelectionOutcome::Unchanged, |b| {
b.poll_selection(desired_selected)
});
(ops, selection)
}
fn install_row_click_sub(
&mut self,
entity: Entity,
name: &str,
element: &FrameworkElement,
click_queue: &SharedClickQueue,
) {
let queue_handle = click_queue.clone();
let list_name = name.to_owned();
let Some(sub) = subscribe_event(
element,
RoutedEvent::MouseLeftButtonUp,
false,
move |args: &EventArgs| {
if let Some(bits) = args.source_data_context_u64(crate::list::ENTITY_FIELD)
&& let Some(row) = Entity::try_from_bits(bits)
{
queue_handle.push(entity, row, list_name.clone());
}
false
},
) else {
warn!("UiList: element {name:?} is not a UIElement; per-row clicks disabled");
return;
};
if let Some(scene) = self.scenes.get_mut(&entity) {
scene.row_click_subs.insert(name.to_owned(), sub);
}
}
#[must_use]
pub(crate) fn live_list_count(&self) -> usize {
self.lists.len()
}
#[must_use]
pub(crate) fn live_binding_count(&self) -> usize {
self.binding_entries.len()
}
pub(crate) fn has_binding(&self, entity: Entity, element: &str, property: &str) -> bool {
self.binding_entries
.contains_key(&(entity, element.to_owned(), property.to_owned()))
}
pub(crate) fn insert_binding(
&mut self,
entity: Entity,
element: String,
property: String,
built: BuiltBinding,
) {
self.binding_entries
.insert((entity, element, property), BindingEntry::new(built));
}
pub(crate) fn reap_binding_for(&mut self, entity: Entity, element: &str, property: &str) {
let key = (entity, element.to_owned(), property.to_owned());
if let Some(entry) = self.binding_entries.get(&key)
&& let Some(scene) = self.scenes.get(&entity)
&& !entry.needs_bind(&scene.built_for_uri)
&& let Some(content) = scene.view.content()
&& let Some(target) = resolve_named(&content, element)
{
let _ = noesis_runtime::binding::clear_binding(&target, property);
}
self.binding_entries.remove(&key);
}
pub(crate) fn reap_bindings_for(&mut self, entity: Entity) {
let targets: Vec<(String, String)> = self
.binding_entries
.keys()
.filter(|(ent, _, _)| *ent == entity)
.map(|(_, element, property)| (element.clone(), property.clone()))
.collect();
for (element, property) in targets {
self.reap_binding_for(entity, &element, &property);
}
}
pub(crate) fn prune_bindings_for(&mut self, entity: Entity, keep: &[(String, String)]) {
let stale: Vec<(String, String)> = self
.binding_entries
.keys()
.filter(|(ent, element, property)| {
*ent == entity && !keep.iter().any(|(ke, kp)| ke == element && kp == property)
})
.map(|(_, element, property)| (element.clone(), property.clone()))
.collect();
for (element, property) in stale {
self.reap_binding_for(entity, &element, &property);
}
}
pub(crate) fn bind_pending_for(&mut self, entity: Entity) {
let Some(scene) = self.scenes.get(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
let uri = scene.built_for_uri.clone();
for ((ent, element, property), entry) in &mut self.binding_entries {
if *ent != entity || !entry.needs_bind(&uri) {
continue;
}
let Some(target) = resolve_named(&content, element) else {
warn!(
"NoesisBinding: x:Name {:?} not found in scene {:?}",
element, scene.built_for_uri,
);
continue;
};
if entry.bind_onto(&target, property) {
entry.mark_bound(&uri);
} else {
warn!(
"NoesisBinding: binding {element:?}.{property:?} failed \
(unknown property or type mismatch)",
);
}
}
}
pub(crate) fn sync_plain_vm(
&mut self,
entity: Entity,
type_id: std::any::TypeId,
type_name: &str,
props: &[(&'static str, crate::plain_vm::PlainType)],
target: &AttachTarget,
snapshot: Option<Vec<crate::plain_vm::PlainValue>>,
) -> Vec<(u32, crate::plain_vm::PlainValue)> {
let key = (entity, type_id);
if let std::collections::hash_map::Entry::Vacant(slot) = self.plain_vms.entry(key) {
let Some(entry) = PlainVmEntry::build(type_name, entity, props, target.clone()) else {
warn!("NoesisViewModel: failed to register plain VM {type_name:?}");
return Vec::new();
};
slot.insert(entry);
self.warn_datacontext_collisions(entity);
}
if let (Some(entry), Some(snapshot)) = (self.plain_vms.get(&key), snapshot) {
entry.apply_snapshot(&snapshot);
}
if let Some(scene) = self.scenes.get(&entity)
&& let Some(content) = scene.view.content()
{
let uri = scene.built_for_uri.clone();
if let Some(entry) = self.plain_vms.get_mut(&key)
&& entry.needs_attach(&uri)
{
let element = match entry.target() {
AttachTarget::Root => scene.view.content(),
AttachTarget::Named(name) => resolve_named(&content, name),
};
match element {
Some(mut element) => {
if !entry.attach_to(&mut element, &uri) {
warn!(
"NoesisViewModel: set_data_context returned false for \
{type_name:?} (target not a FrameworkElement?)",
);
}
}
None => warn!(
"NoesisViewModel: attach target for {type_name:?} not found in scene {:?}",
scene.built_for_uri,
),
}
}
}
self.plain_vms
.get(&key)
.map(PlainVmEntry::drain_writebacks)
.unwrap_or_default()
}
pub(crate) fn sync_panel(
&mut self,
entity: Entity,
uri: &str,
host: Entity,
host_name: &str,
props: &[(String, PlainType)],
pushes: &[(u32, PlainValue)],
) -> Vec<(u32, PlainValue)> {
if let std::collections::hash_map::Entry::Vacant(slot) = self.panels.entry(entity) {
let sink = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let captured = std::sync::Arc::clone(&sink);
let built = {
let _guard = noesis_runtime::diagnostics::set_thread_error_handler(
move |_file, _line, message, _fatal, ctx| {
let at = ctx
.filter(|c| c.line != 0)
.map(|c| format!(" (line {}, col {})", c.line, c.column))
.unwrap_or_default();
captured.lock().unwrap().push(format!("{message}{at}"));
},
);
PanelEntry::build(uri, host, host_name, props)
};
let warnings = std::mem::take(&mut *sink.lock().unwrap());
match built {
Some(built) => {
slot.insert(built);
self.failed_fragments.remove(&(entity, uri.to_owned()));
if !warnings.is_empty() {
error!(
"UiPanel {entity:?}: fragment {uri:?} loaded with parser warning(s): {}",
warnings.join("; "),
);
}
}
None => {
if self.failed_fragments.insert((entity, uri.to_owned())) {
let why = if warnings.is_empty() {
"unregistered/typo'd URI, or XAML Noesis rejected outright".to_owned()
} else {
warnings.join("; ")
};
error!(
"UiPanel {entity:?}: fragment {uri:?} failed to load: {why}. \
The panel will not mount until it resolves.",
);
}
return Vec::new();
}
}
}
if let Some(entry) = self.panels.get(&entity)
&& !pushes.is_empty()
{
entry.apply_pushes(pushes);
}
let mount = self
.scenes
.get(&host)
.and_then(|s| s.view.content().map(|c| (c, s.built_for_uri.clone())));
if let Some((content, host_uri)) = mount
&& let Some(entry) = self.panels.get_mut(&entity)
&& entry.mounted_for_uri.as_deref() != Some(host_uri.as_str())
{
match resolve_named(&content, &entry.host_name) {
Some(host_el) => match panel_children(&host_el) {
Some(mut children) => {
record_ffi_hop();
if children.add(&entry.fragment).is_some() {
entry.mounted_for_uri = Some(host_uri);
self.panels_mounted_this_frame.insert(entity);
} else {
warn!(
"UiPanel: failed to add fragment to host {:?} for panel {entity:?}",
entry.host_name,
);
}
}
None => warn!(
"UiPanel: host {:?} is not a Panel (panel {entity:?})",
entry.host_name,
),
},
None => warn!(
"UiPanel: host {:?} not found in scene {host_uri:?} (panel {entity:?})",
entry.host_name,
),
}
}
self.panels
.get(&entity)
.map(PanelEntry::drain_writebacks)
.unwrap_or_default()
}
pub(crate) fn teardown_panel_for(&mut self, entity: Entity) {
if let Some(entry) = self.panels.remove(&entity) {
entry.unmount(&self.scenes);
drop(entry);
}
self.failed_fragments.retain(|(ent, _)| *ent != entity);
self.last_click_target.retain(|(ent, _), _| *ent != entity);
self.last_keydown_swallow
.retain(|(ent, _), _| *ent != entity);
}
pub(crate) fn poll_panel_text_for(
&mut self,
entity: Entity,
watched: &[String],
) -> Vec<(String, String)> {
let mut changed = Vec::new();
let Some(entry) = self.panels.get_mut(&entity) else {
return changed;
};
entry
.text_snapshots
.retain(|k, _| watched.iter().any(|w| w == k));
if watched.is_empty() {
return changed;
}
for name in watched {
let Some(element) = resolve_named(&entry.fragment, name) else {
continue;
};
let current = element.text().unwrap_or_default();
if entry.text_snapshots.get(name) == Some(¤t) {
continue;
}
entry.text_snapshots.insert(name.clone(), current.clone());
changed.push((name.clone(), current));
}
changed
}
fn register_pending_fonts(&mut self) {
let Some(registered) = self.registered_fonts.as_ref() else {
return;
};
let pairs: Vec<(String, String)> = {
let guard = self.shared_fonts.0.lock().expect("SharedFontMap poisoned");
guard
.keys()
.filter(|pair| !self.registered_faces.contains(*pair))
.cloned()
.collect()
};
if pairs.is_empty() {
return;
}
for (folder, filename) in pairs {
registered.register_font(&folder, &filename);
self.registered_faces.insert((folder, filename));
}
}
fn install_font_fallbacks_if_needed(&mut self, fallbacks: &[String]) {
if self.fallbacks_installed {
return;
}
let has_fonts = {
let guard = self.shared_fonts.0.lock().expect("SharedFontMap poisoned");
!guard.is_empty()
};
if !has_fonts {
return;
}
let refs: Vec<&str> = fallbacks.iter().map(String::as_str).collect();
noesis_runtime::font_provider::set_font_fallbacks(&refs);
noesis_runtime::font_provider::set_font_default_properties(12.0, 400, 5, 0);
info!(
"NoesisRenderState: font fallbacks installed: {:?}",
fallbacks,
);
self.fallbacks_installed = true;
}
fn shared_map(&self) -> &SharedXamlMap {
&self.shared_map
}
fn shared_fonts(&self) -> &SharedFontMap {
&self.shared_fonts
}
fn shared_images(&self) -> &SharedImageMap {
&self.shared_images
}
fn ensure_scene(&mut self, entity: Entity, config: &NoesisView) {
if config.xaml_uri.is_empty() {
self.teardown_scene(entity);
return;
}
if config.size.x == 0 || config.size.y == 0 {
return;
}
let current_bytes = {
let guard = self.shared_map.0.lock().expect("SharedXamlMap poisoned");
guard.get(&config.xaml_uri).cloned()
};
let bytes_changed = matches!(
(self.scenes.get(&entity), ¤t_bytes),
(Some(scene), Some(bytes))
if scene.built_for_uri == config.xaml_uri
&& !Arc::ptr_eq(&scene.built_bytes, bytes)
);
if !bytes_changed
&& let Some(scene) = self.scenes.get_mut(&entity)
&& scene.built_for_uri == config.xaml_uri
&& scene.size != config.size
{
scene.view.set_size(config.size.x, config.size.y);
scene.intermediates = [
create_intermediate(&self.device, config.size),
create_intermediate(&self.device, config.size),
];
scene.size = config.size;
return;
}
let up_to_date = !bytes_changed
&& self
.scenes
.get(&entity)
.is_some_and(|s| s.built_for_uri == config.xaml_uri && s.size == config.size);
if up_to_date {
return;
}
let Some(current_bytes) = current_bytes else {
return;
};
for folder in &config.wait_for_fonts {
let guard = self.shared_fonts.0.lock().expect("SharedFontMap poisoned");
let have = guard.keys().any(|(f, _)| f == folder);
if !have {
return;
}
}
for (folder, filename) in &config.wait_for_font_files {
let guard = self.shared_fonts.0.lock().expect("SharedFontMap poisoned");
let have = guard.contains_key(&(folder.clone(), filename.clone()));
if !have {
return;
}
}
for uri in &config.wait_for_images {
let guard = self
.shared_images
.0
.lock()
.expect("SharedImageMap poisoned");
if !guard.contains_key(uri) {
return;
}
}
{
let guard = self.shared_map.0.lock().expect("SharedXamlMap poisoned");
for uri in &config.application_resources {
if !guard.contains_key(uri) {
return;
}
}
}
self.teardown_scene(entity);
self.register_pending_fonts();
self.install_font_fallbacks_if_needed(&config.font_fallbacks);
let Some(element) = FrameworkElement::load(&config.xaml_uri) else {
warn!(
"FrameworkElement::load({:?}) returned None despite bytes being in the registry",
config.xaml_uri,
);
return;
};
let mut view = View::create(element);
view.set_size(config.size.x, config.size.y);
view.set_scale(config.scale);
let initial_flags = flags_from(config);
view.set_flags(initial_flags);
view.activate();
let intermediates = [
create_intermediate(&self.device, config.size),
create_intermediate(&self.device, config.size),
];
info!(
"NoesisRenderState: scene built — view + intermediate at {}x{} for uri {:?}",
config.size.x, config.size.y, config.xaml_uri,
);
self.scenes.insert(
entity,
SceneInstance {
view,
renderer_initialized: false,
intermediates,
write_index: 0,
size: config.size,
built_for_uri: config.xaml_uri.clone(),
built_bytes: current_bytes,
applied_flags: initial_flags,
applied_scale: config.scale,
click_subs: HashMap::new(),
keydown_subs: HashMap::new(),
event_subs: HashMap::new(),
row_click_subs: HashMap::new(),
text_snapshots: HashMap::new(),
dp_snapshots: HashMap::new(),
transform_handles: HashMap::new(),
transform_snapshots: HashMap::new(),
transform3d_handles: HashMap::new(),
transform3d_snapshots: HashMap::new(),
matrix_transform3d_handles: HashMap::new(),
matrix_transform3d_snapshots: HashMap::new(),
brush_snapshots: HashMap::new(),
typo_snapshots: HashMap::new(),
input_bindings: HashMap::new(),
predict_snapshots: HashMap::new(),
image_snapshots: HashMap::new(),
inline_handles: HashMap::new(),
inlines_snapshots: HashMap::new(),
},
);
self.scenes_built_this_frame.insert(entity);
}
pub(crate) fn scene_rebuilt_this_frame(&self, entity: Entity) -> bool {
self.scenes_built_this_frame.contains(&entity)
}
pub(crate) fn panel_mounted_this_frame(&self, entity: Entity) -> bool {
self.panels_mounted_this_frame.contains(&entity)
}
pub(crate) fn apply_visibility_for(&mut self, entity: Entity, desired: &HashMap<String, bool>) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, &visible) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisVisibility: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
element.set_visibility(visible);
}
}
pub(crate) fn apply_layout_for(&mut self, entity: Entity, desired: &HashMap<String, [f32; 4]>) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
if let Some(panel) = self.panels.get(&entity) {
for (name, &[left, top, right, bottom]) in desired {
let Some(mut element) = resolve_named(&panel.fragment, name) else {
warn!("NoesisLayout: x:Name {name:?} not found in panel fragment");
continue;
};
element.set_margin(left, top, right, bottom);
}
}
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, &[left, top, right, bottom]) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisLayout: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
element.set_margin(left, top, right, bottom);
}
}
pub(crate) fn apply_visual_state_for(
&mut self,
entity: Entity,
desired: &HashMap<String, (String, bool)>,
) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, (state, use_transitions)) in desired {
let Some(element) = resolve_named(&content, name) else {
warn!(
"NoesisVisualState: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
if !element.go_to_state(state, *use_transitions) {
warn!(
"NoesisVisualState: GoToState({state:?}) failed for {name:?} \
in scene {:?} (not a templated control, or unknown state)",
scene.built_for_uri,
);
}
}
}
pub(crate) fn begin_animations_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::animation::AnimationSpec>,
) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, spec) in desired {
let Some(element) = resolve_named(&content, name) else {
warn!(
"NoesisAnimation: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let mut anim = DoubleAnimation::new();
let _ = anim.set_from(spec.from);
let _ = anim.set_to(Some(spec.to));
let _ = anim.set_duration_secs(spec.duration_secs);
if !anim.begin_on(
&element,
&spec.property,
noesis_runtime::animation::HandoffBehavior::SnapshotAndReplace,
) {
warn!(
"NoesisAnimation: begin_on({:?}) failed for {name:?} in scene {:?} \
(unknown / non-float property, or disconnected target)",
spec.property, scene.built_for_uri,
);
}
}
}
pub(crate) fn sync_click_subscriptions_for(
&mut self,
entity: Entity,
entries: &[crate::events::ClickWatchEntry],
queue: &SharedClickQueue,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
if self.panels.contains_key(&entity) {
self.sync_click_subs_panel(entity, entries, queue);
}
return;
};
scene
.click_subs
.retain(|k, _| entries.iter().any(|e| &e.name == k));
let needs_change = entries.iter().any(|e| {
let target = e.target.unwrap_or(entity);
!scene.click_subs.contains_key(&e.name)
|| self
.last_click_target
.get(&(entity, e.name.clone()))
.is_none_or(|prev| *prev != target)
});
if !needs_change {
return;
}
let Some(content) = scene.view.content() else {
return;
};
for entry in entries {
let target = entry.target.unwrap_or(entity);
if scene.click_subs.contains_key(&entry.name)
&& self
.last_click_target
.get(&(entity, entry.name.clone()))
.is_some_and(|prev| *prev == target)
{
continue;
}
let Some(element) = resolve_named(&content, &entry.name) else {
warn!(
"NoesisClickWatch: x:Name {:?} not found in scene {:?}",
entry.name, scene.built_for_uri,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = entry.name.clone();
let Some(sub) = subscribe_click(&element, move || {
queue_handle.push(entity, target, captured_name.clone());
}) else {
warn!(
"NoesisClickWatch: element {:?} is not a BaseButton; skipping",
entry.name
);
continue;
};
scene.click_subs.insert(entry.name.clone(), sub);
self.last_click_target
.insert((entity, entry.name.clone()), target);
}
self.last_click_target
.retain(|(ent, name), _| *ent != entity || entries.iter().any(|e| &e.name == name));
}
fn sync_click_subs_panel(
&mut self,
entity: Entity,
entries: &[crate::events::ClickWatchEntry],
queue: &SharedClickQueue,
) {
let Some(panel) = self.panels.get_mut(&entity) else {
return;
};
let host = panel.host;
panel
.click_subs
.retain(|k, _| entries.iter().any(|e| &e.name == k));
let needs_change = entries.iter().any(|e| {
let target = e.target.unwrap_or(entity);
!panel.click_subs.contains_key(&e.name)
|| self
.last_click_target
.get(&(entity, e.name.clone()))
.is_none_or(|prev| *prev != target)
});
if !needs_change {
return;
}
for entry in entries {
let target = entry.target.unwrap_or(entity);
if panel.click_subs.contains_key(&entry.name)
&& self
.last_click_target
.get(&(entity, entry.name.clone()))
.is_some_and(|prev| *prev == target)
{
continue;
}
let Some(element) = resolve_named(&panel.fragment, &entry.name) else {
warn!(
"NoesisClickWatch: x:Name {:?} not found in panel fragment (host {host:?})",
entry.name,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = entry.name.clone();
let Some(sub) = subscribe_click(&element, move || {
queue_handle.push(host, target, captured_name.clone());
}) else {
warn!(
"NoesisClickWatch: element {:?} is not a BaseButton; skipping",
entry.name
);
continue;
};
panel.click_subs.insert(entry.name.clone(), sub);
self.last_click_target
.insert((entity, entry.name.clone()), target);
}
self.last_click_target
.retain(|(ent, name), _| *ent != entity || entries.iter().any(|e| &e.name == name));
}
pub(crate) fn sync_keydown_subscriptions_for(
&mut self,
entity: Entity,
entries: &[crate::events::KeyDownWatchEntry],
queue: &SharedKeyDownQueue,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
if self.panels.contains_key(&entity) {
self.sync_keydown_subs_panel(entity, entries, queue);
}
return;
};
scene
.keydown_subs
.retain(|k, _| entries.iter().any(|e| e.name == *k));
for entry in entries {
let target = entry.target.unwrap_or(entity);
if scene.keydown_subs.contains_key(&entry.name)
&& self
.last_keydown_swallow
.get(&(entity, entry.name.clone()))
.is_some_and(|(swallow, prev_target)| {
swallow == &entry.swallow && *prev_target == target
})
{
continue;
}
let Some(content) = scene.view.content() else {
return;
};
let Some(element) = resolve_named(&content, &entry.name) else {
warn!(
"NoesisKeyDownWatch: x:Name {:?} not found in scene {:?}",
entry.name, scene.built_for_uri,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = entry.name.clone();
let swallow = entry.swallow.clone();
let Some(sub) = subscribe_keydown(&element, move |key: Key| {
queue_handle.push(entity, target, captured_name.clone(), key);
swallow.contains(&key)
}) else {
warn!(
"NoesisKeyDownWatch: element {:?} is not a UIElement; skipping",
entry.name
);
continue;
};
scene.keydown_subs.insert(entry.name.clone(), sub);
self.last_keydown_swallow.insert(
(entity, entry.name.clone()),
(entry.swallow.clone(), target),
);
}
self.last_keydown_swallow
.retain(|(ent, name), _| *ent != entity || entries.iter().any(|e| &e.name == name));
}
fn sync_keydown_subs_panel(
&mut self,
entity: Entity,
entries: &[crate::events::KeyDownWatchEntry],
queue: &SharedKeyDownQueue,
) {
let Some(panel) = self.panels.get_mut(&entity) else {
return;
};
let host = panel.host;
panel
.keydown_subs
.retain(|k, _| entries.iter().any(|e| e.name == *k));
for entry in entries {
let target = entry.target.unwrap_or(entity);
if panel.keydown_subs.contains_key(&entry.name)
&& self
.last_keydown_swallow
.get(&(entity, entry.name.clone()))
.is_some_and(|(swallow, prev_target)| {
swallow == &entry.swallow && *prev_target == target
})
{
continue;
}
let Some(element) = resolve_named(&panel.fragment, &entry.name) else {
warn!(
"NoesisKeyDownWatch: x:Name {:?} not found in panel fragment (host {host:?})",
entry.name,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = entry.name.clone();
let swallow = entry.swallow.clone();
let Some(sub) = subscribe_keydown(&element, move |key: Key| {
queue_handle.push(host, target, captured_name.clone(), key);
swallow.contains(&key)
}) else {
warn!(
"NoesisKeyDownWatch: element {:?} is not a UIElement; skipping",
entry.name
);
continue;
};
panel.keydown_subs.insert(entry.name.clone(), sub);
self.last_keydown_swallow.insert(
(entity, entry.name.clone()),
(entry.swallow.clone(), target),
);
}
self.last_keydown_swallow
.retain(|(ent, name), _| *ent != entity || entries.iter().any(|e| &e.name == name));
}
pub(crate) fn sync_event_subscriptions_for(
&mut self,
entity: Entity,
entries: &[crate::routed_events::EventWatchEntry],
queue: &SharedRoutedEventQueue,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
if self.panels.contains_key(&entity) {
self.sync_event_subs_panel(entity, entries, queue);
}
return;
};
scene.event_subs.retain(|(name, evname), _| {
entries
.iter()
.any(|e| e.name == *name && e.event.as_str() == *evname)
});
for entry in entries {
let evname = entry.event.as_str();
let key = (entry.name.clone(), evname);
let target = entry.target.unwrap_or(entity);
if scene.event_subs.contains_key(&key)
&& self
.last_event_config
.get(&(entity, entry.name.clone(), evname))
.is_some_and(|prev| *prev == (entry.mark_handled, entry.handled_too, target))
{
continue;
}
let Some(content) = scene.view.content() else {
return;
};
let Some(element) = resolve_named(&content, &entry.name) else {
warn!(
"NoesisEventWatch: x:Name {:?} not found in scene {:?}",
entry.name, scene.built_for_uri,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = entry.name.clone();
let captured_event = entry.event;
let mark_handled = entry.mark_handled;
let Some(sub) = subscribe_event(
&element,
entry.event,
entry.handled_too,
move |args: &EventArgs| {
let snapshot = RoutedEventSnapshot::capture(args);
queue_handle.push(
entity,
target,
captured_name.clone(),
captured_event,
snapshot,
);
mark_handled
},
) else {
warn!(
"NoesisEventWatch: element {:?} not a UIElement / event {:?} unknown; skipping",
entry.name, evname,
);
continue;
};
scene.event_subs.insert(key, sub);
self.last_event_config.insert(
(entity, entry.name.clone(), evname),
(entry.mark_handled, entry.handled_too, target),
);
}
self.last_event_config.retain(|(ent, name, evname), _| {
*ent != entity
|| entries
.iter()
.any(|e| &e.name == name && e.event.as_str() == *evname)
});
}
fn sync_event_subs_panel(
&mut self,
entity: Entity,
entries: &[crate::routed_events::EventWatchEntry],
queue: &SharedRoutedEventQueue,
) {
let Some(panel) = self.panels.get_mut(&entity) else {
return;
};
let host = panel.host;
panel.event_subs.retain(|(name, evname), _| {
entries
.iter()
.any(|e| e.name == *name && e.event.as_str() == *evname)
});
for entry in entries {
let evname = entry.event.as_str();
let key = (entry.name.clone(), evname);
let target = entry.target.unwrap_or(entity);
if panel.event_subs.contains_key(&key)
&& self
.last_event_config
.get(&(entity, entry.name.clone(), evname))
.is_some_and(|prev| *prev == (entry.mark_handled, entry.handled_too, target))
{
continue;
}
let Some(element) = resolve_named(&panel.fragment, &entry.name) else {
warn!(
"NoesisEventWatch: x:Name {:?} not found in panel fragment (host {host:?})",
entry.name,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = entry.name.clone();
let captured_event = entry.event;
let mark_handled = entry.mark_handled;
let Some(sub) = subscribe_event(
&element,
entry.event,
entry.handled_too,
move |args: &EventArgs| {
let snapshot = RoutedEventSnapshot::capture(args);
queue_handle.push(
host,
target,
captured_name.clone(),
captured_event,
snapshot,
);
mark_handled
},
) else {
warn!(
"NoesisEventWatch: element {:?} not a UIElement / event {:?} unknown; skipping",
entry.name, evname,
);
continue;
};
panel.event_subs.insert(key, sub);
self.last_event_config.insert(
(entity, entry.name.clone(), evname),
(entry.mark_handled, entry.handled_too, target),
);
}
self.last_event_config.retain(|(ent, name, evname), _| {
*ent != entity
|| entries
.iter()
.any(|e| &e.name == name && e.event.as_str() == *evname)
});
}
pub(crate) fn apply_text_writes_for(
&mut self,
entity: Entity,
set: &std::collections::HashMap<String, String>,
) {
if set.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, text) in set {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisText: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
if !element.set_text(text) {
warn!("NoesisText: element {name:?} is not a TextBox/TextBlock; set_text skipped");
continue;
}
scene.text_snapshots.insert(name.clone(), text.clone());
}
}
pub(crate) fn apply_typography_for(
&mut self,
entity: Entity,
set: &HashMap<String, crate::typography::FontStyling>,
) {
if set.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, styling) in set {
if styling.is_empty() {
continue;
}
let Some(element) = resolve_named(&content, name) else {
warn!(
"NoesisTypography: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
if let Some(size) = styling.font_size
&& !noesis_runtime::typography::set_font_size(&element, size)
{
debug!("NoesisTypography: {name:?} did not accept FontSize");
}
if let Some(source) = &styling.font_family {
let family = noesis_runtime::typography::FontFamily::new(source);
if !noesis_runtime::typography::set_font_family(&element, &family) {
debug!("NoesisTypography: {name:?} did not accept FontFamily");
}
}
if let Some(weight) = styling.font_weight
&& !noesis_runtime::typography::set_font_weight(&element, weight)
{
debug!("NoesisTypography: {name:?} did not accept FontWeight");
}
if let Some(style) = styling.font_style
&& !noesis_runtime::typography::set_font_style(&element, style)
{
debug!("NoesisTypography: {name:?} did not accept FontStyle");
}
if let Some(stretch) = styling.font_stretch
&& !noesis_runtime::typography::set_font_stretch(&element, stretch)
{
debug!("NoesisTypography: {name:?} did not accept FontStretch");
}
}
}
pub(crate) fn poll_typography_reads_for(
&mut self,
entity: Entity,
watched: &[crate::typography::TypographyWatch],
) -> Vec<(String, crate::typography::TypographyValue)> {
use crate::typography::{TypographyField, TypographyValue};
use noesis_runtime::typography as ty;
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene.typo_snapshots.retain(|(name, field), _| {
watched.iter().any(|w| &w.name == name && w.field == *field)
});
if watched.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for watch in watched {
let Some(element) = resolve_named(&content, &watch.name) else {
continue;
};
let current = match watch.field {
TypographyField::FontSize => ty::font_size(&element).map(TypographyValue::FontSize),
TypographyField::FontFamily => {
ty::get_font_family(&element).map(|f| TypographyValue::FontFamily(f.source()))
}
TypographyField::FontWeight => {
ty::font_weight(&element).map(TypographyValue::FontWeight)
}
TypographyField::FontStyle => {
ty::font_style(&element).map(TypographyValue::FontStyle)
}
TypographyField::FontStretch => {
ty::font_stretch(&element).map(TypographyValue::FontStretch)
}
};
let Some(current) = current else {
continue;
};
let key = (watch.name.clone(), watch.field);
if scene.typo_snapshots.get(&key) == Some(¤t) {
continue;
}
scene.typo_snapshots.insert(key, current.clone());
changed.push((watch.name.clone(), current));
}
changed
}
pub(crate) fn apply_inlines_for(
&mut self,
entity: Entity,
set: &HashMap<String, Vec<crate::inlines::InlineSpec>>,
) {
if set.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, specs) in set {
if specs.is_empty() {
continue;
}
let Some(element) = resolve_named(&content, name) else {
warn!(
"NoesisInlines: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let Some(mut collection) = noesis_runtime::text_inlines::text_block_inlines(&element)
else {
warn!("NoesisInlines: element {name:?} is not a TextBlock; skipped");
continue;
};
if collection.count() != 0 {
collection.clear();
}
scene.inline_handles.remove(name);
let built = crate::inlines::build_into(&mut collection, specs);
scene.inline_handles.insert(name.clone(), built);
}
}
pub(crate) fn poll_inlines_reads_for(
&mut self,
entity: Entity,
watched: &[String],
) -> Vec<(String, crate::inlines::InlinesReadback)> {
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.inlines_snapshots
.retain(|name, _| watched.iter().any(|w| w == name));
if watched.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for name in watched {
let Some(element) = resolve_named(&content, name) else {
continue;
};
let Some(collection) = noesis_runtime::text_inlines::text_block_inlines(&element)
else {
continue;
};
let empty = Vec::new();
let tree = scene.inline_handles.get(name).unwrap_or(&empty);
let current = crate::inlines::readback(tree, &collection);
if scene.inlines_snapshots.get(name) == Some(¤t) {
continue;
}
scene
.inlines_snapshots
.insert(name.clone(), current.clone());
changed.push((name.clone(), current));
}
changed
}
pub(crate) fn apply_geometry_for(
&mut self,
entity: Entity,
desired: &HashMap<String, Vec<[f32; 2]>>,
) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
if let Some(panel) = self.panels.get(&entity) {
for (name, points) in desired {
let Some(mut element) = resolve_named(&panel.fragment, name) else {
warn!("NoesisGeometry: x:Name {name:?} not found in panel fragment");
continue;
};
if !element.set_path_points(points) {
warn!(
"NoesisGeometry: element {name:?} is not a Path (or < 2 points); skipped"
);
}
}
}
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, points) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisGeometry: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
if !element.set_path_points(points) {
warn!("NoesisGeometry: element {name:?} is not a Path (or < 2 points); skipped",);
}
}
}
pub(crate) fn apply_clip_for(
&mut self,
entity: Entity,
desired: &HashMap<String, Vec<[f32; 2]>>,
) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
if let Some(panel) = self.panels.get(&entity) {
for (name, points) in desired {
let Some(mut element) = resolve_named(&panel.fragment, name) else {
warn!("NoesisClip: x:Name {name:?} not found in panel fragment");
continue;
};
if !element.set_clip_points(points) {
warn!("NoesisClip: element {name:?} clip not set (degenerate points)");
}
}
}
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, points) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisClip: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
if !element.set_clip_points(points) {
warn!("NoesisClip: element {name:?} clip not set (degenerate points)");
}
}
}
pub(crate) fn apply_shapes_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::shapes::ShapeSpec>,
) {
use crate::shapes::ShapeKind;
use noesis_runtime::brushes::SolidColorBrush;
use noesis_runtime::shapes::{Ellipse, Line, Rectangle, Shape};
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
fn finish<S: Shape>(
mut shape: S,
spec: &crate::shapes::ShapeSpec,
) -> noesis_runtime::view::FrameworkElement {
if let Some(rgba) = spec.fill {
shape.set_fill(&SolidColorBrush::new(rgba));
}
if let Some(rgba) = spec.stroke {
shape.set_stroke(&SolidColorBrush::new(rgba));
}
if let Some(t) = spec.stroke_thickness {
shape.set_stroke_thickness(t);
}
shape.as_element()
}
for (name, spec) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisShapes: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let shape_el = match spec.kind {
ShapeKind::Rectangle {
width,
height,
radius_x,
radius_y,
} => {
let mut r = Rectangle::new();
r.set_width(width);
r.set_height(height);
r.set_radius_x(radius_x);
r.set_radius_y(radius_y);
finish(r, spec)
}
ShapeKind::Ellipse { width, height } => {
let mut e = Ellipse::new();
e.set_width(width);
e.set_height(height);
finish(e, spec)
}
ShapeKind::Line { x1, y1, x2, y2 } => {
let mut l = Line::new();
l.set_points(x1, y1, x2, y2);
finish(l, spec)
}
};
if !element.set_content(&shape_el) && !element.set_decorator_child(&shape_el) {
warn!(
"NoesisShapes: container {name:?} accepts neither Content nor a decorator Child; skipped",
);
}
}
}
pub(crate) fn apply_svg_for(
&mut self,
entity: Entity,
desired: &HashMap<String, String>,
) -> Vec<(String, [f32; 4])> {
use noesis_runtime::svg::SvgPath;
let mut applied = Vec::new();
if desired.is_empty() {
return applied;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return applied;
};
let Some(content) = scene.view.content() else {
return applied;
};
for (name, source) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisSvg: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let Some(path) = SvgPath::parse(source) else {
warn!("NoesisSvg: source for {name:?} failed to parse; skipped");
continue;
};
let bounds = path.bounds();
if !element.set_width(bounds[2]) || !element.set_height(bounds[3]) {
warn!(
"NoesisSvg: element {name:?} did not accept Width/Height; bounds still reported"
);
}
applied.push((name.clone(), bounds));
}
applied
}
pub(crate) fn apply_focus_for(&mut self, entity: Entity, target: Option<&str>) {
let Some(name) = target else {
return;
};
let Some(scene) = self.scenes.get_mut(&entity) else {
if let Some(panel) = self.panels.get(&entity) {
match resolve_named(&panel.fragment, name) {
Some(mut element) => {
if !element.focus() {
warn!("NoesisFocus: element {name:?} refused focus (non-focusable?)");
}
}
None => warn!("NoesisFocus: x:Name {name:?} not found in panel fragment"),
}
}
return;
};
let Some(content) = scene.view.content() else {
return;
};
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisFocus: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
return;
};
if !element.focus() {
warn!("NoesisFocus: element {name:?} refused focus (non-focusable?)");
}
}
pub(crate) fn apply_focus_moves_for(
&mut self,
entity: Entity,
moves: &[crate::focus_input::FocusMove],
) -> bool {
if moves.is_empty() {
return true;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
let Some(panel) = self.panels.get(&entity) else {
return false;
};
if panel.mounted_for_uri.is_none() {
return false;
}
for m in moves {
let Some(mut element) = resolve_named(&panel.fragment, &m.from) else {
warn!(
"NoesisFocusControl: move-from x:Name {:?} not found in panel fragment",
m.from,
);
continue;
};
if !element.move_focus(m.direction, m.wrapped) {
warn!(
"NoesisFocusControl: MoveFocus({:?}, wrapped={}) from {:?} moved nothing",
m.direction, m.wrapped, m.from,
);
}
}
return true;
};
let Some(content) = scene.view.content() else {
return false;
};
for m in moves {
let Some(mut element) = resolve_named(&content, &m.from) else {
warn!(
"NoesisFocusControl: move-from x:Name {:?} not found in scene {:?}",
m.from, scene.built_for_uri,
);
continue;
};
if !element.move_focus(m.direction, m.wrapped) {
warn!(
"NoesisFocusControl: MoveFocus({:?}, wrapped={}) from {:?} moved nothing",
m.direction, m.wrapped, m.from,
);
}
}
true
}
pub(crate) fn apply_focus_engages_for(
&mut self,
entity: Entity,
engages: &[crate::focus_input::FocusEngage],
) -> bool {
if engages.is_empty() {
return true;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
let Some(panel) = self.panels.get(&entity) else {
return false;
};
if panel.mounted_for_uri.is_none() {
return false;
}
for e in engages {
let Some(mut element) = resolve_named(&panel.fragment, &e.name) else {
warn!(
"NoesisFocusControl: engage x:Name {:?} not found in panel fragment",
e.name,
);
continue;
};
if !element.focus_engage(e.engage) {
warn!(
"NoesisFocusControl: element {:?} refused focus(engage={})",
e.name, e.engage,
);
}
}
return true;
};
let Some(content) = scene.view.content() else {
return false;
};
for e in engages {
let Some(mut element) = resolve_named(&content, &e.name) else {
warn!(
"NoesisFocusControl: engage x:Name {:?} not found in scene {:?}",
e.name, scene.built_for_uri,
);
continue;
};
if !element.focus_engage(e.engage) {
warn!(
"NoesisFocusControl: element {:?} refused focus(engage={})",
e.name, e.engage,
);
}
}
true
}
pub(crate) fn sync_key_bindings_for(
&mut self,
entity: Entity,
specs: &[crate::focus_input::KeyBindingSpec],
queue: &crate::focus_input::SharedFocusBindingQueue,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let dropped: Vec<(String, i32, i32)> = scene
.input_bindings
.keys()
.filter(|k| !specs.iter().any(|s| &s.ident() == *k))
.cloned()
.collect();
let needs_new = specs
.iter()
.any(|s| !scene.input_bindings.contains_key(&s.ident()));
if dropped.is_empty() && !needs_new {
return;
}
let Some(content) = scene.view.content() else {
return;
};
for ident in dropped {
if let Some(installed) = scene.input_bindings.remove(&ident)
&& let Some(element) = resolve_named(&content, &ident.0)
{
installed.binding.remove_from(&element);
}
}
for spec in specs {
let ident = spec.ident();
if scene.input_bindings.contains_key(&ident) {
continue;
}
let Some(element) = resolve_named(&content, &spec.name) else {
warn!(
"NoesisFocusControl: binding x:Name {:?} not found in scene {:?}",
spec.name, scene.built_for_uri,
);
continue;
};
let queue_handle = queue.clone();
let view = entity;
let name = spec.name.clone();
let key = spec.key;
let modifiers = spec.modifiers;
let command = Command::new(move |_param| {
queue_handle.push(view, name.clone(), key, modifiers);
});
let Some(binding) = KeyBinding::new(&command, spec.key, spec.modifiers) else {
warn!(
"NoesisFocusControl: could not build KeyBinding for {:?} (command not an ICommand?)",
spec.name,
);
continue;
};
if !binding.add_to(&element) {
warn!(
"NoesisFocusControl: element {:?} is not a UIElement; binding skipped",
spec.name,
);
continue;
}
scene
.input_bindings
.insert(ident, InstalledKeyBinding { command, binding });
}
}
pub(crate) fn poll_focus_predictions_for(
&mut self,
entity: Entity,
predicts: &[crate::focus_input::FocusPredict],
) -> Vec<(
String,
crate::focus_input::FocusNavigationDirection,
bool,
Option<String>,
bool,
)> {
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.predict_snapshots
.retain(|k, _| predicts.iter().any(|p| &p.ident() == k));
if predicts.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for p in predicts {
let Some(from) = resolve_named(&content, &p.from) else {
continue;
};
let candidate = from.predict_focus(p.direction).is_some();
let predicted_name = from.predict_focus_name(p.direction);
let matches_expected = match &p.expect {
Some(expect) => predicted_name.as_deref() == Some(expect.as_str()),
None => false,
};
let ident = p.ident();
let snapshot = (candidate, predicted_name.clone(), matches_expected);
if scene.predict_snapshots.get(&ident) == Some(&snapshot) {
continue;
}
scene.predict_snapshots.insert(ident, snapshot);
changed.push((
p.from.clone(),
p.direction,
candidate,
predicted_name,
matches_expected,
));
}
changed
}
pub(crate) fn poll_text_reads_for(
&mut self,
entity: Entity,
watched: &[String],
) -> Vec<(String, String)> {
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.text_snapshots
.retain(|k, _| watched.iter().any(|w| w == k));
if watched.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for name in watched {
let Some(element) = resolve_named(&content, name) else {
continue;
};
let current = element.text().unwrap_or_default();
if scene.text_snapshots.get(name) == Some(¤t) {
continue;
}
scene.text_snapshots.insert(name.clone(), current.clone());
changed.push((name.clone(), current));
}
changed
}
pub(crate) fn apply_dp_for(
&mut self,
entity: Entity,
desired: &HashMap<(String, String), crate::dp::DpValue>,
) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for ((name, property), value) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisDp: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
record_ffi_hop();
if value.write_to(&mut element, property) {
scene
.dp_snapshots
.insert((name.clone(), property.clone()), value.clone());
} else {
warn!("NoesisDp: write to {name:?}.{property:?} failed (unknown property or type)");
}
}
}
pub(crate) fn poll_dp_reads_for(
&mut self,
entity: Entity,
watched: &[crate::dp::DpWatch],
) -> Vec<(String, String, crate::dp::DpValue)> {
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene.dp_snapshots.retain(|(name, property), _| {
watched
.iter()
.any(|w| &w.name == name && &w.property == property)
});
if watched.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for watch in watched {
let Some(element) = resolve_named(&content, &watch.name) else {
continue;
};
record_ffi_hop();
let Some(current) = watch.kind.read_from(&element, &watch.property) else {
continue;
};
let key = (watch.name.clone(), watch.property.clone());
if scene.dp_snapshots.get(&key) == Some(¤t) {
continue;
}
scene.dp_snapshots.insert(key, current.clone());
changed.push((watch.name.clone(), watch.property.clone(), current));
}
changed
}
pub(crate) fn apply_transforms_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::transforms::TransformSpec>,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
if let Some(panel) = self.panels.get(&entity) {
for (name, spec) in desired {
let Some(mut element) = resolve_named(&panel.fragment, name) else {
warn!("NoesisTransform: x:Name {name:?} not found in panel fragment");
continue;
};
let transform = CompositeTransform::new(spec.to_fields());
if !element.set_render_transform(&transform) {
warn!(
"NoesisTransform: {name:?} has no RenderTransform (not a UIElement?) in panel fragment"
);
}
}
}
return;
};
scene
.transform_handles
.retain(|k, _| desired.contains_key(k));
if desired.is_empty() {
return;
}
let Some(content) = scene.view.content() else {
return;
};
for (name, spec) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisTransform: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let transform = CompositeTransform::new(spec.to_fields());
if element.set_render_transform(&transform) {
scene.transform_handles.insert(name.clone(), transform);
} else {
warn!(
"NoesisTransform: {name:?} has no RenderTransform (not a UIElement?) \
in scene {:?}",
scene.built_for_uri,
);
}
}
}
pub(crate) fn apply_transforms3d_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::transforms3d::Transform3DSpec>,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
scene
.transform3d_handles
.retain(|k, _| desired.contains_key(k));
if desired.is_empty() {
return;
}
let Some(content) = scene.view.content() else {
return;
};
for (name, spec) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisTransform3D: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let transform = CompositeTransform3D::new(spec.to_fields());
if element.set_transform3d(&transform) {
scene.transform3d_handles.insert(name.clone(), transform);
} else {
warn!(
"NoesisTransform3D: {name:?} has no Transform3D (not a UIElement?) \
in scene {:?}",
scene.built_for_uri,
);
}
}
}
pub(crate) fn poll_transforms3d_for(
&mut self,
entity: Entity,
names: &[&str],
) -> Vec<(String, crate::transforms3d::Transform3DSpec)> {
use crate::transforms3d::Transform3DSpec;
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.transform3d_snapshots
.retain(|name, _| names.contains(&name.as_str()));
if names.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for &name in names {
let Some(handle) = scene.transform3d_handles.get(name) else {
continue;
};
let Some(element) = resolve_named(&content, name) else {
continue;
};
let Some(live) = element.transform3d() else {
continue;
};
if live.raw() != handle.raw() {
continue;
}
let current = Transform3DSpec::from_fields(handle.get());
if scene.transform3d_snapshots.get(name) == Some(¤t) {
continue;
}
scene
.transform3d_snapshots
.insert(name.to_string(), current);
changed.push((name.to_string(), current));
}
changed
}
pub(crate) fn apply_matrix_transforms3d_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::transforms3d::Matrix3DSpec>,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
scene
.matrix_transform3d_handles
.retain(|k, _| desired.contains_key(k));
if desired.is_empty() {
return;
}
let Some(content) = scene.view.content() else {
return;
};
for (name, spec) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisTransform3D(matrix): x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let transform = MatrixTransform3D::new(spec.rows);
if element.set_transform3d(&transform) {
scene
.matrix_transform3d_handles
.insert(name.clone(), transform);
} else {
warn!(
"NoesisTransform3D(matrix): {name:?} has no Transform3D (not a UIElement?) \
in scene {:?}",
scene.built_for_uri,
);
}
}
}
pub(crate) fn poll_matrix_transforms3d_for(
&mut self,
entity: Entity,
names: &[&str],
) -> Vec<(String, [f32; 12])> {
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.matrix_transform3d_snapshots
.retain(|name, _| names.contains(&name.as_str()));
if names.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for &name in names {
let Some(handle) = scene.matrix_transform3d_handles.get(name) else {
continue;
};
let Some(element) = resolve_named(&content, name) else {
continue;
};
let Some(live) = element.transform3d() else {
continue;
};
if live.raw() != handle.raw() {
continue;
}
let current = handle.get();
if scene.matrix_transform3d_snapshots.get(name) == Some(¤t) {
continue;
}
scene
.matrix_transform3d_snapshots
.insert(name.to_string(), current);
changed.push((name.to_string(), current));
}
changed
}
pub(crate) fn apply_brushes_for(
&mut self,
entity: Entity,
desired: &HashMap<(String, crate::brushes::BrushTarget), crate::brushes::BrushSpec>,
) {
use crate::brushes::{BrushSpec, BrushTarget};
use noesis_runtime::brushes::{GradientStop, LinearGradientBrush, SolidColorBrush};
use noesis_runtime::view::FrameworkElement;
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
fn assign(
target: BrushTarget,
el: &mut FrameworkElement,
brush: &impl noesis_runtime::brushes::Brush,
) -> bool {
match target {
BrushTarget::Background => el.set_background(brush),
BrushTarget::Foreground => el.set_foreground(brush),
BrushTarget::Fill => el.set_fill(brush),
BrushTarget::Stroke => el.set_stroke(brush),
}
}
for ((name, target), spec) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!(
"NoesisBrushes: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let ok = match spec {
BrushSpec::Solid(rgba) => {
let brush = SolidColorBrush::new(*rgba);
assign(*target, &mut element, &brush)
}
BrushSpec::LinearGradient { start, end, stops } => {
let mut brush = LinearGradientBrush::new();
brush.set_start_point(start[0], start[1]);
brush.set_end_point(end[0], end[1]);
for stop in stops {
brush.add_stop(GradientStop::new(stop.offset, stop.color));
}
assign(*target, &mut element, &brush)
}
};
if !ok {
warn!(
"NoesisBrushes: assigning {:?} to {name:?} failed (no such property on this element type)",
target.property(),
);
}
}
}
pub(crate) fn apply_styles_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::styles::StyleSpec>,
) {
if desired.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let uri = scene.built_for_uri.clone();
let Some(content) = scene.view.content() else {
return;
};
for (name, spec) in desired {
let Some(mut element) = resolve_named(&content, name) else {
warn!("NoesisStyles: x:Name {name:?} not found in scene {uri:?}");
continue;
};
let Some(style) = build_noesis_style(spec, name, &uri) else {
continue;
};
if !element.set_style(&style) {
warn!("NoesisStyles: {name:?} is not a FrameworkElement in scene {uri:?}");
}
}
}
pub(crate) fn reconcile_app_resources(
&mut self,
entries: &HashMap<String, crate::resources::ResourceEntry>,
merged_xaml: &[String],
chain_uris: &[String],
wait_fonts: &[String],
wait_font_files: &[(String, String)],
) -> Option<Vec<String>> {
use crate::brushes::BrushSpec;
use crate::resources::ResourceEntry;
use noesis_runtime::brushes::{GradientStop, LinearGradientBrush, SolidColorBrush};
use noesis_runtime::resources::{
ResourceDictionary, application_resources_contains, set_application_resources,
};
if entries.is_empty() && merged_xaml.is_empty() && chain_uris.is_empty() {
return None;
}
if self.installed_app_resources.as_ref().is_some_and(|s| {
s.entries == *entries && s.merged_xaml == merged_xaml && s.chain_uris == chain_uris
}) {
return None;
}
{
let guard = self.shared_map.0.lock().expect("SharedXamlMap poisoned");
if chain_uris.iter().any(|uri| guard.get(uri).is_none()) {
return None;
}
}
{
let guard = self.shared_fonts.0.lock().expect("SharedFontMap poisoned");
for folder in wait_fonts {
if !guard.keys().any(|(f, _)| f == folder) {
return None;
}
}
for pair in wait_font_files {
if !guard.contains_key(pair) {
return None;
}
}
}
self.register_pending_fonts();
let mut dict = ResourceDictionary::new();
set_application_resources(&dict);
for uri in chain_uris {
let mut child = ResourceDictionary::new();
if !dict.add_merged(&child) {
warn!("NoesisResources: failed to merge chain dictionary {uri:?}");
continue;
}
if !child.set_source(uri) {
warn!("NoesisResources: failed to set Source on chain dictionary {uri:?}");
}
}
for xaml in merged_xaml {
match ResourceDictionary::parse(xaml) {
Some(merged) => {
if !dict.add_merged(&merged) {
warn!("NoesisResources: failed to merge a parsed ResourceDictionary");
}
}
None => warn!(
"NoesisResources: a merged_xaml fragment did not parse as a <ResourceDictionary>",
),
}
}
for (key, entry) in entries {
let ok = match entry {
ResourceEntry::Value(value) => dict.add_boxed(key, &value.to_boxed()),
ResourceEntry::Brush(BrushSpec::Solid(rgba)) => {
dict.add_brush(key, &SolidColorBrush::new(*rgba))
}
ResourceEntry::Brush(BrushSpec::LinearGradient { start, end, stops }) => {
let mut brush = LinearGradientBrush::new();
brush.set_start_point(start[0], start[1]);
brush.set_end_point(end[0], end[1]);
for stop in stops {
brush.add_stop(GradientStop::new(stop.offset, stop.color));
}
dict.add_brush(key, &brush)
}
};
if !ok {
warn!("NoesisResources: failed to add resource {key:?}");
}
}
self.installed_app_resources = Some(AppResourcesSnapshot {
entries: entries.clone(),
merged_xaml: merged_xaml.to_vec(),
chain_uris: chain_uris.to_vec(),
});
let mut present: Vec<String> = entries
.keys()
.filter(|key| application_resources_contains(key))
.cloned()
.collect();
present.sort();
info!(
"Installed Noesis application resources: {} entries, {} merged dicts, {} chain dicts, {} present",
entries.len(),
merged_xaml.len(),
chain_uris.len(),
present.len(),
);
Some(present)
}
pub(crate) fn poll_transforms_for(
&mut self,
entity: Entity,
names: &[&str],
) -> Vec<(String, crate::transforms::TransformSpec)> {
use crate::transforms::TransformSpec;
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.transform_snapshots
.retain(|name, _| names.contains(&name.as_str()));
if names.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for &name in names {
let Some(handle) = scene.transform_handles.get(name) else {
continue;
};
let Some(element) = resolve_named(&content, name) else {
continue;
};
let Some(live) = element.render_transform() else {
continue;
};
if live.raw() != handle.raw() {
continue;
}
let current = TransformSpec::from_fields(handle.get());
if scene.transform_snapshots.get(name) == Some(¤t) {
continue;
}
scene.transform_snapshots.insert(name.to_string(), current);
changed.push((name.to_string(), current));
}
changed
}
pub(crate) fn poll_brush_reads_for(
&mut self,
entity: Entity,
desired: &HashMap<(String, crate::brushes::BrushTarget), crate::brushes::BrushSpec>,
) -> Vec<(
String,
crate::brushes::BrushTarget,
crate::brushes::BrushReadback,
)> {
use crate::brushes::BrushReadback;
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene.brush_snapshots.retain(|(name, property), _| {
desired
.keys()
.any(|(n, t)| n == name && t.property() == property)
});
if desired.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for (name, target) in desired.keys() {
let Some(element) = resolve_named(&content, name) else {
continue;
};
let property = target.property();
let current = if let Some(color) = element.solid_brush_color(property) {
BrushReadback::Solid(color)
} else if element.get_component(property).is_some() {
BrushReadback::NonSolid
} else {
continue;
};
let key = (name.clone(), property.to_string());
if scene.brush_snapshots.get(&key) == Some(¤t) {
continue;
}
scene.brush_snapshots.insert(key, current);
changed.push((name.clone(), *target, current));
}
changed
}
pub(crate) fn poll_image_reads_for(
&mut self,
entity: Entity,
desired: &HashMap<String, crate::imaging::ImageBitmap>,
) -> Vec<(String, crate::imaging::ImageReadback)> {
use crate::imaging::ImageReadback;
let mut changed = Vec::new();
let Some(scene) = self.scenes.get_mut(&entity) else {
return changed;
};
scene
.image_snapshots
.retain(|name, _| desired.contains_key(name));
if desired.is_empty() {
return changed;
}
let Some(content) = scene.view.content() else {
return changed;
};
for name in desired.keys() {
let Some(element) = resolve_named(&content, name) else {
continue;
};
let current = ImageReadback {
has_source: element.image_source().is_some(),
actual_size: [
element.actual_width().unwrap_or(0.0),
element.actual_height().unwrap_or(0.0),
],
};
if scene.image_snapshots.get(name) == Some(¤t) {
continue;
}
scene.image_snapshots.insert(name.clone(), current);
changed.push((name.clone(), current));
}
changed
}
fn apply_live_flags(&mut self, entity: Entity, config: &NoesisView) {
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let desired = flags_from(config);
if desired != scene.applied_flags {
scene.view.set_flags(desired);
scene.applied_flags = desired;
}
#[allow(clippy::float_cmp)]
if config.scale != scene.applied_scale {
scene.view.set_scale(config.scale);
scene.applied_scale = config.scale;
}
}
fn apply_input(&mut self, events: &[crate::input::TargetedInput]) {
use crate::input::NoesisInputEvent as E;
let Some(primary) = self.scenes.keys().min().copied() else {
self.pointer_over_ui = false;
return;
};
if self.scenes.len() > 1 {
bevy::log::warn_once!(
"Noesis input routes to the primary view (lowest Entity); per-view \
pointer routing across {} views is not implemented yet",
self.scenes.len()
);
}
let mut over_ui = self.pointer_over_ui;
for targeted in events {
let Some(scene) = self.scenes.get_mut(&targeted.target.unwrap_or(primary)) else {
continue;
};
match targeted.event {
E::MouseMove { x, y } => {
over_ui = scene.view.mouse_move(x, y);
}
E::MouseButton {
down: true,
x,
y,
button,
} => {
over_ui = scene.view.mouse_button_down(x, y, button);
}
E::MouseButton {
down: false,
x,
y,
button,
} => {
over_ui = scene.view.mouse_button_up(x, y, button);
}
E::MouseWheel { x, y, delta } => {
over_ui |= scene.view.mouse_wheel(x, y, delta);
}
E::MouseHWheel { x, y, delta } => {
over_ui |= scene.view.mouse_hwheel(x, y, delta);
}
E::Scroll {
x,
y,
value,
horizontal: false,
} => {
over_ui |= scene.view.scroll(x, y, value);
}
E::Scroll {
x,
y,
value,
horizontal: true,
} => {
over_ui |= scene.view.hscroll(x, y, value);
}
E::TouchDown { x, y, id } => {
over_ui = scene.view.touch_down(x, y, id);
}
E::TouchMove { x, y, id } => {
over_ui = scene.view.touch_move(x, y, id);
}
E::TouchUp { x, y, id } => {
over_ui = scene.view.touch_up(x, y, id);
}
E::KeyDown(k) => {
let _ = scene.view.key_down(k);
}
E::KeyUp(k) => {
let _ = scene.view.key_up(k);
}
E::Char(cp) => {
let _ = scene.view.char_input(cp);
}
E::Focus(true) => scene.view.activate(),
E::Focus(false) => scene.view.deactivate(),
}
}
self.pointer_over_ui = over_ui;
}
fn drive_frame(&mut self) {
let time_secs = self.clock_origin.elapsed().as_secs_f64();
let Self {
scenes,
registered_device,
..
} = self;
if scenes.is_empty() {
return;
}
let registered_device = registered_device
.as_mut()
.expect("registered_device dropped mid-frame");
for scene in scenes.values_mut() {
if !scene.renderer_initialized {
let mut renderer = scene.view.renderer();
renderer.init(registered_device);
scene.renderer_initialized = true;
}
registered_device
.device_mut::<WgpuRenderDevice>()
.set_onscreen_target(
scene.intermediates[scene.write_index].view.clone(),
scene.size.x,
scene.size.y,
);
let _changed = scene.view.update(time_secs);
let mut renderer = scene.view.renderer();
let _ = renderer.update_render_tree();
let _ = renderer.render_offscreen();
renderer.render(false, true);
}
}
fn publish_intermediates(&mut self, commands: &mut Commands) {
for (&entity, scene) in &mut self.scenes {
if !scene.renderer_initialized {
continue;
}
let published = &scene.intermediates[scene.write_index];
commands.entity(entity).insert(NoesisIntermediate {
view: published.view.clone(),
sample_view: published.sample_view.clone(),
});
scene.write_index ^= 1;
self.published_intermediates.insert(entity);
}
let scenes = &self.scenes;
self.published_intermediates.retain(|&entity| {
if scenes.contains_key(&entity) {
return true;
}
commands.entity(entity).remove::<NoesisIntermediate>();
false
});
}
pub(crate) fn bake_into(
&mut self,
target: &wgpu::TextureView,
xaml_uri: &str,
size: UVec2,
fields: &[(String, String)],
) -> bool {
if !self.fallbacks_installed {
return false;
}
let needs_build = self
.bake_rig
.as_ref()
.is_none_or(|rig| rig.built_for_uri != xaml_uri);
if needs_build {
{
let guard = self.shared_map.0.lock().expect("SharedXamlMap poisoned");
if !guard.contains_key(xaml_uri) {
return false;
}
}
self.teardown_bake_rig();
let Some(element) = FrameworkElement::load(xaml_uri) else {
warn!("bake_into: FrameworkElement::load({xaml_uri:?}) returned None");
return false;
};
let mut view = View::create(element);
view.set_size(size.x, size.y);
view.set_scale(1.0);
view.activate();
self.bake_rig = Some(BakeRig {
view,
renderer_initialized: false,
built_for_uri: xaml_uri.to_string(),
size,
});
}
let time_secs = self.clock_origin.elapsed().as_secs_f64();
let registered_device = self
.registered_device
.as_mut()
.expect("registered_device dropped mid-bake");
let rig = self.bake_rig.as_mut().expect("rig built above");
if let Some(content) = rig.view.content() {
for (name, text) in fields {
if let Some(mut element) = resolve_named(&content, name) {
let _ = element.set_text(text);
} else {
warn!("bake_into: x:Name {name:?} not found in {xaml_uri:?}");
}
}
}
if rig.size != size {
rig.view.set_size(size.x, size.y);
rig.size = size;
}
if !rig.renderer_initialized {
let mut renderer = rig.view.renderer();
renderer.init(registered_device);
rig.renderer_initialized = true;
}
registered_device
.device_mut::<WgpuRenderDevice>()
.set_onscreen_target(target.clone(), size.x, size.y);
let _ = rig.view.update(time_secs);
let mut renderer = rig.view.renderer();
let _ = renderer.update_render_tree();
let _ = renderer.render_offscreen();
renderer.render(false, true);
true
}
fn teardown_bake_rig(&mut self) {
let Some(mut rig) = self.bake_rig.take() else {
return;
};
if rig.renderer_initialized {
rig.view.renderer().shutdown();
}
drop(rig);
}
fn teardown_scene(&mut self, entity: Entity) {
if let Some(entry) = self.view_models.get_mut(&entity) {
entry.reset_attach();
}
for ((ent, _), binding) in &mut self.items_sources {
if *ent == entity {
binding.reset_bind();
}
}
for ((ent, _), binding) in &mut self.lists {
if *ent == entity {
binding.reset_bind();
}
}
for ((ent, _), entry) in &mut self.plain_vms {
if *ent == entity {
entry.reset_attach();
}
}
if let Some(entry) = self.command_hosts.get_mut(&entity) {
entry.reset_attach();
}
for ((ent, _, _), entry) in &mut self.binding_entries {
if *ent == entity {
entry.reset_bind();
}
}
for entry in self.panels.values_mut() {
if entry.host == entity {
entry.mounted_for_uri = None;
}
}
let Some(mut scene) = self.scenes.remove(&entity) else {
return;
};
if scene.renderer_initialized {
scene.view.renderer().shutdown();
}
drop(scene);
}
fn teardown_all_scenes(&mut self) {
let entities: Vec<Entity> = self.scenes.keys().copied().collect();
for entity in entities {
self.teardown_scene(entity);
}
}
#[must_use]
pub(crate) fn live_scene_count(&self) -> usize {
self.scenes.len()
}
#[must_use]
pub(crate) fn live_panel_count(&self) -> usize {
self.panels.len()
}
pub(crate) fn teardown_for(&mut self, entity: Entity) {
self.teardown_scene(entity);
self.view_models.remove(&entity);
self.items_sources.retain(|(ent, _), _| *ent != entity);
self.lists.retain(|(ent, _), _| *ent != entity);
self.list_owners.retain(|_, (v, _)| *v != entity);
self.plain_vms.retain(|(ent, _), _| *ent != entity);
self.command_hosts.remove(&entity);
self.warned_host_build_failures
.retain(|(ent, _)| *ent != entity);
self.warned_dc_collisions.retain(|(ent, _)| *ent != entity);
self.binding_entries.retain(|(ent, _, _), _| *ent != entity);
self.last_keydown_swallow
.retain(|(ent, _), _| *ent != entity);
self.last_click_target.retain(|(ent, _), _| *ent != entity);
self.last_event_config
.retain(|(ent, _, _), _| *ent != entity);
self.scenes_built_this_frame.remove(&entity);
self.published_intermediates.remove(&entity);
}
fn clear_host_data_context(&self, entity: Entity, target: &AttachTarget) {
let Some(scene) = self.scenes.get(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
let element = match target {
AttachTarget::Root => Some(content),
AttachTarget::Named(name) => resolve_named(&content, name),
};
if let Some(mut element) = element {
element.clear_data_context();
}
}
pub(crate) fn reap_view_model_for(&mut self, entity: Entity) {
if let Some(entry) = self.view_models.get(&entity)
&& self
.scenes
.get(&entity)
.is_some_and(|s| !entry.needs_attach(&s.built_for_uri))
{
self.clear_host_data_context(entity, entry.target());
}
self.view_models.remove(&entity);
self.warned_host_build_failures
.remove(&(entity, "NoesisVm"));
self.warned_dc_collisions.retain(|(ent, _)| *ent != entity);
}
pub(crate) fn reap_commands_for(&mut self, entity: Entity) {
if let Some(entry) = self.command_hosts.get(&entity)
&& self
.scenes
.get(&entity)
.is_some_and(|s| !entry.needs_attach(&s.built_for_uri))
{
self.clear_host_data_context(entity, entry.target());
}
self.command_hosts.remove(&entity);
self.warned_host_build_failures
.remove(&(entity, "NoesisCommands"));
self.warned_dc_collisions.retain(|(ent, _)| *ent != entity);
}
pub(crate) fn reap_plain_vm_for(&mut self, entity: Entity, type_id: std::any::TypeId) {
let key = (entity, type_id);
if let Some(entry) = self.plain_vms.get(&key)
&& self
.scenes
.get(&entity)
.is_some_and(|s| !entry.needs_attach(&s.built_for_uri))
{
self.clear_host_data_context(entity, entry.target());
}
self.plain_vms.remove(&key);
self.warned_dc_collisions.retain(|(ent, _)| *ent != entity);
}
pub(crate) fn reap_click_watch_for(&mut self, entity: Entity) {
if let Some(scene) = self.scenes.get_mut(&entity) {
scene.click_subs.clear();
}
if let Some(panel) = self.panels.get_mut(&entity) {
panel.click_subs.clear();
}
self.last_click_target.retain(|(ent, _), _| *ent != entity);
}
pub(crate) fn reap_keydown_watch_for(&mut self, entity: Entity) {
if let Some(scene) = self.scenes.get_mut(&entity) {
scene.keydown_subs.clear();
}
if let Some(panel) = self.panels.get_mut(&entity) {
panel.keydown_subs.clear();
}
self.last_keydown_swallow
.retain(|(ent, _), _| *ent != entity);
}
pub(crate) fn reap_event_watch_for(&mut self, entity: Entity) {
if let Some(scene) = self.scenes.get_mut(&entity) {
scene.event_subs.clear();
}
if let Some(panel) = self.panels.get_mut(&entity) {
panel.event_subs.clear();
}
self.last_event_config
.retain(|(ent, _, _), _| *ent != entity);
}
pub(crate) fn reap_items_for(&mut self, entity: Entity) {
if let Some(scene) = self.scenes.get(&entity)
&& let Some(content) = scene.view.content()
{
let uri = &scene.built_for_uri;
for ((ent, name), binding) in &self.items_sources {
if *ent != entity || binding.needs_bind(uri) {
continue;
}
if let Some(mut element) = resolve_named(&content, name) {
element.clear_items_source();
}
}
}
self.items_sources.retain(|(ent, _), _| *ent != entity);
}
pub(crate) fn reap_list_for(&mut self, list_ent: Entity) {
let Some((view, name)) = self.list_owners.remove(&list_ent) else {
return;
};
let key = (view, name.clone());
if let Some(binding) = self.lists.get_mut(&key) {
binding.detach();
}
self.lists.remove(&key);
if let Some(scene) = self.scenes.get_mut(&view) {
scene.row_click_subs.remove(&name);
}
}
pub(crate) fn reap_imaging_snapshots_for(&mut self, entity: Entity) {
if let Some(scene) = self.scenes.get_mut(&entity) {
scene.image_snapshots.clear();
}
}
}
impl Drop for NoesisRenderState {
fn drop(&mut self) {
self.teardown_all_scenes();
self.view_models.clear();
self.items_sources.clear();
self.lists.clear();
self.plain_vms.clear();
self.panels.clear();
self.command_hosts.clear();
self.binding_entries.clear();
self.teardown_bake_rig();
drop(self.registered_device.take());
drop(self.registered_provider.take());
drop(self.registered_fonts.take());
drop(self.registered_textures.take());
self.integration_guards.clear();
noesis_runtime::shutdown();
}
}
pub(crate) struct BlitPipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
}
const PREMULTIPLIED_OVER: wgpu::BlendState = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
};
impl BlitPipeline {
fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("noesis blit shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(BLIT_WGSL)),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis blit bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("noesis blit sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("noesis blit pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("noesis blit pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: Some(PREMULTIPLIED_OVER),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
Self {
pipeline,
bind_group_layout,
sampler,
}
}
fn bind_group(&self, device: &wgpu::Device, src: &wgpu::TextureView) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis blit bg"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(src),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
})
}
}
const BLIT_WGSL: &str = r"
struct VertexOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOut {
// Fullscreen triangle covering NDC [-1, 3] x [-1, 3] clipped to viewport.
let x = f32((idx << 1u) & 2u);
let y = f32(idx & 2u);
var out: VertexOut;
out.pos = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
out.uv = vec2<f32>(x, y);
return out;
}
@group(0) @binding(0) var src_texture: texture_2d<f32>;
@group(0) @binding(1) var src_sampler: sampler;
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
return textureSample(src_texture, src_sampler, in.uv);
}
";
#[doc(hidden)]
pub fn blit_composite_for_test(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
src: &wgpu::TextureView,
target: &wgpu::TextureView,
target_format: wgpu::TextureFormat,
) {
let blit = BlitPipeline::new(device, target_format);
let bg = blit.bind_group(device, src);
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("blit_composite_for_test"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&blit.pipeline);
pass.set_bind_group(0, &bg, &[]);
pass.draw(0..3, 0..1);
}
#[derive(Resource, Default)]
pub(crate) struct BlitPipelineCache {
over: HashMap<wgpu::TextureFormat, BlitPipeline>,
}
impl BlitPipelineCache {
fn get(&self, format: wgpu::TextureFormat) -> Option<&BlitPipeline> {
self.over.get(&format)
}
fn ensure(&mut self, device: &wgpu::Device, format: wgpu::TextureFormat) {
self.over
.entry(format)
.or_insert_with(|| BlitPipeline::new(device, format));
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_xaml_provider_map(
registry: Option<Res<XamlRegistry>>,
state: Option<NonSend<NoesisRenderState>>,
) {
let (Some(registry), Some(state)) = (registry, state) else {
return;
};
state.shared_map().sync_from(®istry);
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_font_provider_map(
registry: Option<Res<FontRegistry>>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let (Some(registry), Some(mut state)) = (registry, state) else {
return;
};
state.shared_fonts().sync_from(®istry);
state.register_pending_fonts();
}
#[allow(clippy::needless_pass_by_value)]
fn sync_texture_provider_map(
registry: Option<Res<ImageRegistry>>,
state: Option<NonSend<NoesisRenderState>>,
) {
let (Some(registry), Some(state)) = (registry, state) else {
return;
};
state.shared_images().sync_from(®istry);
}
#[allow(clippy::needless_pass_by_value)]
fn ensure_noesis_scene(
views: Query<(Entity, &NoesisView)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
state.scenes_built_this_frame.clear();
state.panels_mounted_this_frame.clear();
for (entity, config) in &views {
state.ensure_scene(entity, config);
}
}
pub(crate) trait ReapOnRemove: Component {
fn reap(state: &mut NoesisRenderState, entity: Entity);
}
#[allow(clippy::needless_pass_by_value)]
fn reap_removed_bridge<C: ReapOnRemove>(
mut removed: RemovedComponents<C>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for entity in removed.read() {
C::reap(&mut state, entity);
}
}
pub(crate) fn add_reap_system<M>(
app: &mut App,
system: impl IntoScheduleConfigs<ScheduleSystem, M>,
) {
app.add_systems(
PostUpdate,
system.in_set(NoesisSet::Ensure).before(ensure_noesis_scene),
);
}
pub(crate) fn add_bridge_reap<C: ReapOnRemove>(app: &mut App) {
add_reap_system(app, reap_removed_bridge::<C>);
}
#[allow(clippy::needless_pass_by_value)]
fn teardown_removed_views(
mut removed: RemovedComponents<NoesisView>,
alive: Query<Entity>,
mut commands: Commands,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for entity in removed.read() {
state.teardown_for(entity);
if alive.contains(entity) {
commands.entity(entity).remove::<NoesisIntermediate>();
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn teardown_removed_panels(
mut removed: RemovedComponents<crate::panel::UiPanel>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for entity in removed.read() {
state.teardown_panel_for(entity);
}
}
fn apply_timer_start(mut timer: ResMut<NoesisApplyTimer>) {
timer.started = Some(std::time::Instant::now());
}
fn apply_timer_end(mut timer: ResMut<NoesisApplyTimer>) {
if let Some(start) = timer.started.take() {
timer.last = start.elapsed();
}
}
#[allow(clippy::needless_pass_by_value)]
fn apply_live_scene_flags(
views: Query<(Entity, &NoesisView)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, config) in &views {
state.apply_live_flags(entity, config);
}
}
#[allow(clippy::needless_pass_by_value)]
fn apply_noesis_input(
queue: Option<ResMut<crate::input::NoesisInputQueue>>,
state: Option<NonSendMut<NoesisRenderState>>,
over_ui: Option<ResMut<crate::input::NoesisPointerOverUi>>,
) {
let (Some(mut queue), Some(mut state)) = (queue, state) else {
return;
};
if !queue.events.is_empty() {
let events: Vec<_> = queue.drain().collect();
state.apply_input(&events);
} else if state.scenes.is_empty() {
state.pointer_over_ui = false;
} else {
return;
}
if let Some(mut over_ui) = over_ui
&& over_ui.over != state.pointer_over_ui
{
over_ui.over = state.pointer_over_ui;
}
}
#[allow(clippy::needless_pass_by_value)]
fn drive_noesis_frame(mut commands: Commands, state: Option<NonSendMut<NoesisRenderState>>) {
let Some(mut state) = state else {
return;
};
state.drive_frame();
state.publish_intermediates(&mut commands);
}
#[allow(clippy::needless_pass_by_value)]
fn prepare_noesis_blit(
render_device: Res<RenderDevice>,
targets: Query<&ViewTarget>,
mut cache: ResMut<BlitPipelineCache>,
) {
for target in &targets {
cache.ensure(render_device.wgpu_device(), target.main_texture_format());
}
}
#[derive(Component, ExtractComponent, Clone, Copy, Default, Debug)]
#[extract_component_sync_target(Self)]
pub struct NoesisCamera;
#[derive(Component, ExtractComponent, Clone)]
#[extract_component_sync_target(Self)]
pub struct NoesisIntermediate {
view: wgpu::TextureView,
sample_view: wgpu::TextureView,
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub enum NoesisSet {
Sync,
Ensure,
Apply,
Drive,
}
fn blit_noesis_ui(
ctx: &mut RenderContext,
intermediate: &NoesisIntermediate,
view_target: &ViewTarget,
cache: &BlitPipelineCache,
) {
let target_format = view_target.main_texture_format();
let Some(blit) = cache.get(target_format) else {
return;
};
let decode_srgb = target_format.is_srgb() || is_linear_float(target_format);
let sample_view = if decode_srgb {
&intermediate.sample_view
} else {
&intermediate.view
};
let bg = blit.bind_group(ctx.render_device().wgpu_device(), sample_view);
let encoder = ctx.command_encoder();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("noesis blit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: view_target.main_texture_view(),
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&blit.pipeline);
pass.set_bind_group(0, &bg, &[]);
pass.draw(0..3, 0..1);
drop(pass);
}
fn noesis_blit_2d(
view: ViewQuery<(&ViewTarget, &NoesisIntermediate)>,
cache: Res<BlitPipelineCache>,
mut ctx: RenderContext,
) {
let (view_target, intermediate) = view.into_inner();
blit_noesis_ui(&mut ctx, intermediate, view_target, &cache);
}
fn noesis_blit_3d(
view: ViewQuery<(&ViewTarget, &NoesisIntermediate), With<NoesisCamera>>,
cache: Res<BlitPipelineCache>,
mut ctx: RenderContext,
) {
let (view_target, intermediate) = view.into_inner();
blit_noesis_ui(&mut ctx, intermediate, view_target, &cache);
}
pub struct NoesisRenderPlugin;
pub(crate) fn build_main_world_pipeline(app: &mut App) {
app.init_resource::<NoesisApplyTimer>();
app.configure_sets(
PostUpdate,
(
NoesisSet::Sync,
NoesisSet::Ensure,
NoesisSet::Apply,
NoesisSet::Drive,
)
.chain(),
)
.add_systems(
PostUpdate,
(
(
sync_xaml_provider_map,
sync_font_provider_map,
sync_texture_provider_map,
)
.in_set(NoesisSet::Sync),
teardown_removed_views
.in_set(NoesisSet::Ensure)
.before(ensure_noesis_scene),
teardown_removed_panels
.in_set(NoesisSet::Ensure)
.before(ensure_noesis_scene),
ensure_noesis_scene.in_set(NoesisSet::Ensure),
apply_timer_start
.in_set(NoesisSet::Ensure)
.after(ensure_noesis_scene),
(apply_live_scene_flags, apply_noesis_input).in_set(NoesisSet::Apply),
apply_timer_end
.in_set(NoesisSet::Drive)
.before(drive_noesis_frame),
drive_noesis_frame.in_set(NoesisSet::Drive),
),
);
}
impl Plugin for NoesisRenderPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<NoesisIntermediate>::default(),
ExtractComponentPlugin::<NoesisCamera>::default(),
));
build_main_world_pipeline(app);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
warn!("RenderApp not present; NoesisRenderPlugin compositing is a no-op");
return;
};
render_app
.init_resource::<BlitPipelineCache>()
.add_systems(Render, prepare_noesis_blit.in_set(RenderSystems::Prepare))
.add_systems(
Core2d,
noesis_blit_2d
.after(Core2dSystems::MainPass)
.before(Core2dSystems::PostProcess),
)
.add_systems(
Core3d,
noesis_blit_3d
.after(Core3dSystems::PostProcess)
.before(upscaling),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app(RenderApp) else {
return;
};
let device = render_app
.world()
.resource::<RenderDevice>()
.wgpu_device()
.clone();
let queue = (**render_app.world().resource::<RenderQueue>().0).clone();
app.insert_non_send(NoesisRenderState::new(device, queue));
}
}
#[cfg(test)]
mod tests {
use super::{is_linear_float, resolve_scope_path};
use wgpu::TextureFormat;
struct Scope {
name: &'static str,
children: Vec<Scope>,
}
impl Scope {
fn child(&self, name: &str) -> Option<&Scope> {
self.children.iter().find(|c| c.name == name)
}
}
fn resolve<'a>(root: &'a Scope, path: &str) -> Option<&'a str> {
resolve_scope_path(&root, path, |node, seg| node.child(seg)).map(|n| n.name)
}
#[test]
fn scope_path_walks_into_nested_namescopes() {
let tree = Scope {
name: "Root",
children: vec![Scope {
name: "MainMenu",
children: vec![Scope {
name: "Footer",
children: vec![Scope {
name: "PlayButton",
children: vec![],
}],
}],
}],
};
assert_eq!(resolve(&tree, "MainMenu"), Some("MainMenu"));
assert_eq!(resolve(&tree, "MainMenu/Footer"), Some("Footer"));
assert_eq!(
resolve(&tree, "MainMenu/Footer/PlayButton"),
Some("PlayButton")
);
assert_eq!(resolve(&tree, "PlayButton"), None);
assert_eq!(resolve(&tree, "MainMenu/Nope"), None);
assert_eq!(resolve(&tree, "Nope/Footer"), None);
}
#[test]
fn hdr_float_targets_decode_srgb() {
assert!(is_linear_float(TextureFormat::Rgba16Float));
assert!(is_linear_float(TextureFormat::Rgba32Float));
assert!(is_linear_float(TextureFormat::Rg11b10Ufloat));
}
#[test]
fn noesis_view_requires_the_bridge_components() {
use bevy::prelude::*;
let mut world = World::new();
let view = world
.spawn(super::NoesisView {
xaml_uri: "x.xaml".to_string(),
..default()
})
.id();
let e = world.entity(view);
assert!(e.contains::<crate::text::NoesisText>());
assert!(e.contains::<crate::visibility::NoesisVisibility>());
assert!(e.contains::<crate::dp::NoesisDp>());
assert!(e.contains::<crate::items::NoesisItems>());
assert!(e.contains::<crate::focus::NoesisFocus>());
assert!(e.contains::<crate::svg::NoesisSvg>());
assert!(e.contains::<crate::events::NoesisClickWatch>());
assert!(!e.contains::<crate::viewmodel::NoesisVm>());
assert!(!e.contains::<crate::commands::NoesisCommands>());
}
#[test]
fn ldr_unorm_targets_sample_raw() {
assert!(!is_linear_float(TextureFormat::Rgba8Unorm));
assert!(!is_linear_float(TextureFormat::Bgra8Unorm));
assert!(!is_linear_float(TextureFormat::Rgba8UnormSrgb));
}
}