use core::time::Duration;
use unlit_ecs::{Entity, Scope, World};
use unlit_wgpu::globals::{Globals, View};
use unlit_wgpu::pipeline::{CAMERA_BINDING, FRAME_BINDING, SpecializedUnlitPipeline};
use unlit_wgpu::resources::ResourceGraph;
use unlit_wgpu::scene::Scene;
use unlit_wgpu::specialize::SpecializedPipeline;
use unlit_wgpu::specialize::SurfaceKey;
use unlit_wgpu::ui::{EguiIntegration, ScreenDescriptor, Source, UiFrame, screen_view, ui_variant};
use web_time::Instant;
use zerocopy::IntoBytes;
pub use crate::source::{OpenUrl, OpenUrls};
use crate::input::InputEvents;
use crate::relay::Snapshot;
use crate::pass::PassInfo;
use crate::source::{FrameSource, FrameSourceOrder, RenderContext};
pub use egui;
pub mod convert;
type PanelCallback = Box<dyn FnMut(Scope<'_>, Entity, &mut egui::Ui)>;
pub struct UiPanel(pub PanelCallback);
impl UiPanel {
pub fn new(f: impl FnMut(Scope<'_>, Entity, &mut egui::Ui) + 'static) -> Self {
Self(Box::new(f))
}
pub fn run(&mut self, scope: Scope<'_>, entity: Entity, ui: &mut egui::Ui) {
(self.0)(scope, entity, ui)
}
}
struct Gpu {
camera: wgpu::Buffer,
globals: wgpu::Buffer,
integration: EguiIntegration,
}
struct Target {
key: SurfaceKey,
gpu: Gpu,
}
pub struct UiSource {
ctx: egui::Context,
start: Instant,
targets: Vec<Target>,
event_scratch: Vec<egui::Event>,
pub frames: ExternalFrames,
screen: Snapshot<UiTarget>,
url_scratch: Vec<OpenUrl>,
}
impl UiSource {
pub fn new() -> Self {
Self {
ctx: egui::Context::default(),
start: Instant::now(),
targets: Vec::new(),
event_scratch: Vec::new(),
frames: ExternalFrames::new(),
screen: Snapshot::new(),
url_scratch: Vec::new(),
}
}
pub fn publish_target(&mut self, screen: Snapshot<UiTarget>) -> &mut Self {
self.screen = screen;
self
}
pub fn context(&self) -> &egui::Context {
&self.ctx
}
pub fn context_mut(&mut self) -> &mut egui::Context {
&mut self.ctx
}
fn run_panels(&self, scope: &Scope<'_>, input: egui::RawInput) -> egui::FullOutput {
let root = *scope;
self.ctx.run_ui(input, |ui| {
for (mut ui_panel, entity) in root.query::<(&mut UiPanel, Entity)>() {
ui_panel.run(root, entity, ui);
}
})
}
fn target_index(
&mut self,
device: &wgpu::Device,
graph: &mut ResourceGraph,
surface: SurfaceKey,
) -> usize {
if let Some(index) = self.targets.iter().position(|target| target.key == surface) {
return index;
}
let variant = ui_variant(device, surface.color_format.is_srgb(), surface);
let pipeline = SpecializedPipeline::create(device, variant);
let camera = uniform_buffer(
device,
"unlit3d::ui::camera",
<View as const_shader_layout::ShaderLayout>::SIZE.get(),
);
let globals = uniform_buffer(
device,
"unlit3d::ui::globals",
<Globals as const_shader_layout::ShaderLayout>::SIZE.get(),
);
let camera_id = graph.insert(camera.clone(), None);
let globals_id = graph.insert(globals.clone(), None);
let group = global_group(device, &pipeline, &camera, &globals);
let group_id = graph.insert(group, None);
graph.add_dependency(&group_id, &camera_id);
graph.add_dependency(&group_id, &globals_id);
self.targets.push(Target {
key: surface,
gpu: Gpu {
camera,
globals,
integration: EguiIntegration::new(device, group_id, pipeline),
},
});
self.targets.len() - 1
}
}
impl Default for UiSource {
fn default() -> Self {
Self::new()
}
}
impl FrameSource for UiSource {
fn build_scene(
&mut self,
scope: &Scope<'_>,
ctx: RenderContext,
info: PassInfo,
scene: &mut Scene,
encoder: &mut wgpu::CommandEncoder,
) {
let maintain = ctx.maintain_scope(*scope);
let target = info;
let device = scope
.query_one::<&wgpu::Device>(ctx.device)
.expect("the context's device resource exists")
.clone();
let queue = scope
.query_one::<&wgpu::Queue>(ctx.queue)
.expect("the context's queue resource exists")
.clone();
let state = scope.query::<&InputEvents>().next().map(|input_events| {
input_events.with_state(|state| (state.scale_factor, state.size_px, state.focused))
});
let (scale_factor, size_px, focused) = match state {
Some(state) => state,
None => (1.0, (target.width, target.height), true),
};
let size_px = if size_px == (0, 0) {
(target.width, target.height)
} else {
size_px
};
let pixels_per_point = if scale_factor > 0.0 {
scale_factor
} else {
1.0
};
let mut event_scratch = core::mem::take(&mut self.event_scratch);
event_scratch.clear();
if let Some(input_events) = scope.query::<&InputEvents>().next() {
input_events.with_batch(|batch| {
convert::to_egui_events_into(batch, pixels_per_point, &mut event_scratch);
});
}
let screen = ScreenDescriptor {
size_in_pixels: [size_px.0, size_px.1],
pixels_per_point,
};
let target_screen = UiTarget {
screen,
max_texture_side: device.limits().max_texture_dimension_2d,
};
self.screen.publish(target_screen);
let mut graph = ctx.graph(scope);
let index = self.target_index(&device, &mut graph, target.surface);
let input = raw_input(
target_screen,
Some(self.start.elapsed().as_secs_f64()),
focused,
event_scratch,
);
let mut url_scratch = core::mem::take(&mut self.url_scratch);
url_scratch.clear();
self.frames.with_frames(|slots| {
for slot in slots.iter().flatten() {
url_scratch.extend(open_urls(&slot.platform_output));
}
});
let mut output = self.run_panels(scope, input);
self.event_scratch = self
.ctx
.input_mut(|input| core::mem::take(&mut input.raw.events));
url_scratch.extend(open_urls(&output.platform_output));
publish_open_urls(scope, &mut url_scratch);
self.url_scratch = url_scratch;
let own = self.ctx.tessellate(output.shapes, screen.pixels_per_point);
let own_textures = core::mem::take(&mut output.textures_delta);
const NO_PRIMITIVES: [egui::ClippedPrimitive; 0] = [];
let gpu = &mut self.targets[index].gpu;
self.frames.with_frames(|slots| {
let mut sources = Vec::with_capacity(slots.len() + 1);
sources.push(UiFrame {
source: Source::LOCAL,
textures: own_textures,
primitives: &own,
});
for (index, slot) in slots.iter_mut().enumerate() {
let Some(frame) = slot else {
continue;
};
let primitives = if frame.screen == screen {
&frame.primitives[..]
} else {
&NO_PRIMITIVES[..]
};
sources.push(UiFrame {
source: Source::published(index),
textures: core::mem::take(&mut frame.textures),
primitives,
});
}
gpu.integration
.update_with(&mut graph, &queue, encoder, &mut sources, screen);
});
queue.write_buffer(
&gpu.camera,
0,
screen_view(screen.size_in_points()).as_bytes(),
);
queue.write_buffer(&gpu.globals, 0, Globals::default().as_bytes());
drop(graph);
drop(maintain);
let graph = ctx.graph(scope);
let mut ui_scene = gpu.integration.scene(&graph);
scene.draws.append(&mut ui_scene.draws);
}
fn order(&self) -> FrameSourceOrder {
FrameSourceOrder::UI
}
}
fn open_urls(output: &egui::PlatformOutput) -> impl Iterator<Item = OpenUrl> + '_ {
output.commands.iter().filter_map(|command| match command {
egui::OutputCommand::OpenUrl(open) => Some(OpenUrl {
url: open.url.clone(),
new_tab: open.new_tab,
}),
_ => None,
})
}
fn publish_open_urls(scope: &Scope<'_>, urls: &mut Vec<OpenUrl>) {
let Some(entity) = scope
.query::<(&OpenUrls, Entity)>()
.next()
.map(|(_open_urls, e)| e)
else {
urls.clear();
return;
};
let _ = scope
.query_one::<&mut OpenUrls>(entity)
.map(|mut slot| slot.replace(urls.drain(..)));
}
fn uniform_buffer(device: &wgpu::Device, label: &str, size: u64) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn global_group(
device: &wgpu::Device,
pipeline: &SpecializedUnlitPipeline,
camera: &wgpu::Buffer,
globals: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("unlit3d::ui::globals"),
layout: &pipeline.descriptor().bind_group_layouts(device).global,
entries: &[
wgpu::BindGroupEntry {
binding: CAMERA_BINDING,
resource: camera.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: FRAME_BINDING,
resource: globals.as_entire_binding(),
},
],
})
}
pub struct ExternalFrame {
pub textures: egui::TexturesDelta,
pub primitives: Vec<egui::ClippedPrimitive>,
pub platform_output: egui::PlatformOutput,
pub repaint_delay: Duration,
pub screen: ScreenDescriptor,
}
impl Drop for ExternalFrame {
fn drop(&mut self) {
self.textures.clear();
}
}
#[derive(Default)]
pub struct ExternalFrames(Vec<Option<ExternalFrame>>);
impl ExternalFrames {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn publish(&mut self, slot: usize, mut frame: ExternalFrame) {
if self.0.len() <= slot {
self.0.resize_with(slot + 1, || None);
}
if let Some(displaced) = self.0[slot].as_mut() {
let mut carried = core::mem::take(&mut displaced.textures);
carried.append(core::mem::take(&mut frame.textures));
frame.textures = carried;
}
self.0[slot] = Some(frame);
}
pub fn with_frames<R>(&mut self, f: impl FnOnce(&mut [Option<ExternalFrame>]) -> R) -> R {
f(&mut self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UiTarget {
pub screen: ScreenDescriptor,
pub max_texture_side: u32,
}
pub fn raw_input(
target: UiTarget,
time: Option<f64>,
focused: bool,
events: Vec<egui::Event>,
) -> egui::RawInput {
let [width, height] = target.screen.size_in_points();
let mut input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::Vec2::new(width, height),
)),
time,
focused,
events,
max_texture_side: Some(target.max_texture_side as usize),
..Default::default()
};
input
.viewports
.get_mut(&input.viewport_id)
.expect("the root viewport is always present")
.native_pixels_per_point = Some(target.screen.pixels_per_point);
input
}
pub fn lay_out(
world: &World,
ctx: &egui::Context,
input: egui::RawInput,
screen: ScreenDescriptor,
) -> ExternalFrame {
let scope = world.root_scope();
let mut output = ctx.run_ui(input, |ui| {
for (mut ui_panel, entity) in scope.query::<(&mut UiPanel, Entity)>() {
ui_panel.run(scope, entity, ui);
}
});
let repaint_delay = output
.viewport_output
.get(&egui::ViewportId::ROOT)
.map_or(Duration::MAX, |viewport| viewport.repaint_delay);
let primitives = ctx.tessellate(output.shapes, screen.pixels_per_point);
let textures = core::mem::take(&mut output.textures_delta);
ExternalFrame {
textures,
primitives,
platform_output: output.platform_output,
repaint_delay,
screen,
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::cell::{Cell, RefCell};
use std::rc::Rc;
use unlit_ecs::bundle;
use unlit_wgpu::capabilities::DeviceCapabilities;
use crate::input::{
InputEvent, InputFeed, InputWriter, Modifiers, MouseButton, MouseEvent, TextEvent,
refresh_input,
};
use crate::pass::PassInfo;
use crate::source::spawn_context;
struct Runs(u32);
fn test_ui_target() -> UiTarget {
UiTarget {
screen: ScreenDescriptor {
size_in_pixels: [128, 96],
pixels_per_point: 1.0,
},
max_texture_side: 2048,
}
}
fn test_target() -> PassInfo {
PassInfo {
surface: SurfaceKey {
color_format: wgpu::TextureFormat::Rgba8UnormSrgb,
depth_stencil_format: None,
sample_count: 1,
},
width: 128,
height: 96,
viewport: None,
}
}
fn test_world() -> (World, RenderContext) {
let mut world = World::new();
let (device, queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
let ctx = spawn_context(
&mut world,
device,
queue,
ResourceGraph::new(),
DeviceCapabilities::default(),
);
(world, ctx)
}
fn encoder(world: &World, ctx: RenderContext) -> wgpu::CommandEncoder {
world
.root_scope()
.query_one::<&wgpu::Device>(ctx.device)
.expect("the context's device")
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("test::encoder"),
})
}
#[test]
fn each_panel_runs_once_and_may_write_a_sibling() {
let mut world = World::new();
let runs = Rc::new(Cell::new(0u32));
for _ in 0..2 {
let runs = Rc::clone(&runs);
world.root_scope_mut().spawn(bundle![
Runs(0),
UiPanel::new(move |world, entity, ui| {
runs.set(runs.get() + 1);
let seen = world
.query_one::<&Runs>(entity)
.expect("the sibling exists")
.0;
let _ = world
.query_one::<&mut Runs>(entity)
.map(|mut runs| runs.0 = seen + 1);
ui.label("panel");
}),
]);
}
let source = UiSource::new();
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::Vec2::new(128.0, 96.0),
)),
..Default::default()
};
let mut output = source.run_panels(&world.root_scope(), input);
output.textures_delta.clear();
let panels: Vec<Entity> = world
.root_scope()
.query::<(&UiPanel, Entity)>()
.map(|(_ui_panel, entity)| entity)
.collect();
assert_eq!(panels.len(), 2, "both panels are driven by one query");
for entity in panels {
assert_eq!(
world
.root_scope()
.query_one::<&Runs>(entity)
.expect("the sibling")
.0,
1,
"the panel ran once and wrote its sibling"
);
}
assert_eq!(runs.get(), 2, "one closure call per panel");
}
#[test]
fn a_panel_is_driven_by_the_source() {
let (mut world, ctx) = test_world();
let target = test_target();
world
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, ui| {
ui.painter().rect_filled(
egui::Rect::from_min_size(
egui::Pos2::new(8.0, 8.0),
egui::Vec2::new(64.0, 32.0),
),
0.0,
egui::Color32::from_rgb(0, 255, 0),
);
}),]);
let mut source = UiSource::new();
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
source.build_scene(&world.root_scope(), ctx, target, &mut scene, &mut encoder);
assert!(
!scene.draws.is_empty(),
"the panel's rectangle became a draw"
);
}
#[test]
fn a_fresh_source_declares_the_overlay_order() {
let source = UiSource::new();
assert_eq!(source.order(), FrameSourceOrder::UI);
}
#[test]
fn a_panel_may_queue_a_spawn() {
struct Spawned;
let world = World::new();
let source = UiSource::new();
let world = Rc::new(RefCell::new(world));
{
let world = Rc::clone(&world);
world
.borrow_mut()
.root_scope_mut()
.spawn(bundle![UiPanel::new(move |world, _entity, ui| {
if world.query::<&Spawned>().next().is_none() {
world.queue().spawn(bundle![Spawned]);
}
ui.label("once");
}),]);
}
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::Vec2::new(128.0, 96.0),
)),
..Default::default()
};
let mut output = {
let world = world.borrow();
source.run_panels(&world.root_scope(), input.clone())
};
output.textures_delta.clear();
assert_eq!(
world.borrow().root_scope().query::<&Spawned>().count(),
0,
"a queued spawn has not happened yet"
);
world.borrow_mut().apply().unwrap();
assert_eq!(
world.borrow().root_scope().query::<&Spawned>().count(),
1,
"the queue landed on apply"
);
let mut output = source.run_panels(&world.borrow().root_scope(), input);
output.textures_delta.clear();
world.borrow_mut().apply().unwrap();
assert_eq!(
world.borrow().root_scope().query::<&Spawned>().count(),
1,
"the panel re-ran and found what it asked for"
);
}
#[test]
fn every_pass_drives_every_panel() {
let mut world = World::new();
let runs = Rc::new(Cell::new(0u32));
for _ in 0..2 {
let runs = Rc::clone(&runs);
world
.root_scope_mut()
.spawn(bundle![UiPanel::new(move |_world, _entity, _ui| {
runs.set(runs.get() + 1);
}),]);
}
let source = UiSource::new();
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::Vec2::new(128.0, 96.0),
)),
..Default::default()
};
let mut seen = Vec::new();
for _ in 0..2 {
let mut output = source.run_panels(&world.root_scope(), input.clone());
output.textures_delta.clear();
seen.push(runs.get());
}
assert_eq!(
seen,
vec![2, 4],
"each pass drove both panels again, so the query was rebuilt"
);
}
#[test]
fn a_filter_selects_which_panels_a_caller_collects() {
struct Foreground;
let mut world = World::new();
world
.root_scope_mut()
.spawn(bundle![Foreground, UiPanel::new(|_world, _entity, _ui| {})]);
world
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, _ui| {})]);
let foreground: Vec<Entity> = world
.root_scope()
.query::<Entity>()
.by(|archetype: &unlit_ecs::Archetype| archetype.has::<Foreground>())
.collect();
assert_eq!(foreground.len(), 1, "the filter picked one of two panels");
let all: Vec<Entity> = world
.root_scope()
.query::<(&UiPanel, Entity)>()
.map(|(_ui_panel, e)| e)
.collect();
assert_eq!(all.len(), 2, "the unfiltered query sees both");
assert!(
all.contains(&foreground[0]),
"the filtered panel is one of them"
);
}
#[test]
fn the_frame_events_reach_the_panels() {
let (mut world, ctx) = test_world();
let target = test_target();
let seen: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let recorded = Rc::clone(&seen);
world
.root_scope_mut()
.spawn(bundle![UiPanel::new(move |_world, _entity, ui| {
ui.text_edit_singleline(&mut String::new());
let pressed = ui.input(|input| {
input
.events
.iter()
.filter_map(|event| match event {
egui::Event::Text(text) => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
});
recorded.borrow_mut().extend(pressed);
}),]);
let input = InputWriter::new();
world.root_scope_mut().spawn(InputFeed(input.clone()));
input.push(InputEvent::Resized { size_px: (128, 96) });
refresh_input(&world);
let mut source = UiSource::new();
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
source.build_scene(&world.root_scope(), ctx, target, &mut scene, &mut encoder);
input.push(InputEvent::Text(TextEvent("hi".to_string())));
refresh_input(&world);
source.build_scene(&world.root_scope(), ctx, target, &mut scene, &mut encoder);
assert_eq!(
&*seen.borrow(),
&["hi".to_string()],
"the frame's text event reached the panel's egui input, once"
);
}
#[test]
fn a_mouse_click_reaches_a_widget() {
fn button() -> egui::Rect {
egui::Rect::from_min_size(egui::Pos2::new(8.0, 8.0), egui::Vec2::new(48.0, 24.0))
}
let clicked = Rc::new(Cell::new(false));
let (mut world, ctx) = test_world();
let target = test_target();
let flag = Rc::clone(&clicked);
world
.root_scope_mut()
.spawn(bundle![UiPanel::new(move |_world, _entity, ui| {
let response = ui.allocate_rect(button(), egui::Sense::click());
if response.clicked() {
flag.set(true);
}
}),]);
let input = InputWriter::new();
world.root_scope_mut().spawn(InputFeed(input.clone()));
input.push(InputEvent::Resized { size_px: (128, 96) });
refresh_input(&world);
let mut source = UiSource::new();
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
source.build_scene(&world.root_scope(), ctx, target, &mut scene, &mut encoder);
assert!(!clicked.get(), "nothing is clicked before any input");
let centre = [button().center().x, button().center().y];
for event in [
MouseEvent::Moved { position: centre },
MouseEvent::Button {
position: centre,
button: MouseButton::Primary,
pressed: true,
modifiers: Modifiers::default(),
},
MouseEvent::Button {
position: centre,
button: MouseButton::Primary,
pressed: false,
modifiers: Modifiers::default(),
},
] {
input.push(InputEvent::Mouse(event));
}
refresh_input(&world);
source.build_scene(&world.root_scope(), ctx, target, &mut scene, &mut encoder);
assert!(
clicked.get(),
"a press and release inside a widget must click it"
);
}
#[test]
fn a_published_frame_is_drawn_until_a_newer_one_arrives() {
let (world, ctx) = test_world();
let info = test_target();
let mut other = World::new();
other
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, ui| {
ui.painter().rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(16.0)),
0.0,
egui::Color32::RED,
);
}),]);
let target = test_ui_target();
let frame = lay_out(
&other,
&egui::Context::default(),
raw_input(target, Some(0.0), true, Vec::new()),
target.screen,
);
let mut source = UiSource::new();
source.frames.publish(0, frame);
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
source.build_scene(&world.root_scope(), ctx, info, &mut scene, &mut encoder);
assert!(
!scene.draws.is_empty(),
"the published frame is drawn when it arrives"
);
source.build_scene(&world.root_scope(), ctx, info, &mut scene, &mut encoder);
assert!(
!scene.draws.is_empty(),
"the same frame is drawn again while no newer one arrives"
);
}
#[test]
fn a_displaced_frame_hands_its_textures_to_its_successor() {
let (world, ctx) = test_world();
let info = test_target();
let context = egui::Context::default();
let mut other = World::new();
other
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, ui| {
ui.label("a panel with text");
}),]);
let target = test_ui_target();
let run = |time: f64| {
lay_out(
&other,
&context,
raw_input(target, Some(time), true, Vec::new()),
target.screen,
)
};
let first = run(0.0);
let second = run(1.0);
assert!(
!first.textures.is_empty(),
"the first pass writes the font atlas"
);
assert!(
second.textures.is_empty(),
"the second pass writes nothing new"
);
let mut source = UiSource::new();
source.frames.publish(0, first);
source.frames.publish(0, second);
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
source.build_scene(&world.root_scope(), ctx, info, &mut scene, &mut encoder);
assert!(
!scene.draws.is_empty(),
"the frame that survived carries the atlas its predecessor wrote"
);
}
#[test]
fn a_frame_laid_out_at_another_size_hands_its_textures_over() {
let (world, ctx) = test_world();
let target = test_ui_target();
let mut other = World::new();
other
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, ui| {
ui.label("a panel with text");
}),]);
let frame = lay_out(
&other,
&egui::Context::default(),
raw_input(target, Some(0.0), true, Vec::new()),
target.screen,
);
assert!(
!frame.textures.is_empty(),
"the first pass writes the font atlas"
);
let mut source = UiSource::new();
source.frames.publish(0, frame);
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
let [width, height] = target.screen.size_in_pixels;
let resized = PassInfo {
width: 2 * width,
height: 2 * height,
..test_target()
};
source.build_scene(&world.root_scope(), ctx, resized, &mut scene, &mut encoder);
assert!(
scene.draws.is_empty(),
"a frame laid out at another size is not drawn"
);
source.frames.with_frames(|slots| {
let frame = slots[0].as_ref().expect("the frame stays in its slot");
assert!(
frame.textures.is_empty(),
"its updates were taken and uploaded"
);
});
source.build_scene(
&world.root_scope(),
ctx,
test_target(),
&mut scene,
&mut encoder,
);
assert!(
!scene.draws.is_empty(),
"its textures were not lost to the pass that could not draw it"
);
}
#[test]
fn two_contexts_keep_their_textures_apart() {
let target = test_ui_target();
let mut other = World::new();
other
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, ui| {
ui.painter().rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(16.0)),
0.0,
egui::Color32::RED,
);
}),]);
let frame = lay_out(
&other,
&egui::Context::default(),
raw_input(target, Some(0.0), true, Vec::new()),
target.screen,
);
let ids = frame
.primitives
.iter()
.filter_map(|primitive| match &primitive.primitive {
egui::epaint::Primitive::Mesh(mesh) => Some(mesh.texture_id),
_ => None,
})
.collect::<Vec<_>>();
assert!(!ids.is_empty(), "the frame names a texture");
let mut source = UiSource::new();
source.frames.publish(0, frame);
let drawn = source.frames.with_frames(|slots| {
slots[0]
.as_ref()
.expect("published")
.primitives
.iter()
.filter_map(|primitive| match &primitive.primitive {
egui::epaint::Primitive::Mesh(mesh) => Some(mesh.texture_id),
_ => None,
})
.collect::<Vec<_>>()
});
assert_eq!(ids, drawn, "publishing leaves the ids alone");
}
#[test]
fn a_world_without_input_state_draws_an_idle_frame() {
let (mut world, ctx) = test_world();
let target = test_target();
world
.root_scope_mut()
.spawn(bundle![UiPanel::new(|_world, _entity, ui| {
ui.painter().rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::Vec2::splat(16.0)),
0.0,
egui::Color32::RED,
);
}),]);
let mut source = UiSource::new();
let mut encoder = encoder(&world, ctx);
let mut scene = Scene::new();
source.build_scene(&world.root_scope(), ctx, target, &mut scene, &mut encoder);
assert!(
!scene.draws.is_empty(),
"a panel draws with no input in the world"
);
}
}