use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use bevy::core_pipeline::core_2d::graph::{Core2d, Node2d};
use bevy::core_pipeline::core_3d::graph::{Core3d, Node3d};
use bevy::prelude::*;
use bevy_render::{
Render, RenderApp, RenderSystems,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_graph::{
NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
},
renderer::{RenderContext, RenderDevice, RenderQueue},
view::ViewTarget,
};
use noesis_runtime::animation::{Animation, DoubleAnimation, Timeline};
use noesis_runtime::commands::Command;
use noesis_runtime::events::{
ClickSubscription, EventArgs, EventSubscription, KeyDownSubscription, 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::PlainVmEntry;
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};
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, ExtractComponent, 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(),
}
}
}
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>,
scenes_built_this_frame: HashSet<Entity>,
fallbacks_installed: bool,
registered_faces: HashSet<(String, String)>,
loaded_app_resources_chain: Option<Vec<String>>,
clock_origin: std::time::Instant,
last_keydown_swallow: HashMap<(Entity, String), Vec<Key>>,
last_event_config: HashMap<(Entity, String, &'static str), (bool, bool)>,
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>,
binding_entries: HashMap<(Entity, String, String), BindingEntry>,
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>,
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,
}
struct BakeRig {
view: View,
renderer_initialized: bool,
built_for_uri: String,
size: UVec2,
}
impl NoesisRenderState {
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(),
scenes_built_this_frame: HashSet::new(),
fallbacks_installed: false,
registered_faces: HashSet::new(),
loaded_app_resources_chain: None,
clock_origin: std::time::Instant::now(),
last_keydown_swallow: 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(),
binding_entries: 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 self.view_models.contains_key(&entity) {
return;
}
match VmEntry::build(entity, def, changed) {
Some(entry) => {
self.view_models.insert(entity, entry);
}
None => 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) => content.find_name(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 self.command_hosts.contains_key(&entity) {
return;
}
match CommandEntry::build(entity, def, queue) {
Some(entry) => {
self.command_hosts.insert(entity, entry);
}
None => 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) => content.find_name(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:?}");
}
}
}
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 {
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) = content.find_name(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) {
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) = content.find_name(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
}
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 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) = content.find_name(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, props, target.clone()) else {
warn!(
"NoesisViewModel: failed to register plain VM {type_name:?} (duplicate name?)",
);
return Vec::new();
};
slot.insert(entry);
}
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) => content.find_name(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()
}
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;
}
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;
}
self.teardown_scene(entity);
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.register_pending_fonts();
self.install_font_fallbacks_if_needed(&config.font_fallbacks);
self.install_application_resources_if_needed(config);
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(),
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 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) = content.find_name(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 {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, &[left, top, right, bottom]) in desired {
let Some(mut element) = content.find_name(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) = content.find_name(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) = content.find_name(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,
watch: &[String],
queue: &SharedClickQueue,
) {
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
scene.click_subs.retain(|k, _| watch.iter().any(|w| w == k));
let needs_new = watch.iter().any(|n| !scene.click_subs.contains_key(n));
if !needs_new {
return;
}
let Some(content) = scene.view.content() else {
return;
};
for name in watch {
if scene.click_subs.contains_key(name) {
continue;
}
let Some(element) = content.find_name(name) else {
warn!(
"NoesisClickWatch: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let queue_handle = queue.clone();
let captured_name = name.clone();
let Some(sub) = subscribe_click(&element, move || {
queue_handle.push(entity, captured_name.clone());
}) else {
warn!("NoesisClickWatch: element {name:?} is not a BaseButton; skipping");
continue;
};
scene.click_subs.insert(name.clone(), sub);
}
}
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 {
return;
};
scene
.keydown_subs
.retain(|k, _| entries.iter().any(|e| e.name == *k));
for entry in entries {
if scene.keydown_subs.contains_key(&entry.name)
&& self
.last_keydown_swallow
.get(&(entity, entry.name.clone()))
.is_some_and(|prev| prev == &entry.swallow)
{
continue;
}
let Some(content) = scene.view.content() else {
return;
};
let Some(element) = content.find_name(&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, 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());
}
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 {
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);
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))
{
continue;
}
let Some(content) = scene.view.content() else {
return;
};
let Some(element) = content.find_name(&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, 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),
);
}
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) = content.find_name(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) = content.find_name(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) = content.find_name(&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) = content.find_name(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) = content.find_name(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 {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for (name, points) in desired {
let Some(mut element) = content.find_name(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_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) = content.find_name(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) = content.find_name(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 {
return;
};
let Some(content) = scene.view.content() else {
return;
};
let Some(mut element) = content.find_name(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],
) {
if moves.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for m in moves {
let Some(mut element) = content.find_name(&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,
);
}
}
}
pub(crate) fn apply_focus_engages_for(
&mut self,
entity: Entity,
engages: &[crate::focus_input::FocusEngage],
) {
if engages.is_empty() {
return;
}
let Some(scene) = self.scenes.get_mut(&entity) else {
return;
};
let Some(content) = scene.view.content() else {
return;
};
for e in engages {
let Some(mut element) = content.find_name(&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,
);
}
}
}
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) = content.find_name(&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) = content.find_name(&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) = content.find_name(&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) = content.find_name(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) = content.find_name(name) else {
warn!(
"NoesisDp: x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
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) = content.find_name(&watch.name) else {
continue;
};
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 {
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) = content.find_name(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) = content.find_name(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) = content.find_name(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, [f32; 12]>,
) {
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, matrix) in desired {
let Some(mut element) = content.find_name(name) else {
warn!(
"NoesisTransform3D(matrix): x:Name {:?} not found in scene {:?}",
name, scene.built_for_uri,
);
continue;
};
let transform = MatrixTransform3D::new(*matrix);
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) = content.find_name(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) = content.find_name(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) = content.find_name(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 install_app_resources_from(
&mut self,
entries: &HashMap<String, crate::resources::ResourceEntry>,
merged_xaml: &[String],
) -> 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,
};
let mut dict = ResourceDictionary::new();
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:?}");
}
}
set_application_resources(&dict);
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, {} present",
entries.len(),
merged_xaml.len(),
present.len(),
);
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) = content.find_name(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) = content.find_name(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) = content.find_name(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 install_application_resources_if_needed(&mut self, config: &NoesisView) {
if config.application_resources.is_empty() {
return;
}
if self
.loaded_app_resources_chain
.as_ref()
.is_some_and(|loaded| loaded == &config.application_resources)
{
return;
}
{
let guard = self.shared_map.0.lock().expect("SharedXamlMap poisoned");
for uri in &config.application_resources {
if !guard.contains_key(uri) {
return; }
}
}
if noesis_runtime::gui::install_app_resources_chain(&config.application_resources) {
info!(
"Installed Noesis application resources chain ({} entries): {:?}",
config.application_resources.len(),
config.application_resources,
);
self.loaded_app_resources_chain = Some(config.application_resources.clone());
} else {
warn!(
"install_app_resources_chain returned false for {:?}",
config.application_resources,
);
}
}
fn apply_input(&mut self, events: &[crate::input::NoesisInputEvent]) {
let Some(scene) = self.scenes.values_mut().next() else {
return;
};
use crate::input::NoesisInputEvent as E;
for ev in events {
match *ev {
E::MouseMove { x, y } => {
let _ = scene.view.mouse_move(x, y);
}
E::MouseButton {
down: true,
x,
y,
button,
} => {
let _ = scene.view.mouse_button_down(x, y, button);
}
E::MouseButton {
down: false,
x,
y,
button,
} => {
let _ = scene.view.mouse_button_up(x, y, button);
}
E::MouseWheel { x, y, delta } => {
let _ = scene.view.mouse_wheel(x, y, delta);
}
E::Scroll {
x,
y,
value,
horizontal: false,
} => {
let _ = scene.view.scroll(x, y, value);
}
E::Scroll {
x,
y,
value,
horizontal: true,
} => {
let _ = scene.view.hscroll(x, y, value);
}
E::TouchDown { x, y, id } => {
let _ = scene.view.touch_down(x, y, id);
}
E::TouchMove { x, y, id } => {
let _ = scene.view.touch_move(x, y, id);
}
E::TouchUp { x, y, id } => {
let _ = 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(),
}
}
}
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;
}
}
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) = content.find_name(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, _), 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();
}
}
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);
}
}
}
impl Drop for NoesisRenderState {
fn drop(&mut self) {
self.teardown_all_scenes();
self.view_models.clear();
self.items_sources.clear();
self.plain_vms.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: &[&bind_group_layout],
push_constant_ranges: &[],
});
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: 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,
});
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)]
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)]
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();
for (entity, config) in &views {
state.ensure_scene(entity, config);
}
}
#[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<Res<crate::input::NoesisInputQueue>>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let (Some(queue), Some(mut state)) = (queue, state) else {
return;
};
if queue.events.is_empty() {
return;
}
state.apply_input(&queue.events);
}
#[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(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub struct NoesisNodeLabel;
#[derive(Component, ExtractComponent, Clone, Copy, Default, Debug)]
pub struct NoesisCamera;
#[derive(Component, ExtractComponent, Clone)]
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(
render_context: &mut RenderContext<'_>,
intermediate: &NoesisIntermediate,
view_target: &ViewTarget,
world: &World,
) -> Result<(), NodeRunError> {
let Some(cache) = world.get_resource::<BlitPipelineCache>() else {
return Ok(());
};
let target_format = view_target.main_texture_format();
let Some(blit) = cache.get(target_format) else {
return Ok(());
};
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(render_context.render_device().wgpu_device(), sample_view);
let encoder = render_context.command_encoder();
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("NoesisNode 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,
});
pass.set_pipeline(&blit.pipeline);
pass.set_bind_group(0, &bg, &[]);
pass.draw(0..3, 0..1);
drop(pass);
Ok(())
}
#[derive(Default)]
pub struct NoesisNode;
impl ViewNode for NoesisNode {
type ViewQuery = (&'static ViewTarget, &'static NoesisIntermediate);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(view_target, intermediate): (&'w ViewTarget, &'w NoesisIntermediate),
world: &'w World,
) -> Result<(), NodeRunError> {
blit_noesis_ui(render_context, intermediate, view_target, world)
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub struct NoesisOverlayNodeLabel;
#[derive(Default)]
pub struct NoesisOverlayNode;
impl ViewNode for NoesisOverlayNode {
type ViewQuery = (
&'static ViewTarget,
&'static NoesisCamera,
&'static NoesisIntermediate,
);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(view_target, _marker, intermediate): (
&'w ViewTarget,
&'w NoesisCamera,
&'w NoesisIntermediate,
),
world: &'w World,
) -> Result<(), NodeRunError> {
blit_noesis_ui(render_context, intermediate, view_target, world)
}
}
pub struct NoesisRenderPlugin;
impl Plugin for NoesisRenderPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<NoesisIntermediate>::default(),
ExtractComponentPlugin::<NoesisCamera>::default(),
));
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),
ensure_noesis_scene.in_set(NoesisSet::Ensure),
(apply_live_scene_flags, apply_noesis_input).in_set(NoesisSet::Apply),
drive_noesis_frame.in_set(NoesisSet::Drive),
),
);
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_render_graph_node::<ViewNodeRunner<NoesisNode>>(Core2d, NoesisNodeLabel)
.add_render_graph_edges(
Core2d,
(
Node2d::MainTransparentPass,
NoesisNodeLabel,
Node2d::EndMainPass,
),
)
.add_render_graph_node::<ViewNodeRunner<NoesisOverlayNode>>(
Core3d,
NoesisOverlayNodeLabel,
)
.add_render_graph_edges(
Core3d,
(
Node3d::EndMainPassPostProcessing,
NoesisOverlayNodeLabel,
Node3d::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_resource(NoesisRenderState::new(device, queue));
}
}
#[cfg(test)]
mod tests {
use super::is_linear_float;
use wgpu::TextureFormat;
#[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));
}
}