From c4603c5c88847c085d5639993b05080e4c6a0eb2 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Thu, 5 Dec 2024 18:25:57 -0800 Subject: [PATCH] listen: Split `all_is_cubes::listen` into a library, `nosy`. This commit contains only the minimal migration changes; in particular, `all_is_cubes::listen` still exists, and contains aliases for the old item names rather than replacing them everywhere. Rationale: * Reduces the build time of `all-is-cubes` (both that crate in particular, and the whole project) by moving code into dependencies. * Makes the functionality more available to other projects (my own and others) without taking a dependency on `all-is-cubes`. A benefit of the migration is that the replacement of `ListenableSource` with `DynSource` allows for sources that are neither `ListenableCell` (now `Cell`) nor constants. We do not take advantage of this yet. --- CHANGELOG.md | 3 + Cargo.lock | 13 + Cargo.toml | 1 + .../src/city/exhibits/prelude.rs | 2 +- all-is-cubes-content/src/city/exhibits/ui.rs | 2 +- all-is-cubes-desktop/src/record.rs | 15 +- all-is-cubes-desktop/src/startup.rs | 6 +- all-is-cubes-desktop/src/terminal.rs | 4 +- all-is-cubes-desktop/src/winit.rs | 1 + all-is-cubes-gpu/src/common/reloadable.rs | 6 +- all-is-cubes-gpu/src/in_wgpu/headless.rs | 4 +- all-is-cubes-gpu/src/in_wgpu/pipelines.rs | 8 +- .../src/in_wgpu/raytrace_to_texture.rs | 4 +- .../src/in_wgpu/shader_testing.rs | 4 +- all-is-cubes-gpu/src/in_wgpu/shaders.rs | 13 +- all-is-cubes-gpu/src/in_wgpu/space.rs | 2 +- all-is-cubes-mesh/src/dynamic/chunked_mesh.rs | 2 +- all-is-cubes-render/benches/raytrace.rs | 12 +- all-is-cubes-render/src/camera/stdcam.rs | 52 +-- all-is-cubes-render/src/raytracer/renderer.rs | 10 +- all-is-cubes-ui/src/apps/input.rs | 12 +- all-is-cubes-ui/src/apps/session.rs | 40 +-- all-is-cubes-ui/src/inv_watch.rs | 9 +- all-is-cubes-ui/src/ui_content/hud.rs | 14 +- .../src/ui_content/notification.rs | 4 +- all-is-cubes-ui/src/ui_content/vui_manager.rs | 32 +- all-is-cubes-ui/src/vui/widgets/button.rs | 10 +- all-is-cubes-ui/src/vui/widgets/crosshair.rs | 6 +- .../src/vui/widgets/progress_bar.rs | 8 +- all-is-cubes-ui/src/vui/widgets/toolbar.rs | 4 +- all-is-cubes-wasm/Cargo.lock | 13 + all-is-cubes/Cargo.toml | 2 + all-is-cubes/benches/space.rs | 6 +- all-is-cubes/src/block.rs | 6 +- all-is-cubes/src/block/block_def.rs | 11 +- all-is-cubes/src/block/tests.rs | 4 +- all-is-cubes/src/character.rs | 15 +- all-is-cubes/src/listen.rs | 272 ++------------ all-is-cubes/src/listen/cell.rs | 340 ------------------ all-is-cubes/src/listen/listeners.rs | 242 +------------ all-is-cubes/src/listen/notifier.rs | 280 --------------- all-is-cubes/src/listen/store.rs | 298 --------------- all-is-cubes/src/listen/util.rs | 276 -------------- all-is-cubes/src/raytracer/updating.rs | 21 +- all-is-cubes/src/space.rs | 10 +- all-is-cubes/src/space/palette.rs | 13 +- all-is-cubes/src/universe/handle.rs | 4 +- all-is-cubes/src/universe/universe_txn.rs | 2 +- all-is-cubes/src/util/maybe_sync.rs | 8 + test-renderers/src/render.rs | 2 +- test-renderers/src/test_cases.rs | 38 +- test-renderers/tests/ui.rs | 4 +- 52 files changed, 281 insertions(+), 1889 deletions(-) delete mode 100644 all-is-cubes/src/listen/cell.rs delete mode 100644 all-is-cubes/src/listen/notifier.rs delete mode 100644 all-is-cubes/src/listen/store.rs delete mode 100644 all-is-cubes/src/listen/util.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9280bd364..455a52c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ - `block::EvalBlockError` is now a `struct` with an inner `ErrorKind` enum, instead of an enum, and contains more information. - `block::Move`’s means of construction have been changed to be more systematic and orthogonal. In particular, paired moves are constructed from unpaired ones. + - The `listen` module is now a reexport of the separate library [`nosy`](https://docs.rs/nosy). + Many items have changed in name and signature. + - `math::FaceMap::repeat()` has been renamed to `splat()`, for consistency with the same concept in the `euclid` vector types which we use. * `math::Geometry` is now `math::Wireframe`, and its `translate()` method has been replaced with inherent methods on its implementors. - `math::GridAab::expand()` now takes unsigned values; use `GridAab::shrink()` instead of negative ones. This allows both versions to never panic. diff --git a/Cargo.lock b/Cargo.lock index ade265a7d..5f86470d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,7 @@ dependencies = [ "log", "manyfmt", "mutants", + "nosy", "num-traits", "ordered-float", "paste", @@ -3727,6 +3728,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nosy" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb59fd933a0061e11498ce4eebcb47a2b8baa8f22b163c5d21ea4d090cdd1cb2" +dependencies = [ + "arrayvec", + "cfg-if", + "manyfmt", + "mutants", +] + [[package]] name = "ntapi" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 3fac5859e..c58162708 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ macro_rules_attribute = { version = "0.2.0", default-features = false } manyfmt = { version = "0.1.0", default-features = false } mutants = { version = "0.0.3", default-features = false } noise = { version = "0.9.0", default-features = false } +nosy = { version = "0.1.0", default-features = false } num-traits = { version = "0.2.19", default-features = false } ordered-float = { version = "4.2.0", default-features = false } paste = {version = "1.0.15", default-features = false } diff --git a/all-is-cubes-content/src/city/exhibits/prelude.rs b/all-is-cubes-content/src/city/exhibits/prelude.rs index 1441560ab..6759a6651 100644 --- a/all-is-cubes-content/src/city/exhibits/prelude.rs +++ b/all-is-cubes-content/src/city/exhibits/prelude.rs @@ -26,7 +26,7 @@ pub(super) use all_is_cubes::euclid::{ size3, vec3, Point3D, Rotation2D, Size3D, Vector2D, Vector3D, }; pub(super) use all_is_cubes::linking::{BlockProvider, InGenError}; -pub(super) use all_is_cubes::listen::ListenableSource; +pub(super) use all_is_cubes::listen; pub(super) use all_is_cubes::math::{ ps32, rgb_const, rgba_const, zo32, Cube, Face6, FaceMap, FreeCoordinate, GridAab, GridCoordinate, GridPoint, GridRotation, GridSize, GridVector, Gridgid, PositiveSign, Rgb, diff --git a/all-is-cubes-content/src/city/exhibits/ui.rs b/all-is-cubes-content/src/city/exhibits/ui.rs index cc90b0981..ce35f0f78 100644 --- a/all-is-cubes-content/src/city/exhibits/ui.rs +++ b/all-is-cubes-content/src/city/exhibits/ui.rs @@ -83,7 +83,7 @@ fn UI_PROGRESS_BAR(ctx: Context<'_>) { vui::LayoutTree::leaf(widgets::ProgressBar::new( ctx.widget_theme, Face6::PX, - ListenableSource::constant(widgets::ProgressBarState::new(fraction)), + listen::constant(widgets::ProgressBarState::new(fraction)), )) }; diff --git a/all-is-cubes-desktop/src/record.rs b/all-is-cubes-desktop/src/record.rs index b16b6e5dc..a233f4ff5 100644 --- a/all-is-cubes-desktop/src/record.rs +++ b/all-is-cubes-desktop/src/record.rs @@ -7,7 +7,7 @@ use std::time::Duration; use anyhow::Context; -use all_is_cubes::listen::{self, Listen as _, ListenableSource}; +use all_is_cubes::listen::{self, Listen as _}; use all_is_cubes::universe::Universe; use all_is_cubes_port::gltf::{GltfDataDestination, GltfWriter}; use all_is_cubes_port::{ExportSet, Format}; @@ -168,16 +168,16 @@ impl Recorder { let export_set = if options.save_all || export_format == Format::AicJson { ExportSet::all_of_universe(universe) } else { - ExportSet::from_spaces(vec![cameras.world_space().get().ok_or_else( - || match universe.whence.document_name() { + ExportSet::from_spaces(vec![cameras.world_space().get().ok_or_else(|| { + match universe.whence.document_name() { None => { anyhow::anyhow!("universe contains no default space to export") } Some(name) => anyhow::anyhow!( "universe {name:?} contains no default space to export", ), - }, - )?]) + } + })?]) }; RecorderInner::Export { @@ -208,7 +208,7 @@ impl Recorder { let mut renderer = RtRenderer::new( rec.cameras.clone(), Box::new(|v| v), - ListenableSource::constant(Arc::new(())), + listen::constant(Arc::new(())), ); renderer.update(None).unwrap(); @@ -328,8 +328,9 @@ impl Recorder { impl listen::Listen for Recorder { type Msg = Status; + type Listener = as listen::Listen>::Listener; - fn listen_raw(&self, listener: listen::DynListener) { + fn listen_raw(&self, listener: Self::Listener) { if let Some(notifier) = self.status_notifier.upgrade() { notifier.listen_raw(listener) } diff --git a/all-is-cubes-desktop/src/startup.rs b/all-is-cubes-desktop/src/startup.rs index f933b69be..12e8b2a85 100644 --- a/all-is-cubes-desktop/src/startup.rs +++ b/all-is-cubes-desktop/src/startup.rs @@ -113,9 +113,9 @@ pub fn inner_main( // Note that this does NOT use the session's viewport_cell, so that the recording can // have a consistent, as-requested size, regardless of what other rendering might be // doing. (Of course, the UI will fail to adapt, but there isn't much to do about that.) - let recording_cameras = ctx.create_cameras( - all_is_cubes::listen::ListenableSource::constant(record_options.viewport()), - ); + let recording_cameras = ctx.create_cameras(all_is_cubes::listen::constant( + record_options.viewport(), + )); let recorder = ctx.with_universe(|universe| { record::configure_universe_for_recording( diff --git a/all-is-cubes-desktop/src/terminal.rs b/all-is-cubes-desktop/src/terminal.rs index edeb42f3f..4a30e6e43 100644 --- a/all-is-cubes-desktop/src/terminal.rs +++ b/all-is-cubes-desktop/src/terminal.rs @@ -13,7 +13,7 @@ use ratatui::layout::Rect; use all_is_cubes::arcstr::literal_substr; use all_is_cubes::euclid::{Point2D, Size2D}; -use all_is_cubes::listen::{ListenableCell, ListenableSource}; +use all_is_cubes::listen::{self, ListenableCell}; use all_is_cubes::math::Rgba; use all_is_cubes_render::camera::{self, Camera, StandardCameras, Viewport}; use all_is_cubes_render::raytracer::{ @@ -108,7 +108,7 @@ pub fn create_terminal_session( .send(RtRenderer::new( cameras.clone(), Box::new(|v| v), - ListenableSource::constant(Arc::new(())), + listen::constant(Arc::new(())), )) .unwrap(); } diff --git a/all-is-cubes-desktop/src/winit.rs b/all-is-cubes-desktop/src/winit.rs index 9bcfdfc05..a78b1f7d6 100644 --- a/all-is-cubes-desktop/src/winit.rs +++ b/all-is-cubes-desktop/src/winit.rs @@ -164,6 +164,7 @@ pub fn winit_main_loop_and_init( } /// Creates a [`DesktopSession`] that can be run in an [`winit`] event loop. +#[allow(clippy::large_stack_frames, reason = "wildly overestimated somehow")] pub async fn create_winit_wgpu_desktop_session( executor: Arc, session: Session, diff --git a/all-is-cubes-gpu/src/common/reloadable.rs b/all-is-cubes-gpu/src/common/reloadable.rs index 5bb5537ec..af29fd2cd 100644 --- a/all-is-cubes-gpu/src/common/reloadable.rs +++ b/all-is-cubes-gpu/src/common/reloadable.rs @@ -1,14 +1,14 @@ //! “Hot-reloadable” data sources such as shaders. //! //! This module builds on top of the `resource` library to add change notification -//! via a background thread and all-is-cubes's `ListenableSource` mechanism. +//! via a background thread and all-is-cubes's `ListenableCell` mechanism. use std::sync::{Arc, LazyLock, Mutex, PoisonError}; use std::time::Duration; use resource::Resource; -use all_is_cubes::listen::{ListenableCell, ListenableSource}; +use all_is_cubes::listen::{self, ListenableCell}; #[derive(Clone)] pub(crate) struct Reloadable(Arc); @@ -55,7 +55,7 @@ impl Reloadable { } } - pub fn as_source(&self) -> ListenableSource> { + pub fn as_source(&self) -> listen::DynSource> { self.0.cell.as_source() } } diff --git a/all-is-cubes-gpu/src/in_wgpu/headless.rs b/all-is-cubes-gpu/src/in_wgpu/headless.rs index ae7e1e169..6870add74 100644 --- a/all-is-cubes-gpu/src/in_wgpu/headless.rs +++ b/all-is-cubes-gpu/src/in_wgpu/headless.rs @@ -6,7 +6,7 @@ use futures_channel::oneshot; use futures_core::future::BoxFuture; use all_is_cubes::character::Cursor; -use all_is_cubes::listen::{DirtyFlag, ListenableSource}; +use all_is_cubes::listen::{self, DirtyFlag}; use all_is_cubes::util::Executor; use all_is_cubes_render::camera::{StandardCameras, Viewport}; use all_is_cubes_render::{Flaws, HeadlessRenderer, RenderError, Rendering}; @@ -104,7 +104,7 @@ struct RendererImpl { queue: Arc, color_texture: wgpu::Texture, everything: super::EverythingRenderer, - viewport_source: ListenableSource, + viewport_source: listen::DynSource, viewport_dirty: DirtyFlag, flaws: Flaws, } diff --git a/all-is-cubes-gpu/src/in_wgpu/pipelines.rs b/all-is-cubes-gpu/src/in_wgpu/pipelines.rs index 023480514..dd5b56b21 100644 --- a/all-is-cubes-gpu/src/in_wgpu/pipelines.rs +++ b/all-is-cubes-gpu/src/in_wgpu/pipelines.rs @@ -2,7 +2,7 @@ use std::mem; use std::sync::Arc; use all_is_cubes::listen::DirtyFlag; -use all_is_cubes::listen::{Listen, ListenableSource}; +use all_is_cubes::listen::{self, Listen as _}; use all_is_cubes_render::camera::{GraphicsOptions, TransparencyOption}; use crate::in_wgpu::frame_texture::FramebufferTextures; @@ -26,7 +26,7 @@ pub(crate) struct Pipelines { /// Tracks whether we need to rebuild pipelines for any reasons. dirty: DirtyFlag, - graphics_options: ListenableSource>, + graphics_options: listen::DynSource>, /// Layout for the camera buffer. pub(crate) camera_bind_group_layout: wgpu::BindGroupLayout, @@ -84,7 +84,7 @@ impl Pipelines { device: &wgpu::Device, shaders: &Shaders, fb: &FramebufferTextures, - graphics_options: ListenableSource>, + graphics_options: listen::DynSource>, ) -> Self { // TODO: This is a hazard we should remove. `Pipelines` needs to be consistent with // other objects (in particular, pipeline versus framebuffer sample_count), and so @@ -551,7 +551,7 @@ impl Pipelines { fb, mem::replace( &mut self.graphics_options, - ListenableSource::constant(Default::default()), + listen::Constant::default().into(), ), ); } diff --git a/all-is-cubes-gpu/src/in_wgpu/raytrace_to_texture.rs b/all-is-cubes-gpu/src/in_wgpu/raytrace_to_texture.rs index 91be53034..4bbad166a 100644 --- a/all-is-cubes-gpu/src/in_wgpu/raytrace_to_texture.rs +++ b/all-is-cubes-gpu/src/in_wgpu/raytrace_to_texture.rs @@ -16,7 +16,7 @@ use all_is_cubes::character::Cursor; use all_is_cubes::drawing::embedded_graphics::pixelcolor::PixelColor; use all_is_cubes::drawing::embedded_graphics::{draw_target::DrawTarget, prelude::Point, Pixel}; use all_is_cubes::euclid::{point2, vec2, Box2D}; -use all_is_cubes::listen::ListenableSource; +use all_is_cubes::listen; use all_is_cubes::math::{Rgb, Rgba, VectorOps as _}; use all_is_cubes_render::camera::{area_usize, Camera, StandardCameras, Viewport}; use all_is_cubes_render::raytracer::{ColorBuf, RtRenderer}; @@ -51,7 +51,7 @@ impl RaytraceToTexture { rtr: RtRenderer::new( cameras, Box::new(raytracer_size_policy), - ListenableSource::constant(Arc::new(())), + listen::constant(Arc::new(())), ), pixel_picker: PixelPicker::new(initial_viewport, false), dirty_pixels: initial_viewport.pixel_count().unwrap(), diff --git a/all-is-cubes-gpu/src/in_wgpu/shader_testing.rs b/all-is-cubes-gpu/src/in_wgpu/shader_testing.rs index 4d22e9ae6..9adbb3d62 100644 --- a/all-is-cubes-gpu/src/in_wgpu/shader_testing.rs +++ b/all-is-cubes-gpu/src/in_wgpu/shader_testing.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use wgpu::util::DeviceExt as _; use all_is_cubes::euclid::{point3, Rotation3D}; -use all_is_cubes::listen::ListenableSource; +use all_is_cubes::listen; use all_is_cubes::math::{ps64, Face6, FreeVector, GridSize, GridVector, Rgba}; use all_is_cubes::time; use all_is_cubes_mesh::{BlockVertex, Coloring}; @@ -82,7 +82,7 @@ where &device, &shaders, &fbt, - ListenableSource::constant(Arc::new(GraphicsOptions::default())), + listen::constant(Arc::new(GraphicsOptions::default())), ); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { diff --git a/all-is-cubes-gpu/src/in_wgpu/shaders.rs b/all-is-cubes-gpu/src/in_wgpu/shaders.rs index 86820a745..263b65add 100644 --- a/all-is-cubes-gpu/src/in_wgpu/shaders.rs +++ b/all-is-cubes-gpu/src/in_wgpu/shaders.rs @@ -89,8 +89,9 @@ impl Shaders { impl listen::Listen for Shaders { type Msg = (); - fn listen_raw(&self, listener: listen::DynListener<()>) { - self.modules_changed.listen(listener) + type Listener = as listen::Listen>::Listener; + fn listen_raw(&self, listener: Self::Listener) { + self.modules_changed.listen_raw(listener) } } @@ -99,18 +100,14 @@ impl listen::Listen for Shaders { /// The code on initial creation must be valid or creation will panic. pub(crate) struct ReloadableShader { label: String, - source: listen::ListenableSource>, + source: listen::DynSource>, dirty: listen::DirtyFlag, current_module: Identified, next_module: Option>>, } impl ReloadableShader { - fn new( - device: &wgpu::Device, - label: String, - wgsl_source: listen::ListenableSource>, - ) -> Self { + fn new(device: &wgpu::Device, label: String, wgsl_source: listen::DynSource>) -> Self { let dirty = listen::DirtyFlag::listening(false, &wgsl_source); let current_module = Identified::new(device.create_shader_module(wgpu::ShaderModuleDescriptor { diff --git a/all-is-cubes-gpu/src/in_wgpu/space.rs b/all-is-cubes-gpu/src/in_wgpu/space.rs index 67bea5a57..031d6de9d 100644 --- a/all-is-cubes-gpu/src/in_wgpu/space.rs +++ b/all-is-cubes-gpu/src/in_wgpu/space.rs @@ -285,7 +285,7 @@ impl SpaceRenderer { ) -> Result { let start_time = I::now(); - let todo = &mut self.todo.lock().unwrap(); + let todo = &mut self.todo.lock(); let Some(csm) = &mut self.csm else { if mem::take(&mut todo.sky) { diff --git a/all-is-cubes-mesh/src/dynamic/chunked_mesh.rs b/all-is-cubes-mesh/src/dynamic/chunked_mesh.rs index f0c680fd6..31cb83ddc 100644 --- a/all-is-cubes-mesh/src/dynamic/chunked_mesh.rs +++ b/all-is-cubes-mesh/src/dynamic/chunked_mesh.rs @@ -265,7 +265,7 @@ where let view_chunk_is_different = self.view_chunk != view_chunk; self.view_chunk = view_chunk; - let todo: &mut CsmTodo = &mut self.todo.lock().unwrap(); + let todo: &mut CsmTodo = &mut self.todo.lock(); let space = &*if let Ok(space) = self.space.read() { space diff --git a/all-is-cubes-render/benches/raytrace.rs b/all-is-cubes-render/benches/raytrace.rs index b0a58ab13..6d927cd26 100644 --- a/all-is-cubes-render/benches/raytrace.rs +++ b/all-is-cubes-render/benches/raytrace.rs @@ -8,7 +8,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use all_is_cubes::character::Character; use all_is_cubes::content::testing::lighting_bench_space; use all_is_cubes::euclid::size3; -use all_is_cubes::listen::ListenableSource; +use all_is_cubes::listen; use all_is_cubes::universe::{Handle, Universe}; use all_is_cubes::util::yield_progress_for_testing; use all_is_cubes_render::camera::{ @@ -46,13 +46,13 @@ impl TestData { options_fn(&mut options); let mut renderer = RtRenderer::new( StandardCameras::new( - ListenableSource::constant(Arc::new(options)), - ListenableSource::constant(Viewport::with_scale(1.0, [64, 16])), - ListenableSource::constant(Some(self.character.clone())), - ListenableSource::constant(Arc::new(UiViewState::default())), + listen::constant(Arc::new(options)), + listen::constant(Viewport::with_scale(1.0, [64, 16])), + listen::constant(Some(self.character.clone())), + listen::constant(Arc::new(UiViewState::default())), ), Box::new(core::convert::identity), - ListenableSource::constant(Arc::new(())), + listen::constant(Arc::new(())), ); renderer.update(None).unwrap(); renderer diff --git a/all-is-cubes-render/src/camera/stdcam.rs b/all-is-cubes-render/src/camera/stdcam.rs index fc0c03c95..da3835cbb 100644 --- a/all-is-cubes-render/src/camera/stdcam.rs +++ b/all-is-cubes-render/src/camera/stdcam.rs @@ -2,7 +2,7 @@ use alloc::sync::Arc; use core::fmt; use all_is_cubes::character::{cursor_raycast, Character, Cursor}; -use all_is_cubes::listen::{DirtyFlag, ListenableCell, ListenableSource}; +use all_is_cubes::listen::{self, DirtyFlag, ListenableCell}; use all_is_cubes::math::FreeCoordinate; use all_is_cubes::space::Space; use all_is_cubes::universe::{Handle, Universe}; @@ -56,7 +56,7 @@ impl Layers { /// Bundle of inputs specifying the “standard” configuration of [`Camera`]s and other /// things to render an All is Cubes scene and user interface. /// -/// All of its data is provided through [`ListenableSource`]s, and consists of: +/// All of its data is provided through [`listen::DynSource`]s, and consists of: /// /// * [`GraphicsOptions`]. /// * A [`Viewport`] specifying the dimensions of image to render. @@ -68,7 +68,7 @@ impl Layers { /// and used to update the [`Camera`] data. Those cameras, and copies of the input /// data, are then available for use while rendering. /// -/// Because every input is a [`ListenableSource`], it is never necessary to call a setter. +/// Because every input is a [`listen::DynSource`], it is never necessary to call a setter. /// Every [`StandardCameras`] which was created with the same sources will have the same /// results (after `update()`). /// @@ -78,10 +78,10 @@ impl Layers { #[derive(Debug)] pub struct StandardCameras { /// Cameras are synced with this - graphics_options: ListenableSource>, + graphics_options: listen::DynSource>, graphics_options_dirty: DirtyFlag, - character_source: ListenableSource>>, + character_source: listen::DynSource>>, /// Tracks whether the character was replaced (not whether its view changed). character_dirty: DirtyFlag, character: Option>, @@ -89,11 +89,11 @@ pub struct StandardCameras { /// TODO: This should be in a `Layers` along with `ui_state`...? world_space: ListenableCell>>, - ui_source: ListenableSource>, + ui_source: listen::DynSource>, ui_dirty: DirtyFlag, ui_space: Option>, - viewport_source: ListenableSource, + viewport_source: listen::DynSource, viewport_dirty: DirtyFlag, cameras: Layers, @@ -104,10 +104,10 @@ impl StandardCameras { /// want to discourage use of this directly. #[doc(hidden)] pub fn new( - graphics_options: ListenableSource>, - viewport_source: ListenableSource, - character_source: ListenableSource>>, - ui_source: ListenableSource>, + graphics_options: listen::DynSource>, + viewport_source: listen::DynSource, + character_source: listen::DynSource>>, + ui_source: listen::DynSource>, ) -> Self { // TODO: Add a unit test that each of these listeners works as intended. // TODO: This is also an awful lot of repetitive code; we should design a pattern @@ -153,10 +153,10 @@ impl StandardCameras { universe: &Universe, ) -> Self { Self::new( - ListenableSource::constant(Arc::new(graphics_options)), - ListenableSource::constant(viewport), - ListenableSource::constant(universe.get_default_character()), - ListenableSource::constant(Default::default()), + listen::constant(Arc::new(graphics_options)), + listen::constant(viewport), + listen::constant(universe.get_default_character()), + listen::constant(Default::default()), ) } @@ -264,7 +264,7 @@ impl StandardCameras { /// Returns a clone of the source of graphics options that this [`StandardCameras`] /// was created with. - pub fn graphics_options_source(&self) -> ListenableSource> { + pub fn graphics_options_source(&self) -> listen::DynSource> { self.graphics_options.clone() } @@ -281,10 +281,10 @@ impl StandardCameras { /// Returns the space that should be drawn as the game world, using `self.cameras().world`. /// - /// This is a [`ListenableSource`] to make it simple to cache the Space rendering data and + /// This is a [`listen::DynSource`] to make it simple to cache the Space rendering data and /// follow space transitions. /// It updates when [`Self::update()`] is called. - pub fn world_space(&self) -> ListenableSource>> { + pub fn world_space(&self) -> listen::DynSource>> { self.world_space.as_source() } @@ -293,7 +293,7 @@ impl StandardCameras { /// This implements [`GraphicsOptions::show_ui`] by returning [`None`] when the option is /// false. /// - /// TODO: Make this also a [`ListenableSource`] + /// TODO: Make this also a [`listen::DynSource`] pub fn ui_space(&self) -> Option<&Handle> { self.ui_space.as_ref() } @@ -307,7 +307,7 @@ impl StandardCameras { } /// Returns a clone of the viewport source this is following. - pub fn viewport_source(&self) -> ListenableSource { + pub fn viewport_source(&self) -> listen::DynSource { self.viewport_source.clone() } @@ -411,10 +411,10 @@ mod tests { fn cameras_follow_character_and_world() { let character_cell = ListenableCell::new(None); let mut cameras = StandardCameras::new( - ListenableSource::constant(Arc::new(GraphicsOptions::default())), - ListenableSource::constant(Viewport::ARBITRARY), + listen::constant(Arc::new(GraphicsOptions::default())), + listen::constant(Viewport::ARBITRARY), character_cell.as_source(), - ListenableSource::constant(Arc::new(UiViewState::default())), + listen::constant(Arc::new(UiViewState::default())), ); let world_source = cameras.world_space(); @@ -454,9 +454,9 @@ mod tests { let options_cell = ListenableCell::new(Arc::new(GraphicsOptions::default())); let mut cameras = StandardCameras::new( options_cell.as_source(), - ListenableSource::constant(Viewport::ARBITRARY), - ListenableSource::constant(None), - ListenableSource::constant(Arc::new(UiViewState::default())), + listen::constant(Viewport::ARBITRARY), + listen::constant(None), + listen::constant(Arc::new(UiViewState::default())), ); let mut cameras2 = cameras.clone(); diff --git a/all-is-cubes-render/src/raytracer/renderer.rs b/all-is-cubes-render/src/raytracer/renderer.rs index 2e9061909..92957f527 100644 --- a/all-is-cubes-render/src/raytracer/renderer.rs +++ b/all-is-cubes-render/src/raytracer/renderer.rs @@ -11,7 +11,7 @@ use core::fmt; use all_is_cubes::character::Cursor; use all_is_cubes::content::palette; use all_is_cubes::euclid::{self, point2, vec2}; -use all_is_cubes::listen::ListenableSource; +use all_is_cubes::listen; use all_is_cubes::math::{Rgba, ZeroOne}; use all_is_cubes::space::Space; use all_is_cubes::universe::Handle; @@ -40,7 +40,7 @@ pub struct RtRenderer { /// The output images will alway size_policy: Box Viewport + Send + Sync>, - custom_options: ListenableSource>, + custom_options: listen::DynSource>, /// Borrowable copy of the value in `custom_options`. custom_options_cache: Arc, @@ -61,7 +61,7 @@ where pub fn new( cameras: StandardCameras, size_policy: Box Viewport + Send + Sync>, - custom_options: ListenableSource>, + custom_options: listen::DynSource>, ) -> Self { RtRenderer { rts: Layers::>::default(), @@ -95,8 +95,8 @@ where fn sync_space( cached_rt: &mut Option>, optional_space: Option<&Handle>, - graphics_options_source: &ListenableSource>, - custom_options_source: &ListenableSource>, + graphics_options_source: &listen::DynSource>, + custom_options_source: &listen::DynSource>, anything_changed: &mut bool, ) -> Result<(), RenderError> where diff --git a/all-is-cubes-ui/src/apps/input.rs b/all-is-cubes-ui/src/apps/input.rs index 0d0278cff..fcd856910 100644 --- a/all-is-cubes-ui/src/apps/input.rs +++ b/all-is-cubes-ui/src/apps/input.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use all_is_cubes::character::Character; use all_is_cubes::euclid::{Point2D, Vector2D}; -use all_is_cubes::listen::{ListenableCell, ListenableSource}; +use all_is_cubes::listen::{self, ListenableCell}; use all_is_cubes::math::{zo32, FreeCoordinate, FreeVector}; use all_is_cubes::time::Tick; use all_is_cubes::universe::{Handle, Universe}; @@ -437,7 +437,7 @@ impl InputProcessor { /// interpreted as view rotation) is currently active. /// /// This value may be toggled by in-game UI. - pub fn mouselook_mode(&self) -> ListenableSource { + pub fn mouselook_mode(&self) -> listen::DynSource { self.mouselook_mode.as_source() } @@ -581,12 +581,12 @@ mod tests { let (cctx, _) = flume::bounded(1); let mut ui = crate::ui_content::Vui::new( &InputProcessor::new(), - ListenableSource::constant(None), + listen::constant(None), paused.as_source(), - ListenableSource::constant(Arc::new(GraphicsOptions::default())), + listen::constant(Arc::new(GraphicsOptions::default())), cctx, - ListenableSource::constant(Viewport::ARBITRARY), - ListenableSource::constant(None), + listen::constant(Viewport::ARBITRARY), + listen::constant(None), None, None, ) diff --git a/all-is-cubes-ui/src/apps/session.rs b/all-is-cubes-ui/src/apps/session.rs index 1ea59eb63..2ecee38a5 100644 --- a/all-is-cubes-ui/src/apps/session.rs +++ b/all-is-cubes-ui/src/apps/session.rs @@ -25,7 +25,7 @@ use all_is_cubes::character::{Character, Cursor}; use all_is_cubes::fluff::Fluff; use all_is_cubes::inv::ToolError; use all_is_cubes::listen::{ - self, Listen as _, ListenableCell, ListenableCellWithLocal, ListenableSource, Listener as _, + self, Listen as _, ListenableCell, ListenableCellWithLocal, Listener as _, }; use all_is_cubes::save::WhenceUniverse; use all_is_cubes::space::{self, Space}; @@ -214,7 +214,7 @@ impl Session { } /// Returns a source for the [`Character`] that should be shown to the user. - pub fn character(&self) -> ListenableSource>> { + pub fn character(&self) -> listen::DynSource>> { self.shuttle().game_character.as_source() } @@ -276,17 +276,17 @@ impl Session { /// Allows observing replacement of the current universe in this session, or updates to its /// [`WhenceUniverse`]. - pub fn universe_info(&self) -> ListenableSource { + pub fn universe_info(&self) -> listen::DynSource { self.shuttle().game_universe_info.as_source() } /// What the renderer should be displaying on screen for the UI. - pub fn ui_view(&self) -> ListenableSource> { + pub fn ui_view(&self) -> listen::DynSource> { self.shuttle().ui_view() } /// Allows reading, and observing changes to, the current graphics options. - pub fn graphics_options(&self) -> ListenableSource> { + pub fn graphics_options(&self) -> listen::DynSource> { self.shuttle().graphics_options.as_source() } @@ -297,7 +297,7 @@ impl Session { /// Returns a [`StandardCameras`] which may be used in rendering a view of this session, /// including following changes to the current character or universe. - pub fn create_cameras(&self, viewport_source: ListenableSource) -> StandardCameras { + pub fn create_cameras(&self, viewport_source: listen::DynSource) -> StandardCameras { StandardCameras::new( self.graphics_options(), viewport_source, @@ -340,7 +340,7 @@ impl Session { self.input_processor.apply_input( InputTargets { universe: Some(&mut shuttle.game_universe), - character: shuttle.game_character.borrow().as_ref(), + character: shuttle.game_character.get().as_ref(), paused: Some(&self.paused), graphics_options: Some(&shuttle.graphics_options), control_channel: Some(&self.control_channel_sender), @@ -682,10 +682,10 @@ impl Shuttle { } /// What the renderer should be displaying on screen for the UI. - fn ui_view(&self) -> ListenableSource> { + fn ui_view(&self) -> listen::DynSource> { match &self.ui { Some(ui) => ui.view(), - None => ListenableSource::constant(Arc::new(UiViewState::default())), // TODO: cache this to allocate less + None => listen::constant(Arc::new(UiViewState::default())), // TODO: cache this to allocate less } } @@ -703,7 +703,7 @@ impl Shuttle { } else { // Otherwise, it's a click inside the game world (even if the cursor hit nothing at all). // Character::click will validate against being a click in the wrong space. - if let Some(character_handle) = self.game_character.borrow() { + if let Some(character_handle) = self.game_character.get() { let transaction = Character::click( character_handle.clone(), self.cursor_result.as_ref(), @@ -745,7 +745,7 @@ impl Shuttle { { let character_read: Option> = self .game_character - .borrow() + .get() .as_ref() .map(|cref| cref.read().expect("TODO: decide how to handle error")); let space: Option<&Handle> = character_read.as_ref().map(|ch| &ch.space); @@ -763,9 +763,9 @@ impl Shuttle { #[must_use] #[expect(missing_debug_implementations)] pub struct SessionBuilder { - viewport_for_ui: Option>, + viewport_for_ui: Option>, - fullscreen_state: ListenableSource, + fullscreen_state: listen::DynSource, set_fullscreen: FullscreenSetter, quit: Option, @@ -777,7 +777,7 @@ impl Default for SessionBuilder { fn default() -> Self { Self { viewport_for_ui: None, - fullscreen_state: ListenableSource::constant(None), + fullscreen_state: listen::constant(None), set_fullscreen: None, quit: None, _instant: PhantomData, @@ -862,7 +862,7 @@ impl SessionBuilder { /// /// If this is not called, then the session will simulate a world but not present any /// controls for it other than those provided directly by the [`InputProcessor`]. - pub fn ui(mut self, viewport: ListenableSource) -> Self { + pub fn ui(mut self, viewport: listen::DynSource) -> Self { self.viewport_for_ui = Some(viewport); self } @@ -874,7 +874,7 @@ impl SessionBuilder { /// * `setter` is a function which attempts to change the fullscreen state. pub fn fullscreen( mut self, - state: ListenableSource>, + state: listen::DynSource>, setter: Option>, ) -> Self { self.fullscreen_state = state; @@ -1020,7 +1020,7 @@ impl> fmt::Display for InfoText<'_, I, T> { .session .shuttle .as_ref() - .and_then(|shuttle| shuttle.game_character.borrow().as_ref()) + .and_then(|shuttle| shuttle.game_character.get().as_ref()) { empty = false; write!(f, "{}", character_handle.read().unwrap().refmt(&fopt)).unwrap(); @@ -1123,7 +1123,7 @@ impl fmt::Debug for MainTaskContext { impl MainTaskContext { /// Returns a [`StandardCameras`] which may be used in rendering a view of this session, /// including following changes to the current character or universe. - pub fn create_cameras(&self, viewport_source: ListenableSource) -> StandardCameras { + pub fn create_cameras(&self, viewport_source: listen::DynSource) -> StandardCameras { self.with_ref(|shuttle| { StandardCameras::new( shuttle.graphics_options.as_source(), @@ -1376,7 +1376,7 @@ mod tests { // Set up task (that won't do anything until it's polled as part of stepping) let (send, recv) = oneshot::channel(); - let mut cameras = session.create_cameras(ListenableSource::constant(Viewport::ARBITRARY)); + let mut cameras = session.create_cameras(listen::constant(Viewport::ARBITRARY)); session.set_main_task({ let noticed_step = noticed_step.clone(); move |mut ctx| async move { @@ -1435,7 +1435,7 @@ mod tests { #[tokio::test] async fn input_is_processed_even_without_character() { let mut session = Session::::builder() - .ui(ListenableSource::constant(Viewport::ARBITRARY)) + .ui(listen::constant(Viewport::ARBITRARY)) .build() .await; assert!(!session.paused.get()); diff --git a/all-is-cubes-ui/src/inv_watch.rs b/all-is-cubes-ui/src/inv_watch.rs index 71e20b9d7..1c19c8d47 100644 --- a/all-is-cubes-ui/src/inv_watch.rs +++ b/all-is-cubes-ui/src/inv_watch.rs @@ -1,7 +1,7 @@ //! Support for widgets that display inventory contents. //! //! TODO: This is a pattern that, if it works out, probably generalizes to many other -//! "derived information from a `ListenableSource` that requires computation" and should become +//! "derived information from a `listen::DynSource` that requires computation" and should become //! general code that handles the re-listening problem. use alloc::sync::Arc; @@ -21,7 +21,7 @@ type Owner = Option>; #[derive(Debug)] pub(crate) struct InventoryWatcher { /// Source of what inventory we should be looking at. - inventory_source: listen::ListenableSource, + inventory_source: listen::DynSource, /// Last value gotten from `inventory_source`. inventory_owner: Owner, @@ -51,7 +51,7 @@ impl InventoryWatcher { /// /// `ui_universe` will be used to create anonymous resources used to depict the inventory. pub fn new( - inventory_source: listen::ListenableSource>>, + inventory_source: listen::DynSource>>, _ui_universe: &mut Universe, ) -> Self { let dirty = listen::DirtyFlag::new(true); @@ -183,8 +183,9 @@ impl InventoryWatcher { impl listen::Listen for InventoryWatcher { type Msg = WatcherChange; + type Listener = as listen::Listen>::Listener; - fn listen_raw(&self, listener: listen::DynListener) { + fn listen_raw(&self, listener: Self::Listener) { self.notifier.listen_raw(listener) } } diff --git a/all-is-cubes-ui/src/ui_content/hud.rs b/all-is-cubes-ui/src/ui_content/hud.rs index a3e2f2b41..07e848f3e 100644 --- a/all-is-cubes-ui/src/ui_content/hud.rs +++ b/all-is-cubes-ui/src/ui_content/hud.rs @@ -5,7 +5,7 @@ use std::sync::Mutex; use all_is_cubes::character::Character; use all_is_cubes::inv::Icons; use all_is_cubes::linking::BlockProvider; -use all_is_cubes::listen::ListenableSource; +use all_is_cubes::listen; use all_is_cubes::math::Face6; use all_is_cubes::universe::{Handle, Universe, UniverseTransaction}; use all_is_cubes::util::YieldProgress; @@ -29,11 +29,11 @@ pub(crate) struct HudInputs { pub cue_channel: CueNotifier, pub vui_control_channel: flume::Sender, pub app_control_channel: flume::Sender, - pub graphics_options: ListenableSource>, - pub paused: ListenableSource, - pub page_state: ListenableSource>, - pub mouselook_mode: ListenableSource, - pub fullscreen_mode: ListenableSource, + pub graphics_options: listen::DynSource>, + pub paused: listen::DynSource, + pub page_state: listen::DynSource>, + pub mouselook_mode: listen::DynSource, + pub fullscreen_mode: listen::DynSource, pub set_fullscreen: FullscreenSetter, pub(crate) quit: Option, } @@ -46,7 +46,7 @@ impl fmt::Debug for HudInputs { pub(super) fn new_hud_page( // TODO: mess of tightly coupled parameters - character_source: ListenableSource>>, + character_source: listen::DynSource>>, hud_inputs: &HudInputs, // TODO: stop mutating the universe in widget construction universe: &mut Universe, diff --git a/all-is-cubes-ui/src/ui_content/notification.rs b/all-is-cubes-ui/src/ui_content/notification.rs index 98ec9531a..de104a443 100644 --- a/all-is-cubes-ui/src/ui_content/notification.rs +++ b/all-is-cubes-ui/src/ui_content/notification.rs @@ -153,8 +153,8 @@ impl Hub { notification } - // TODO: should be optional but ProgressBar isn't friendly to that. We really need a ListenableSource::map() - pub(crate) fn primary_progress(&self) -> listen::ListenableSource { + // TODO: should be optional but ProgressBar isn't friendly to that. We really need a listen::DynSource::map() + pub(crate) fn primary_progress(&self) -> listen::DynSource { self.primary_progress.as_source() } diff --git a/all-is-cubes-ui/src/ui_content/vui_manager.rs b/all-is-cubes-ui/src/ui_content/vui_manager.rs index 79b83aa82..37f49928f 100644 --- a/all-is-cubes-ui/src/ui_content/vui_manager.rs +++ b/all-is-cubes-ui/src/ui_content/vui_manager.rs @@ -7,7 +7,7 @@ use std::sync::Mutex; use all_is_cubes::arcstr::ArcStr; use all_is_cubes::character::{Character, Cursor}; use all_is_cubes::inv::{EphemeralOpaque, Tool, ToolError, ToolInput}; -use all_is_cubes::listen::{DirtyFlag, ListenableCell, ListenableSource, Notifier}; +use all_is_cubes::listen::{self, DirtyFlag, ListenableCell, Notifier}; use all_is_cubes::space::Space; use all_is_cubes::time; use all_is_cubes::transaction::{self, Transaction}; @@ -50,7 +50,7 @@ pub(crate) struct Vui { state: ListenableCell>, changed_viewport: DirtyFlag, - viewport_source: ListenableSource, + viewport_source: listen::DynSource, /// Size computed from `viewport_source` and compared with `PageInst`. last_ui_size: UiSize, hud_inputs: HudInputs, @@ -70,10 +70,10 @@ pub(crate) struct Vui { /// `Send + Sync`, unlike the `std` one. /// Our choice of `flume` in particular is just because our other crates use it. control_channel: flume::Receiver, - character_source: ListenableSource>>, + character_source: listen::DynSource>>, changed_character: DirtyFlag, tooltip_state: Arc>, - /// Messages from session to UI that don't fit as [`ListenableSource`] changes. + /// Messages from session to UI that don't fit as [`listen::DynSource`] changes. cue_channel: CueNotifier, notif_hub: notification::Hub, @@ -91,12 +91,12 @@ impl Vui { #[expect(clippy::too_many_arguments)] pub(crate) async fn new( input_processor: &InputProcessor, - character_source: ListenableSource>>, - paused: ListenableSource, - graphics_options: ListenableSource>, + character_source: listen::DynSource>>, + paused: listen::DynSource, + graphics_options: listen::DynSource>, app_control_channel: flume::Sender, - viewport_source: ListenableSource, - fullscreen_source: ListenableSource, + viewport_source: listen::DynSource, + fullscreen_source: listen::DynSource, set_fullscreen: FullscreenSetter, quit: Option, ) -> Self { @@ -183,7 +183,7 @@ impl Vui { /// The space that should be displayed to the user, drawn on top of the world. // TODO: It'd be more encapsulating if we could provide a _read-only_ Handle... - pub fn view(&self) -> ListenableSource> { + pub fn view(&self) -> listen::DynSource> { self.current_view.as_source() } @@ -561,7 +561,7 @@ pub(crate) enum VuiMessage { } /// Channel for broadcasting, from session to widgets, various user interface responses -/// to events (that don't fit into the [`ListenableSource`] model). +/// to events (that don't fit into the [`listen::DynSource`] model). /// /// TODO: This `Arc` is a kludge; probably Notifier should have some kind of clonable /// add-a-listener handle to itself, and that would help out other situations too. @@ -585,12 +585,12 @@ mod tests { let (cctx, ccrx) = flume::bounded(1); let vui = Vui::new( &InputProcessor::new(), - ListenableSource::constant(None), - ListenableSource::constant(paused), - ListenableSource::constant(Arc::new(GraphicsOptions::default())), + listen::constant(None), + listen::constant(paused), + listen::constant(Arc::new(GraphicsOptions::default())), cctx, - ListenableSource::constant(Viewport::ARBITRARY), - ListenableSource::constant(None), + listen::constant(Viewport::ARBITRARY), + listen::constant(None), None, None, ) diff --git a/all-is-cubes-ui/src/vui/widgets/button.rs b/all-is-cubes-ui/src/vui/widgets/button.rs index 5c020c3c2..e06f3a4c4 100644 --- a/all-is-cubes-ui/src/vui/widgets/button.rs +++ b/all-is-cubes-ui/src/vui/widgets/button.rs @@ -32,7 +32,7 @@ use all_is_cubes::drawing::{DrawingPlane, VoxelBrush}; use all_is_cubes::euclid::vec3; use all_is_cubes::inv::EphemeralOpaque; use all_is_cubes::linking::{self, InGenError}; -use all_is_cubes::listen::{DirtyFlag, ListenableSource}; +use all_is_cubes::listen::{self, DirtyFlag}; use all_is_cubes::math::{ Cube, Face6, GridAab, GridCoordinate, GridSize, GridVector, Gridgid, Rgba, }; @@ -299,11 +299,11 @@ impl vui::WidgetController for ActionButtonController { } /// A single-block button that displays a boolean state derived from a -/// [`ListenableSource`] and can be clicked. +/// [`listen::DynSource`] and can be clicked. #[derive(Clone)] pub struct ToggleButton { common: ButtonCommon, - data_source: ListenableSource, + data_source: listen::DynSource, projection: Arc bool + Send + Sync>, action: Action, } @@ -328,7 +328,7 @@ impl fmt::Debug for ToggleButton { impl ToggleButton { #[allow(missing_docs)] pub fn new( - data_source: ListenableSource, + data_source: listen::DynSource, projection: impl Fn(&D) -> bool + Send + Sync + 'static, label: impl Into, theme: &WidgetTheme, @@ -352,7 +352,7 @@ impl vui::Layoutable for ToggleButton { } } -// TODO: Mess of generic bounds due to the combination of Widget and ListenableSource +// TODO: Mess of generic bounds due to the combination of Widget and listen::DynSource // requirements -- should we make a trait alias for these? impl vui::Widget for ToggleButton { fn controller(self: Arc, grant: &vui::LayoutGrant) -> Box { diff --git a/all-is-cubes-ui/src/vui/widgets/crosshair.rs b/all-is-cubes-ui/src/vui/widgets/crosshair.rs index e905263f9..10375ef44 100644 --- a/all-is-cubes-ui/src/vui/widgets/crosshair.rs +++ b/all-is-cubes-ui/src/vui/widgets/crosshair.rs @@ -3,7 +3,7 @@ use alloc::sync::Arc; use all_is_cubes::block::{Block, AIR}; use all_is_cubes::euclid::size3; -use all_is_cubes::listen::{DirtyFlag, ListenableSource}; +use all_is_cubes::listen::{self, DirtyFlag}; use all_is_cubes::space::SpaceTransaction; use crate::vui; @@ -11,11 +11,11 @@ use crate::vui; #[derive(Debug)] pub(crate) struct Crosshair { icon: Block, - mouselook_mode: ListenableSource, + mouselook_mode: listen::DynSource, } impl Crosshair { - pub fn new(icon: Block, mouselook_mode: ListenableSource) -> Arc { + pub fn new(icon: Block, mouselook_mode: listen::DynSource) -> Arc { Arc::new(Self { icon, mouselook_mode, diff --git a/all-is-cubes-ui/src/vui/widgets/progress_bar.rs b/all-is-cubes-ui/src/vui/widgets/progress_bar.rs index c97d15df1..7fa18e6fb 100644 --- a/all-is-cubes-ui/src/vui/widgets/progress_bar.rs +++ b/all-is-cubes-ui/src/vui/widgets/progress_bar.rs @@ -13,7 +13,7 @@ use num_traits::float::FloatCore as _; use all_is_cubes::block::{self, Block, Composite, CompositeOperator, AIR}; use all_is_cubes::color_block; -use all_is_cubes::listen::{DirtyFlag, ListenableSource}; +use all_is_cubes::listen::{self, DirtyFlag}; use all_is_cubes::math::{Face6, GridAab, GridCoordinate, GridSize, Rgb, ZeroOne}; use all_is_cubes::space::SpaceTransaction; @@ -26,7 +26,7 @@ pub struct ProgressBar { empty_style: BoxStyle, filled_style: BoxStyle, direction: Face6, - source: ListenableSource, + source: listen::DynSource, } /// Information presented by a [`ProgressBar`] widget. @@ -52,7 +52,7 @@ impl ProgressBar { pub fn new( theme: &WidgetTheme, direction: Face6, - source: ListenableSource, + source: listen::DynSource, ) -> Arc { Arc::new(Self { empty_style: theme.progress_bar_empty.clone(), @@ -234,7 +234,7 @@ mod tests { let tree = vui::leaf_widget(ProgressBar::new( &widget_theme, Face6::PX, - ListenableSource::constant(ProgressBarState::new(fraction)), + listen::constant(ProgressBarState::new(fraction)), )); let mut space = space::Builder::default() diff --git a/all-is-cubes-ui/src/vui/widgets/toolbar.rs b/all-is-cubes-ui/src/vui/widgets/toolbar.rs index 1e3a04360..f41c79f2b 100644 --- a/all-is-cubes-ui/src/vui/widgets/toolbar.rs +++ b/all-is-cubes-ui/src/vui/widgets/toolbar.rs @@ -43,7 +43,7 @@ impl Toolbar { const TOOLBAR_STEP: GridCoordinate = 2; pub fn new( - character_source: listen::ListenableSource>>, + character_source: listen::DynSource>>, // TODO: Take WidgetTheme instead of HudBlocks, or move this widget out of the widgets module. hud_blocks: Arc, slot_count: u16, @@ -281,7 +281,7 @@ impl WidgetController for ToolbarController { let mut pressed_buttons: [bool; TOOL_SELECTIONS] = [false; TOOL_SELECTIONS]; let mut should_update_pointers = false; { - let todo = &mut self.todo_more.lock().unwrap(); + let todo = &mut self.todo_more.lock(); for (i, t) in todo.button_pressed_decay.iter_mut().enumerate() { if *t != Duration::ZERO { // include a final goes-to-zero update diff --git a/all-is-cubes-wasm/Cargo.lock b/all-is-cubes-wasm/Cargo.lock index 593152a77..a91a77f7a 100644 --- a/all-is-cubes-wasm/Cargo.lock +++ b/all-is-cubes-wasm/Cargo.lock @@ -51,6 +51,7 @@ dependencies = [ "log", "manyfmt", "mutants", + "nosy", "num-traits", "ordered-float", "paste", @@ -1192,6 +1193,18 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nosy" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb59fd933a0061e11498ce4eebcb47a2b8baa8f22b163c5d21ea4d090cdd1cb2" +dependencies = [ + "arrayvec", + "cfg-if", + "manyfmt", + "mutants", +] + [[package]] name = "num-traits" version = "0.2.19" diff --git a/all-is-cubes/Cargo.toml b/all-is-cubes/Cargo.toml index 91776379d..6e294aca0 100644 --- a/all-is-cubes/Cargo.toml +++ b/all-is-cubes/Cargo.toml @@ -54,6 +54,7 @@ default = ["std"] std = [ "all-is-cubes-base/std", "arcstr/std", # not required but nicer behavior + "nosy/sync", "yield-progress/sync", ] # Adds `impl arbitrary::Arbitrary for ...` @@ -111,6 +112,7 @@ itertools = { workspace = true } log = { workspace = true } manyfmt = { workspace = true } mutants = { workspace = true } +nosy = { workspace = true } num-traits = { workspace = true } ordered-float = { workspace = true } paste = { workspace = true } diff --git a/all-is-cubes/benches/space.rs b/all-is-cubes/benches/space.rs index 49c213d8b..fadbe42e7 100644 --- a/all-is-cubes/benches/space.rs +++ b/all-is-cubes/benches/space.rs @@ -9,7 +9,7 @@ use criterion::{ use all_is_cubes::block; use all_is_cubes::camera::GraphicsOptions; use all_is_cubes::content::make_some_blocks; -use all_is_cubes::listen::ListenableSource; +use all_is_cubes::listen; use all_is_cubes::math::{GridAab, GridCoordinate, GridPoint, GridSize}; use all_is_cubes::raytracer::UpdatingSpaceRaytracer; use all_is_cubes::space::{CubeTransaction, Space, SpaceTransaction}; @@ -62,8 +62,8 @@ fn space_bulk_mutation(c: &mut Criterion) { let rts = std::array::from_fn(|_| { UpdatingSpaceRaytracer::new( space.clone(), - ListenableSource::constant(Arc::new(GraphicsOptions::default())), - ListenableSource::constant(Arc::new(())), + listen::constant(Arc::new(GraphicsOptions::default())), + listen::constant(Arc::new(())), ) }); // 2. Block definitions diff --git a/all-is-cubes/src/block.rs b/all-is-cubes/src/block.rs index 05a909b8d..387340b84 100644 --- a/all-is-cubes/src/block.rs +++ b/all-is-cubes/src/block.rs @@ -11,7 +11,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::fmt; -use crate::listen::{Listen as _, Listener}; +use crate::listen::{self, Listen as _, Listener}; use crate::math::{GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, Rgb, Rgba, Vol}; use crate::space::{SetCubeError, Space, SpaceChange}; use crate::universe::{Handle, HandleVisitor, VisitHandles}; @@ -607,11 +607,11 @@ impl Block { /// incompletely or not at all. It should not be relied on. pub fn evaluate_and_listen( &self, - listener: impl Listener + 'static, + listener: impl listen::IntoDynListener>, ) -> Result { self.evaluate2(&EvalFilter { skip_eval: false, - listener: Some(listener.erased()), + listener: Some(listener.into_dyn_listener()), budget: Default::default(), }) } diff --git a/all-is-cubes/src/block/block_def.rs b/all-is-cubes/src/block/block_def.rs index 5bce83107..bf32223e3 100644 --- a/all-is-cubes/src/block/block_def.rs +++ b/all-is-cubes/src/block/block_def.rs @@ -7,7 +7,7 @@ use alloc::sync::Arc; use core::{fmt, mem, ops}; use crate::block::{self, Block, BlockChange, EvalBlockError, InEvalError, MinEval}; -use crate::listen::{self, Gate, Listen, Listener, Notifier}; +use crate::listen::{self, Gate, IntoDynListener as _, Listener, Notifier}; use crate::transaction::{self, Equal, Transaction}; use crate::universe::{HandleVisitor, VisitHandles}; @@ -116,7 +116,7 @@ impl BlockDef { } = filter; if let Some(listener) = listener { - ::listen(self, listener.clone()); + ::listen(self, listener.clone()); } if skip_eval { @@ -148,7 +148,7 @@ impl BlockDefState { let cache = block .evaluate2(&block::EvalFilter { skip_eval: false, - listener: Some(block_listener.erased()), + listener: Some(block_listener.into_dyn_listener()), budget: Default::default(), }) .map(MinEval::from); @@ -233,10 +233,11 @@ impl fmt::Debug for BlockDef { /// Registers a listener for whenever the result of evaluation of this block definition changes. /// Note that this only occurs when the owning [`Universe`] is being stepped. -impl Listen for BlockDef { +impl listen::Listen for BlockDef { type Msg = BlockChange; + type Listener = as listen::Listen>::Listener; - fn listen_raw(&self, listener: listen::DynListener) { + fn listen_raw(&self, listener: Self::Listener) { self.notifier.listen_raw(listener) } } diff --git a/all-is-cubes/src/block/tests.rs b/all-is-cubes/src/block/tests.rs index a9ed233be..8251dedf9 100644 --- a/all-is-cubes/src/block/tests.rs +++ b/all-is-cubes/src/block/tests.rs @@ -24,12 +24,12 @@ use crate::universe::{HandleError, Name, Universe}; /// changes only with notification. fn listen( block: &Block, - listener: impl listen::Listener + 'static, + listener: impl listen::IntoDynListener>, ) -> Result<(), EvalBlockError> { block .evaluate2(&block::EvalFilter { skip_eval: true, - listener: Some(listener.erased()), + listener: Some(listener.into_dyn_listener()), budget: Default::default(), }) .map(|_| ()) diff --git a/all-is-cubes/src/character.rs b/all-is-cubes/src/character.rs index d52e73fdb..039278f29 100644 --- a/all-is-cubes/src/character.rs +++ b/all-is-cubes/src/character.rs @@ -17,7 +17,7 @@ use num_traits::float::Float as _; use crate::behavior::{self, Behavior, BehaviorSet, BehaviorSetTransaction}; use crate::camera::ViewTransform; use crate::inv::{self, Inventory, InventoryTransaction, Slot, Tool}; -use crate::listen::{self, Listen, Notifier}; +use crate::listen; use crate::math::{Aab, Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector}; use crate::physics; use crate::physics::{Body, BodyStepInfo, BodyTransaction, Contact, Velocity}; @@ -89,7 +89,7 @@ pub struct Character { selected_slots: [usize; inv::TOOL_SELECTIONS], /// Notifier for modifications. - notifier: Notifier, + notifier: listen::Notifier, // TODO: not crate access: we need something like the listen() method for Notifier pub(crate) behaviors: BehaviorSet, @@ -219,7 +219,7 @@ impl Character { exposure: exposure::State::default(), inventory: Inventory::from_slots(inventory), selected_slots, - notifier: Notifier::new(), + notifier: listen::Notifier::new(), behaviors: BehaviorSet::new(), #[cfg(feature = "rerun")] @@ -522,10 +522,11 @@ impl VisitHandles for Character { } /// Registers a listener for mutations of this character. -impl Listen for Character { +impl listen::Listen for Character { type Msg = CharacterChange; - fn listen_raw(&self, listener: listen::DynListener) { - self.notifier.listen(listener) + type Listener = as listen::Listen>::Listener; + fn listen_raw(&self, listener: Self::Listener) { + self.notifier.listen_raw(listener) } } @@ -597,7 +598,7 @@ impl<'de> serde::Deserialize<'de> for Character { behaviors: behaviors.into_owned(), // Not persisted - run-time connections to other things - notifier: Notifier::new(), + notifier: listen::Notifier::new(), velocity_input: Vector3D::zero(), #[cfg(feature = "rerun")] rerun_destination: Default::default(), diff --git a/all-is-cubes/src/listen.rs b/all-is-cubes/src/listen.rs index 629e0d3a4..469384255 100644 --- a/all-is-cubes/src/listen.rs +++ b/all-is-cubes/src/listen.rs @@ -1,255 +1,25 @@ //! Broadcasting of notifications of state changes, and other messages. //! -//! Objects which wish to send notifications use [`Notifier`]s, which manage a collection -//! of [`Listener`]s. Each listener reports when it is no longer needed and may be -//! discarded. -//! -//! When [`Notifier::notify`] is called to send a message, it is synchronously delivered -//! to all listeners; therefore, listeners are obligated to avoid making further -//! significant state changes. The typical pattern is for a listener to contain a -//! `Weak>` or similar multiply-owned mutable structure to aggregate incoming -//! messages, which will then be read and cleared by a later task. - -use alloc::sync::Arc; -use core::fmt; - -use crate::util::maybe_sync::SendSyncIfStd; - -mod cell; -pub use cell::*; +//! This module is a re-export of selected items from [`nosy`]. +//! Caution: if the `"std"` feature is disabled, they will change in non-additive ways. + +// TODO: Get rid of the renames. + +pub use ::nosy::{ + Buffer, Constant, Flag as DirtyFlag, Gate, GateListener, IntoDynListener, Listen, Listener, + NullListener, Sink, Source, Store, StoreLock, +}; + +#[cfg(feature = "std")] +pub use ::nosy::sync::{ + constant, Cell as ListenableCell, CellWithLocal as ListenableCellWithLocal, DynListener, + DynSource, Notifier, +}; +#[cfg(not(feature = "std"))] +pub use ::nosy::unsync::{ + constant, Cell as ListenableCell, CellWithLocal as ListenableCellWithLocal, DynListener, + DynSource, Notifier, +}; mod listeners; -pub use listeners::*; - -mod notifier; -pub use notifier::*; - -mod store; -pub use store::*; - -mod util; -pub use util::*; - -// ------------------------------------------------------------------------------------------------- - -/// Ability to subscribe to a source of messages, causing a [`Listener`] to receive them -/// as long as it wishes to. -pub trait Listen { - /// The type of message which may be obtained from this source. - /// - /// Most message types will satisfy `Copy + Send + Sync + 'static`, but this is not required. - type Msg; - - /// Subscribe the given [`Listener`] to this source of messages. - /// - /// Note that listeners are removed only via their returning [`false`] from - /// [`Listener::receive()`]; there is no operation to remove a listener, - /// nor are subscriptions deduplicated. - fn listen + 'static>(&self, listener: L) - where - Self: Sized, - { - self.listen_raw(listener.erased()) - } - - /// Subscribe the given [`Listener`] to this source of messages, - /// without automatic type conversion. - /// - /// Note that listeners are removed only via their returning [`false`] from - /// [`Listener::receive()`]; there is no operation to remove a listener, - /// nor are subscriptions deduplicated. - fn listen_raw(&self, listener: DynListener); -} - -impl Listen for &T { - type Msg = T::Msg; - - fn listen + 'static>(&self, listener: L) { - (**self).listen(listener) - } - fn listen_raw(&self, listener: DynListener) { - (**self).listen_raw(listener) - } -} -impl Listen for Arc { - type Msg = T::Msg; - - fn listen + 'static>(&self, listener: L) { - (**self).listen(listener) - } - fn listen_raw(&self, listener: DynListener) { - (**self).listen_raw(listener) - } -} - -// ------------------------------------------------------------------------------------------------- - -/// A receiver of messages (typically from something implementing [`Listen`]) which can -/// indicate when it is no longer interested in them (typically because the associated -/// recipient has been dropped). -/// -/// Listeners are typically used in trait object form, which may be created by calling -/// [`erased()`](Self::erased); this is done implicitly by [`Notifier`], but calling it -/// earlier may in some cases be useful to minimize the number of separately allocated -/// clones of the listener. -/// -/// Please note the requirements set out in [`Listener::receive()`]. -/// -/// Implementors must also implement [`Send`] and [`Sync`] if the `std` feature of -/// `all-is-cubes` is enabled. (This non-additive-feature behavior is unfortunately the -/// least bad option available.) -/// -/// Consider implementing [`Store`] and using [`StoreLock`] instead of implementing [`Listener`]. -/// [`StoreLock`] provides the weak reference and mutex that are needed in the most common -/// kind of use of [`Listener`]. -pub trait Listener: fmt::Debug + SendSyncIfStd { - /// Process and store the given series of messages. - /// - /// Returns `true` if the listener is still interested in further messages (“alive”), - /// and `false` if it should be dropped because these and all future messages would have - /// no observable effect. - /// A call of the form `.receive(&[])` may be performed to query aliveness without - /// delivering any messages. - /// - /// # Requirements on implementors - /// - /// Messages are provided in a batch for efficiency of dispatch. - /// Each message in the provided slice should be processed exactly the same as if - /// it were the only message provided. - /// If the slice is empty, there should be no observable effect. - /// - /// This method should not panic under any circumstances, in order to ensure the sender's - /// other work is not interfered with. - /// For example, if the listener accesses a poisoned mutex, it should do nothing or clear - /// the poison, rather than panicking. - /// - /// # Advice for implementors - /// - /// Note that, since this method takes `&Self`, a `Listener` must use interior - /// mutability of some variety to store the message. As a `Listener` may be called - /// from various contexts, and in particular while the sender is still performing - /// its work, that mutability should in general be limited to setting dirty flags - /// or inserting into message queues — not attempting to directly perform further - /// game state changes, and particularly not taking any locks that are not solely - /// used by the `Listener` and its destination, as that could result in deadlock. - /// - /// The typical pattern is for a listener to contain a `Weak>` or similar - /// multiply-owned mutable structure to aggregate incoming messages, which will - /// then be read and cleared by a later task; see [`FnListener`] for assistance in - /// implementing this pattern. - /// - /// Note that a [`Notifier`] might call `.receive(&[])` at any time, particularly when - /// listeners are added. Be careful not to cause a deadlock in this case; it may be - /// necessary to avoid locking in the case where there are no messages to be delivered. - fn receive(&self, messages: &[M]) -> bool; - - /// Convert this listener into trait object form, allowing it to be stored in - /// collections or passed non-generically. - /// - /// The purpose of this method over simply calling [`Arc::new()`] is that it will - /// avoid double-wrapping of a listener that's already in [`Arc`]. **Other - /// implementors should not override this.** - fn erased(self) -> DynListener - where - Self: Sized + 'static, - { - Arc::new(self) - } - - /// Apply a map/filter function (similar to [`Iterator::filter_map()`]) to incoming messages. - /// - /// Note: By default, this filter breaks up all message batching into batches of 1. - /// In order to avoid this and have more efficient message delivery, use - /// [`Filter::with_stack_buffer()`]. - /// This is unnecessary if `size_of::() == 0`; the buffer is automatically unbounded in - /// that case. - /// - /// TODO: Doc test - fn filter(self, function: F) -> Filter - where - Self: Sized, - F: for<'a> Fn(&'a MI) -> Option + Sync, - { - Filter { - function, - target: self, - } - } - - /// Wraps `self` to pass messages only until the returned [`Gate`], and any clones - /// of it, are dropped. - /// - /// This may be used to stop forwarding messages when a dependency no longer exists. - /// - /// ``` - /// use all_is_cubes::listen::{Listen, Listener, Gate, Sink}; - /// - /// let sink = Sink::new(); - /// let (gate, gated) = sink.listener().gate(); - /// gated.receive(&["kept1"]); - /// assert_eq!(sink.drain(), vec!["kept1"]); - /// gated.receive(&["kept2"]); - /// drop(gate); - /// gated.receive(&["discarded"]); - /// assert_eq!(sink.drain(), vec!["kept2"]); - /// ``` - fn gate(self) -> (Gate, GateListener) - where - Self: Sized, - { - Gate::new(self) - } -} - -/// Type-erased form of a [`Listener`] which accepts messages of type `M`. -pub type DynListener = Arc>; - -impl Listener for DynListener { - fn receive(&self, messages: &[M]) -> bool { - (**self).receive(messages) - } - - fn erased(self) -> DynListener { - self - } -} - -// ------------------------------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn erased_listener() { - let sink = Sink::new(); - let listener: DynListener<&str> = sink.listener().erased(); - - // Should not gain a new wrapper when erased() again. - assert_eq!( - Arc::as_ptr(&listener), - Arc::as_ptr(&listener.clone().erased()) - ); - - // Should report alive (and not infinitely recurse). - assert!(listener.receive(&[])); - - // Should deliver messages. - assert!(listener.receive(&["a"])); - assert_eq!(sink.drain(), vec!["a"]); - - // Should report dead - drop(sink); - assert!(!listener.receive(&[])); - assert!(!listener.receive(&["b"])); - } - - /// Demonstrate that [`DynListener`] implements [`fmt::Debug`]. - #[test] - fn dyn_listener_debug() { - let sink: Sink<&str> = Sink::new(); - let listener: DynListener<&str> = Arc::new(sink.listener()); - - assert_eq!(format!("{listener:?}"), "SinkListener { alive: true, .. }"); - } -} +pub use listeners::FnListener; diff --git a/all-is-cubes/src/listen/cell.rs b/all-is-cubes/src/listen/cell.rs deleted file mode 100644 index b89cff22d..000000000 --- a/all-is-cubes/src/listen/cell.rs +++ /dev/null @@ -1,340 +0,0 @@ -#![allow( - clippy::module_name_repetitions, - reason = "false positive; TODO: remove after Rust 1.84 is released" -)] - -use core::fmt; - -use alloc::sync::Arc; - -use crate::listen::{self, Listen, Notifier}; -use crate::util::maybe_sync::{Mutex, MutexGuard}; - -/// A interior-mutable container for a value which can notify that the value changed. -/// -/// Access to the value requires cloning it, so if the clone is not cheap, -/// consider wrapping the value with [`Arc`] to reduce the cost to reference count changes. -pub struct ListenableCell { - storage: Arc>, -} -/// Access to a value that might change (provided by a [`ListenableCell`]) or be [a -/// constant](ListenableSource::constant), and which can be listened to. -pub struct ListenableSource { - storage: Arc>, -} -struct ListenableCellStorage { - /// The current value. - cell: Mutex, - - /// Notifier to track listeners. - /// `None` if this is a constant cell. - /// - /// TODO: Add ability to diff the value and distribute that. - /// TODO: If the `ListenableCell` is dropped, clear this to denote that nothing will ever - /// be sent again. - notifier: Option>, -} - -impl ListenableCell { - /// Creates a new [`ListenableCell`] containing the given value. - pub fn new(value: T) -> Self { - Self { - storage: Arc::new(ListenableCellStorage { - cell: Mutex::new(value), - notifier: Some(Notifier::new()), - }), - } - } - - /// Returns a reference to the current value of the cell. - pub fn get(&self) -> T { - self.storage.cell.lock().unwrap().clone() - } - - /// Sets the contained value and sends out a change notification. - /// - /// Note that this does not test whether the current value is equal to avoid redundant - /// notifications. - /// - /// Caution: While listeners are *expected* not to have immediate side effects on - /// notification, this cannot be enforced. - pub fn set(&self, value: T) { - *self.storage.cell.lock().unwrap() = value; - self.storage - .notifier - .as_ref() - .expect("can't happen: set() on a constant cell") - .notify(&()); - } - - /// Sets the contained value to the given value iff they are unequal. - /// - /// This avoids sending change notifications in the case where - /// - /// Caution: This executes `PartialEq::eq()` with the lock held; this may delay readers of - /// the value, or cause permanent failure in the event of a panic. - #[doc(hidden)] // TODO: good public API? - pub fn set_if_unequal(&self, value: T) - where - T: PartialEq, - { - let mut guard: MutexGuard<'_, T> = self.storage.cell.lock().unwrap(); - if value == *guard { - return; - } - - *guard = value; - - // Don't hold the lock while notifying. - // Listeners shouldn't be trying to read immediately, but we don't want to create - // this deadlock opportunity regardless. - drop(guard); - - self.storage - .notifier - .as_ref() - .expect("can't happen: set() on a constant cell") - .notify(&()); - } - - /// Sets the contained value by modifying a clone of the old value using the provided - /// function. - /// - /// Note: this function is not atomic, in that other modifications can be made between - /// the time this function reads the current value and writes the new one. It is not any more - /// powerful than calling `get()` followed by `set()`. - pub fn update_mut(&self, f: F) { - let mut value = self.get(); - f(&mut value); - self.set(value); - } - - /// Returns a [`ListenableSource`] which provides read-only access to the value - /// managed by this cell. - pub fn as_source(&self) -> ListenableSource { - ListenableSource { - storage: self.storage.clone(), - } - } -} - -impl ListenableSource { - /// Creates a new [`ListenableSource`] containing the given value, which will - /// never change. - pub fn constant(value: T) -> Self { - Self { - storage: Arc::new(ListenableCellStorage { - cell: Mutex::new(value), - notifier: None, - }), - } - } - - /// Returns a clone of the current value of the cell. - pub fn get(&self) -> T { - T::clone(&*self.storage.cell.lock().unwrap()) - } -} - -impl Clone for ListenableSource { - fn clone(&self) -> Self { - Self { - storage: Arc::clone(&self.storage), - } - } -} - -impl Listen for ListenableSource { - type Msg = (); - - fn listen_raw(&self, listener: listen::DynListener) { - if let Some(notifier) = &self.storage.notifier { - notifier.listen_raw(listener); - } - } -} - -/// Convenience wrapper around [`ListenableCell`] which allows borrowing the current -/// value, at the cost of requiring `&mut` access to set it, and storing a clone. -#[doc(hidden)] // TODO: decide if good API -- currently used by all_is_cubes_gpu -pub struct ListenableCellWithLocal { - cell: ListenableCell, - value: T, -} - -impl ListenableCellWithLocal { - pub fn new(value: T) -> Self { - Self { - value: value.clone(), - cell: ListenableCell::new(value), - } - } - - pub fn set(&mut self, value: T) { - self.cell.set(value.clone()); - self.value = value; - } - - #[expect(clippy::should_implement_trait)] // TODO: consider renaming - pub fn borrow(&self) -> &T { - &self.value - } - - /// Returns a [`ListenableSource`] which provides read-only access to the value - /// managed by this cell. - pub fn as_source(&self) -> ListenableSource { - self.cell.as_source() - } -} - -impl fmt::Debug for ListenableCell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("ListenableCell"); - ds.field("value", &self.get()); - format_cell_metadata(&mut ds, &self.storage); - ds.finish() - } -} -impl fmt::Debug for ListenableSource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("ListenableSource"); - ds.field("value", &self.get()); - format_cell_metadata(&mut ds, &self.storage); - ds.finish() - } -} -impl fmt::Debug for ListenableCellWithLocal { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("ListenableCellWithLocal"); - ds.field("value", &self.value); - format_cell_metadata(&mut ds, &self.cell.storage); - ds.finish() - } -} - -// Pointer printing implementations to enable determining whether a cell and a source share -// state. Including the debug_struct to make it less ambiguous what role this pointer plays. -impl fmt::Pointer for ListenableCell { - /// Prints the address of the cell's state storage, which is shared with - /// [`ListenableSource`]s created from this cell. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("ListenableCell"); - ds.field("cell_address", &Arc::as_ptr(&self.storage)); - ds.finish() - } -} -impl fmt::Pointer for ListenableSource { - /// Prints the address of the state storage, which is shared with the originating - /// [`ListenableCell`] and other [`ListenableSource`]s. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("ListenableSource"); - ds.field("cell_address", &Arc::as_ptr(&self.storage)); - ds.finish() - } -} -impl fmt::Pointer for ListenableCellWithLocal { - /// Prints the address of the cell's state storage, which is shared with - /// [`ListenableSource`]s created from this cell. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("ListenableCellWithLocal"); - ds.field("cell_address", &Arc::as_ptr(&self.cell.storage)); - ds.finish() - } -} - -fn format_cell_metadata( - ds: &mut fmt::DebugStruct<'_, '_>, - storage: &Arc>, -) { - ds.field("owners", &Arc::strong_count(storage)); - if let Some(notifier) = &storage.notifier { - ds.field("listeners", ¬ifier.count()); - } else { - ds.field("constant", &true); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::listen::Sink; - use alloc::vec::Vec; - use pretty_assertions::assert_eq; - - #[test] - fn listenable_cell_and_source_debug() { - let cell = ListenableCell::>::new(vec!["hi"]); - let source = cell.as_source(); - assert_eq!( - format!("{cell:#?}"), - indoc::indoc! { - r#"ListenableCell { - value: [ - "hi", - ], - owners: 2, - listeners: 0, - }"# - } - ); - assert_eq!( - format!("{source:#?}"), - indoc::indoc! { - r#"ListenableSource { - value: [ - "hi", - ], - owners: 2, - listeners: 0, - }"# - } - ); - } - - #[test] - fn constant_source_debug() { - let source = ListenableSource::constant(vec!["hi"]); - assert_eq!( - format!("{source:#?}"), - indoc::indoc! { - r#"ListenableSource { - value: [ - "hi", - ], - owners: 1, - constant: true, - }"# - } - ); - } - - #[test] - fn listenable_cell_usage() { - let cell = ListenableCell::new(0); - - let s = cell.as_source(); - let sink = Sink::new(); - s.listen(sink.listener()); - - assert_eq!(sink.drain(), vec![]); - cell.set(1); - assert_eq!(1, s.get()); - assert_eq!(sink.drain(), vec![()]); - } - - #[test] - fn constant_source_usage() { - let s = ListenableSource::constant(123); - assert_eq!(s.get(), 123); - s.listen(Sink::new().listener()); // no panic - } - - #[test] - fn listenable_source_clone() { - let cell = ListenableCell::new(0); - let s = cell.as_source(); - let s = s.clone(); - cell.set(1); - assert_eq!(s.get(), 1); - } -} diff --git a/all-is-cubes/src/listen/listeners.rs b/all-is-cubes/src/listen/listeners.rs index d104a5629..8addad05b 100644 --- a/all-is-cubes/src/listen/listeners.rs +++ b/all-is-cubes/src/listen/listeners.rs @@ -1,43 +1,11 @@ -use alloc::collections::VecDeque; use alloc::sync::{Arc, Weak}; -use alloc::vec::Vec; use core::fmt; -use core::sync::atomic::{AtomicBool, Ordering}; use manyfmt::formats::Unquote; use manyfmt::Refmt as _; -use crate::listen::{Listen, Listener}; -use crate::util::maybe_sync::{RwLock, SendSyncIfStd}; - -// ------------------------------------------------------------------------------------------------- - -/// A [`Listener`] which discards all messages. -/// -/// Use this when a [`Listener`] is demanded, but there is nothing it should do. -#[expect(clippy::exhaustive_structs)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct NullListener; - -impl Listener for NullListener { - fn receive(&self, _messages: &[M]) -> bool { - false - } -} - -// ------------------------------------------------------------------------------------------------- - -/// Tuples of listeners may be used to distribute messages. -impl Listener for (L1, L2) -where - L1: Listener, - L2: Listener, -{ - fn receive(&self, messages: &[M]) -> bool { - // note non-short-circuiting or - self.0.receive(messages) | self.1.receive(messages) - } -} +use crate::listen::Listener; +use crate::util::maybe_sync::SendSyncIfStd; // ------------------------------------------------------------------------------------------------- @@ -90,173 +58,9 @@ where // ------------------------------------------------------------------------------------------------- -/// A [`Listener`] which stores all the messages it receives. -/// -/// This is only intended for testing. -#[derive(Debug)] -pub struct Sink { - messages: Arc>>, -} - -/// [`Sink::listener()`] implementation. -pub struct SinkListener { - weak_messages: Weak>>, -} - -impl Sink { - /// Constructs a new empty [`Sink`]. - pub fn new() -> Self { - Self { - messages: Arc::new(RwLock::new(VecDeque::new())), - } - } - - /// Returns a [`Listener`] which records the messages it receives in this Sink. - pub fn listener(&self) -> SinkListener { - SinkListener { - weak_messages: Arc::downgrade(&self.messages), - } - } - - /// Remove and return all messages returned so far. - /// - /// ``` - /// use all_is_cubes::listen::{Listener, Sink}; - /// - /// let sink = Sink::new(); - /// sink.listener().receive(&[1]); - /// sink.listener().receive(&[2]); - /// assert_eq!(sink.drain(), vec![1, 2]); - /// sink.listener().receive(&[3]); - /// assert_eq!(sink.drain(), vec![3]); - /// ``` - pub fn drain(&self) -> Vec { - self.messages.write().unwrap().drain(..).collect() - } -} - -impl fmt::Debug for SinkListener { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SinkListener") - // not useful to print weak_messages unless we were to upgrade and lock it - .field("alive", &(self.weak_messages.strong_count() > 0)) - .finish_non_exhaustive() - } -} - -impl Listener for SinkListener { - fn receive(&self, messages: &[M]) -> bool { - if let Some(cell) = self.weak_messages.upgrade() { - cell.write().unwrap().extend(messages.iter().cloned()); - true - } else { - false - } - } -} - -impl Clone for SinkListener { - fn clone(&self) -> Self { - Self { - weak_messages: self.weak_messages.clone(), - } - } -} - -impl Default for Sink { - // This implementation cannot be derived because we do not want M: Default - - fn default() -> Self { - Self::new() - } -} - -// ------------------------------------------------------------------------------------------------- - -/// A [`Listener`] destination which only stores a single flag indicating if any messages -/// were received. -pub struct DirtyFlag { - flag: Arc, -} - -impl fmt::Debug for DirtyFlag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // never multiline - write!(f, "DirtyFlag({:?})", self.flag.load(Ordering::Relaxed)) - } -} - -/// [`DirtyFlag::listener()`] implementation. -#[derive(Clone, Debug)] -pub struct DirtyFlagListener { - weak_flag: Weak, -} - -impl DirtyFlag { - /// Constructs a new [`DirtyFlag`] with the given initial value. - pub fn new(value: bool) -> Self { - Self { - flag: Arc::new(AtomicBool::new(value)), - } - } - - /// Constructs a new [`DirtyFlag`] with the given initial value and call - /// [`Listen::listen()`] with its listener. - /// - /// This is a convenience for calling `new()` followed by `listener()`. - pub fn listening(value: bool, source: impl Listen) -> Self { - let new_self = Self::new(value); - source.listen(new_self.listener()); - new_self - } - - /// Returns a [`Listener`] which will set this flag to [`true`] when it receives any - /// message. - pub fn listener(&self) -> DirtyFlagListener { - DirtyFlagListener { - weak_flag: Arc::downgrade(&self.flag), - } - } - - /// Returns the flag value, setting it to [`false`] at the same time. - pub fn get_and_clear(&self) -> bool { - self.flag.swap(false, Ordering::Acquire) - } - - /// Set the flag value to [`true`]. - /// - /// Usually a [`DirtyFlagListener`] is used instead of this, but it may be useful - /// in complex situations. - pub fn set(&self) { - self.flag.store(true, Ordering::Relaxed); - } -} -impl Listener for DirtyFlagListener { - fn receive(&self, messages: &[M]) -> bool { - if let Some(cell) = self.weak_flag.upgrade() { - if !messages.is_empty() { - cell.store(true, Ordering::Release); - } - true - } else { - false - } - } -} - -// ------------------------------------------------------------------------------------------------- - #[cfg(test)] mod tests { use super::*; - use crate::listen::Notifier; - - #[test] - fn null_alive() { - let notifier: Notifier<()> = Notifier::new(); - notifier.listen(NullListener); - assert_eq!(notifier.count(), 0); - } #[test] fn fn_debug() { @@ -271,46 +75,4 @@ mod tests { " } ); } - - #[test] - fn sink_alive() { - let notifier: Notifier<()> = Notifier::new(); - let sink = Sink::new(); - notifier.listen(sink.listener()); - assert_eq!(notifier.count(), 1); - drop(sink); - assert_eq!(notifier.count(), 0); - } - - #[test] - fn dirty_flag_alive() { - let notifier: Notifier<()> = Notifier::new(); - let flag = DirtyFlag::new(false); - notifier.listen(flag.listener()); - assert_eq!(notifier.count(), 1); - drop(flag); - assert_eq!(notifier.count(), 0); - } - - #[test] - fn dirty_flag_set() { - let flag = DirtyFlag::new(false); - - // not set by zero messages - flag.listener().receive(&[(); 0]); - assert!(!flag.get_and_clear()); - - // but set by receiving at least one message - flag.listener().receive(&[()]); - assert!(flag.get_and_clear()); - } - - #[test] - fn dirty_flag_debug() { - assert_eq!(format!("{:?}", DirtyFlag::new(false)), "DirtyFlag(false)"); - assert_eq!(format!("{:?}", DirtyFlag::new(true)), "DirtyFlag(true)"); - let dirtied = DirtyFlag::new(false); - dirtied.listener().receive(&[()]); - assert_eq!(format!("{dirtied:?}"), "DirtyFlag(true)"); - } } diff --git a/all-is-cubes/src/listen/notifier.rs b/all-is-cubes/src/listen/notifier.rs deleted file mode 100644 index 2d3970a94..000000000 --- a/all-is-cubes/src/listen/notifier.rs +++ /dev/null @@ -1,280 +0,0 @@ -#![allow( - clippy::module_name_repetitions, - reason = "false positive; TODO: remove after Rust 1.84 is released" -)] - -use alloc::sync::Weak; -use alloc::vec::Vec; -use core::fmt; -use core::sync::atomic::{AtomicBool, Ordering::Relaxed}; - -#[cfg(doc)] -use alloc::sync::Arc; - -use crate::listen::{DynListener, Listen, Listener}; -use crate::util::maybe_sync::RwLock; - -#[cfg(doc)] -use crate::listen::ListenableCell; - -// ------------------------------------------------------------------------------------------------- - -/// Message broadcaster, usually used for change notifications. -/// -/// A `Notifier` delivers messages of type `M` to a dynamic set of [`Listener`]s. -/// -/// The `Notifier` is usually owned by some entity which emits messages when it changes, -/// such as a [`ListenableCell`]. -/// Each `Listener` usually holds a weak reference to allow it to be removed when the -/// actual recipient is gone or uninterested. -/// -/// [`Listener`]s may be added using the [`Listen`] implementation, and are removed when -/// they report themselves as dead. -pub struct Notifier { - pub(crate) listeners: RwLock>>, -} - -pub(crate) struct NotifierEntry { - pub(crate) listener: DynListener, - /// True iff every call to `listener.receive()` has returned true. - pub(crate) was_alive: AtomicBool, -} - -impl Notifier { - /// Constructs a new empty [`Notifier`]. - pub fn new() -> Self { - Self { - listeners: Default::default(), - } - } - - /// Returns a [`Listener`] which forwards messages to the listeners registered with - /// this `Notifier`, provided that it is owned by an [`Arc`]. - /// - /// This may be used together with [`Listener::filter()`] to forward notifications - /// of changes in dependencies. Using this operation means that the dependent does not - /// need to fan out listener registrations to all of its current dependencies. - /// - /// ``` - /// use std::sync::Arc; - /// use all_is_cubes::listen::{Listen, Notifier, Sink}; - /// - /// let notifier_1 = Notifier::new(); - /// let notifier_2 = Arc::new(Notifier::new()); - /// let mut sink = Sink::new(); - /// notifier_1.listen(Notifier::forwarder(Arc::downgrade(¬ifier_2))); - /// notifier_2.listen(sink.listener()); - /// # assert_eq!(notifier_1.count(), 1); - /// # assert_eq!(notifier_2.count(), 1); - /// - /// notifier_1.notify(&"a"); - /// assert_eq!(sink.drain(), vec!["a"]); - /// drop(notifier_2); - /// notifier_1.notify(&"a"); - /// assert!(sink.drain().is_empty()); - /// - /// # assert_eq!(notifier_1.count(), 0); - /// ``` - pub fn forwarder(this: Weak) -> NotifierForwarder { - NotifierForwarder(this) - } - - /// Deliver a message to all [`Listener`]s. - pub fn notify(&self, message: &M) { - self.notify_many(core::slice::from_ref(message)) - } - - /// Deliver multiple messages to all [`Listener`]s. - pub fn notify_many(&self, messages: &[M]) { - for NotifierEntry { - listener, - was_alive, - } in self.listeners.read().unwrap().iter() - { - // Don't load was_alive before sending, because we assume the common case is that - // a listener implements receive() cheaply when it is dead. - let alive = listener.receive(messages); - - was_alive.fetch_and(alive, Relaxed); - } - } - - /// Creates a [`Buffer`] which batches messages sent through it. - /// This may be used as a more convenient interface to [`Notifier::notify_many()`], - /// at the cost of delaying messages until the buffer is dropped. - /// - /// The buffer does not use any heap allocations and will collect up to `CAPACITY` messages - /// per batch. - pub fn buffer(&self) -> Buffer<'_, M, CAPACITY> { - Buffer::new(self) - } - - /// Computes the exact count of listeners, including asking all current listeners - /// if they are alive. - /// - /// This operation is intended for testing and diagnostic purposes. - pub fn count(&self) -> usize { - let mut listeners = self.listeners.write().unwrap(); - Self::cleanup(&mut listeners); - listeners.len() - } - - /// Discard all dead weak pointers in `listeners`. - pub(crate) fn cleanup(listeners: &mut Vec>) { - let mut i = 0; - while i < listeners.len() { - let entry = &listeners[i]; - // We must ask the listener, not just consult was_alive, in order to avoid - // leaking memory if listen() is called repeatedly without any notify(). - // TODO: But we can skip it if the last operation was notify(). - if entry.was_alive.load(Relaxed) && entry.listener.receive(&[]) { - i += 1; - } else { - listeners.swap_remove(i); - } - } - } -} - -impl Listen for Notifier { - type Msg = M; - - fn listen_raw(&self, listener: DynListener) { - if !listener.receive(&[]) { - // skip adding it if it's already dead - return; - } - let mut listeners = self.listeners.write().unwrap(); - // TODO: consider amortization by not doing cleanup every time - Self::cleanup(&mut listeners); - listeners.push(NotifierEntry { - listener: listener.erased(), - was_alive: AtomicBool::new(true), - }); - } -} - -impl Default for Notifier { - fn default() -> Self { - Self::new() - } -} - -impl fmt::Debug for Notifier { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - // not using fmt.debug_tuple() so this is never printed on multiple lines - if let Ok(listeners) = self.listeners.try_read() { - write!(fmt, "Notifier({})", listeners.len()) - } else { - write!(fmt, "Notifier(?)") - } - } -} - -// ------------------------------------------------------------------------------------------------- - -/// A batch of messages of type `M` to be sent through a [`Notifier`]. -/// -/// Messages may be added to the buffer, and when the buffer is full or when it is dropped, -/// they are sent through the notifier. Creating such a batch is intended to increase performance -/// by not executing dynamic dispatch to every notifier for every message. -#[derive(Debug)] -pub struct Buffer<'notifier, M, const CAPACITY: usize> { - pub(crate) buffer: arrayvec::ArrayVec, - pub(crate) notifier: &'notifier Notifier, -} - -impl<'notifier, M, const CAPACITY: usize> Buffer<'notifier, M, CAPACITY> { - pub(crate) fn new(notifier: &'notifier Notifier) -> Self { - Self { - buffer: arrayvec::ArrayVec::new(), - notifier, - } - } - - /// Store a message in this buffer, to be delivered later as if by [`Notifier::notify()`]. - pub fn push(&mut self, message: M) { - // We don't need to check for fullness before pushing, because we always flush immediately - // if full. - self.buffer.push(message); - if self.buffer.is_full() { - self.flush(); - } - } - - #[cold] - pub(crate) fn flush(&mut self) { - self.notifier.notify_many(&self.buffer); - self.buffer.clear(); - } -} - -impl Drop for Buffer<'_, M, CAPACITY> { - fn drop(&mut self) { - // TODO: Should we discard messages if panicking? - // Currently leaning no, because we've specified that listeners should not panic even under - // error conditions such as poisoned mutexes. - if !self.buffer.is_empty() { - self.flush(); - } - } -} - -// ------------------------------------------------------------------------------------------------- - -/// A [`Listener`] which forwards messages through a [`Notifier`] to its listeners. -/// Constructed by [`Notifier::forwarder()`]. -pub struct NotifierForwarder(pub(super) Weak>); - -impl fmt::Debug for NotifierForwarder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("NotifierForwarder") - .field("alive(shallow)", &(self.0.strong_count() > 0)) - .finish_non_exhaustive() - } -} - -impl Listener for NotifierForwarder { - fn receive(&self, messages: &[M]) -> bool { - if let Some(notifier) = self.0.upgrade() { - notifier.notify_many(messages); - true - } else { - false - } - } -} - -impl Clone for NotifierForwarder { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -// ------------------------------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - use crate::listen::Sink; - - #[test] - fn notifier_basics_and_debug() { - let cn: Notifier = Notifier::new(); - assert_eq!(format!("{cn:?}"), "Notifier(0)"); - cn.notify(&0); - assert_eq!(format!("{cn:?}"), "Notifier(0)"); - let sink = Sink::new(); - cn.listen(sink.listener()); - assert_eq!(format!("{cn:?}"), "Notifier(1)"); - // type annotation to prevent spurious inference failures in the presence - // of other compiler errors - assert_eq!(sink.drain(), Vec::::new()); - cn.notify(&1); - cn.notify(&2); - assert_eq!(sink.drain(), vec![1, 2]); - assert_eq!(format!("{cn:?}"), "Notifier(1)"); - } - - // Test for NotifierForwarder exists as a doc-test. -} diff --git a/all-is-cubes/src/listen/store.rs b/all-is-cubes/src/listen/store.rs deleted file mode 100644 index 70a167ace..000000000 --- a/all-is-cubes/src/listen/store.rs +++ /dev/null @@ -1,298 +0,0 @@ -#![allow( - clippy::module_name_repetitions, - reason = "false positive; TODO: remove after Rust 1.84 is released" -)] - -use alloc::sync::{Arc, Weak}; -use core::fmt; - -use manyfmt::formats::Unquote; -use manyfmt::Refmt as _; - -use crate::listen::Listener; -use crate::util::maybe_sync; - -/// A data structure which records received messages for later processing. -/// -/// Its role is similar to [`Listener`], except that it is permitted and expected to mutate itself, -/// but **should not** communicate outside itself. A [`Listener`] is a sort of channel by which to -/// transmit messages; a `Store` is a data structure which is a destination for messages. -/// -/// After implementing `Store`, wrap it in a [`StoreLock`] to make use of it. -/// -/// Generally, a `Store` implementation will combine and de-duplicate messages in some -/// fashion. For example, if the incoming messages were notifications of modified regions of data -/// as rectangles, then one might define a `Store` owning an `Option` that contains the union -/// of all the rectangle messages, as an adequate constant-space approximation of the whole. -/// -/// The type parameter `M` is the type of messages to be received. -/// -/// TODO: give example -pub trait Store { - /// Record the given series of messages. - /// - /// # Requirements on implementors - /// - /// * Messages are provided in a batch for efficiency of dispatch. - /// Each message in the provided slice should be processed exactly the same as if - /// it were the only message provided. - /// If the slice is empty, there should be no observable effect. - /// - /// * This method should not panic under any possible incoming message stream, - /// in order to ensure the sender's other work is not interfered with. - /// - /// * This method should not attempt to acquire any locks, for performance and to avoid - /// deadlock with locks held by the sender. - /// (Normally, locking is to be provided separately, e.g. by [`StoreLock`].) - /// - /// * This method should not perform any blocking operation. - /// - /// # Advice for implementors - /// - /// Implementations should take care to be efficient, both in time taken and other - /// costs such as working set size. This method is typically called with a mutex held and the - /// original message sender blocking on it, so inefficiency here may have an effect on - /// distant parts of the application. - fn receive(&mut self, messages: &[M]); -} - -// ------------------------------------------------------------------------------------------------- - -/// Records messages delivered via [`Listener`] into a value of type `T` which implements -/// [`Store`]. -/// -/// This value is referred to as the “state”, and it is kept inside a mutex. -#[derive(Default)] -pub struct StoreLock(Arc>); - -/// [`StoreLock::listener()`] implementation. -/// -/// You should not usually need to use this type explicitly. -pub struct StoreLockListener(Weak>); - -impl fmt::Debug for StoreLock { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self(mutex) = self; - - // It is acceptable to lock the mutex for the same reasons it’s acceptable to lock it - // during `Listener::receive()`: because it should only be held for short periods - // and without taking any other locks. - let guard; - let state: &dyn fmt::Debug = match mutex.lock() { - Ok(g) => { - guard = g; - &&*guard - } - Err(maybe_sync::LockError::Poisoned { .. }) => &("").refmt(&Unquote), - }; - - f.debug_tuple("StoreLock").field(state).finish() - } -} - -impl fmt::Debug for StoreLockListener { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("StoreLockListener") - // The type name of T may give a useful clue about who this listener is for, - // without being too verbose or nondeterministic by printing the whole current state. - .field("type", &core::any::type_name::().refmt(&Unquote)) - // not useful to print weak_target unless we were to upgrade and lock it - .field("alive", &(self.0.strong_count() > 0)) - .finish() - } -} - -impl StoreLock { - /// Construct a new [`StoreLock`] with the given initial state. - pub fn new(initial_state: T) -> Self { - Self(Arc::new(maybe_sync::Mutex::new(initial_state))) - } -} - -impl StoreLock { - /// Returns a [`Listener`] which delivers messages to this. - pub fn listener(&self) -> StoreLockListener { - StoreLockListener(Arc::downgrade(&self.0)) - } - - /// Locks and returns access to the state. - /// - /// Callers should be careful to hold the lock for a very short time (e.g. only to copy or - /// [take](core::mem::take) the data) or to do so only while messages will not be arriving. - /// - /// # Errors - /// - /// This method may, but is not guaranteed to, return an error if it is called after a previous - /// operation with the lock held panicked. - /// - /// If it is called while the same thread has already acquired the lock, it may panic or hang. - pub fn lock(&self) -> Result + use<'_, T>, PoisonError> { - // TODO: make poison tracking guaranteed by using a RefCell-with-poison - self.0.lock().map_err(|_| PoisonError) - } - - /// Delivers messages like `self.listener().receive(messages)`, - /// but without creating a temporary listener. - pub fn receive(&self, messages: &[M]) - where - T: Store, - { - receive_bare_mutex(&self.0, messages); - } -} - -impl + Send> Listener for StoreLockListener { - fn receive(&self, messages: &[M]) -> bool { - let Some(strong) = self.0.upgrade() else { - return false; - }; - if messages.is_empty() { - // skip acquiring lock - return true; - } - receive_bare_mutex(&*strong, messages) - } -} - -fn receive_bare_mutex>( - mutex: &maybe_sync::Mutex, - messages: &[M], -) -> bool { - match mutex.lock() { - Ok(mut state) => { - state.receive(messages); - true - } - Err(maybe_sync::LockError::Poisoned { .. }) => { - // If the mutex is poisoned, then the state is corrupted and it is not useful - // to further modify it. The poisoning itself will communicate all there is to say. - false - } - } -} - -// TODO: Provide an alternative to `StoreLock` which doesn't hand out access to the mutex -// but only swaps. - -// ------------------------------------------------------------------------------------------------- - -/// Error from [`StoreLock::lock()`] when a previous operation panicked. -/// -/// This is similar to [`std::sync::PoisonError`], but does not allow bypassing the poison -/// indication. -#[allow(clippy::exhaustive_structs)] -#[derive(Clone, Copy, Debug, displaydoc::Display)] -#[displaydoc("a previous operation on this lock panicked")] -pub struct PoisonError; - -// ------------------------------------------------------------------------------------------------- - -/// This is a poor implementation of [`Store`] because it allocates unboundedly. -/// It should be used only for tests of message processing. -impl Store for alloc::vec::Vec { - fn receive(&mut self, messages: &[M]) { - self.extend_from_slice(messages); - } -} - -impl Store for alloc::collections::BTreeSet { - fn receive(&mut self, messages: &[M]) { - self.extend(messages.iter().cloned()); - } -} -#[cfg(feature = "std")] -impl Store - for std::collections::HashSet -{ - fn receive(&mut self, messages: &[M]) { - self.extend(messages.iter().cloned()); - } -} - -// ------------------------------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - use alloc::vec::Vec; - use core::mem; - - #[test] - fn store_lock_debug() { - let sl: StoreLock> = StoreLock::new(vec!["initial"]); - assert_eq!(format!("{sl:?}"), "StoreLock([\"initial\"])"); - } - - #[test] - fn store_lock_listener_debug() { - let sl: StoreLock> = StoreLock::new(vec!["initial"]); - let listener = sl.listener(); - assert_eq!( - format!("{listener:?}"), - "StoreLockListener { type: alloc::vec::Vec<&str>, alive: true }" - ); - drop(sl); - assert_eq!( - format!("{listener:?}"), - "StoreLockListener { type: alloc::vec::Vec<&str>, alive: false }" - ); - } - - #[test] - fn store_lock_basics() { - let sl: StoreLock> = StoreLock::new(vec!["initial"]); - let listener = sl.listener(); - - // Receive one message and see it added to the initial state - assert_eq!(listener.receive(&["foo"]), true); - assert_eq!(mem::take(&mut *sl.lock().unwrap()), vec!["initial", "foo"]); - - // Receive multiple messages in multiple batches - assert_eq!(listener.receive(&["bar", "baz"]), true); - assert_eq!(listener.receive(&["separate"]), true); - assert_eq!( - mem::take(&mut *sl.lock().unwrap()), - vec!["bar", "baz", "separate"] - ); - - // Receive after drop - drop(sl); - assert_eq!(listener.receive(&["too late"]), false); - } - - #[test] - fn store_lock_receive_inherent() { - let sl: StoreLock> = StoreLock::new(vec!["initial"]); - sl.receive(&["from inherent receive"]); - - assert_eq!( - mem::take(&mut *sl.lock().unwrap()), - vec!["initial", "from inherent receive"] - ); - } - - #[test] - fn store_lock_poisoned() { - let sl: StoreLock> = StoreLock::new(vec!["initial"]); - let listener = sl.listener(); - - // Poison the mutex by panicking inside it - // TODO: Get rid of this `AssertUnwindSafe` by making `StoreLock` *always* (rather than - // conditionally) `RefUnwindSafe` - let _ = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| { - let _guard = sl.lock(); - panic!("poison"); - })); - - // Listener does not panic, and returns false. - assert_eq!(listener.receive(&["foo"]), false); - - // Inherent receive does not panic, but does nothing. - sl.receive(&["bar"]); - - // Access to lock is poisoned - // TODO: This property is not guaranteed, but should be. - // - // assert!(sl.lock().is_err()); - } -} diff --git a/all-is-cubes/src/listen/util.rs b/all-is-cubes/src/listen/util.rs deleted file mode 100644 index a41365ee8..000000000 --- a/all-is-cubes/src/listen/util.rs +++ /dev/null @@ -1,276 +0,0 @@ -use alloc::sync::{Arc, Weak}; -use core::fmt; - -use manyfmt::formats::Unquote; -use manyfmt::Refmt as _; - -use crate::listen::Listener; - -/// A [`Listener`] which transforms or discards messages before passing them on. -/// Construct this using [`Listener::filter`]. -/// -/// This may be used to drop uninteresting messages or reduce their granularity. -/// -/// * `F` is the type of the filter function to use. -/// * `T` is the type of the listener to pass filtered messages to. -/// * `BATCH` is the maximum number of filtered messages to gather before passing them on. -/// It is used as the size of a stack-allocated array, so should be chosen with the size of -/// the message type in mind. -/// -/// TODO: add doc test -pub struct Filter { - /// The function to transform and possibly discard each message. - pub(super) function: F, - /// The recipient of the messages. - pub(super) target: T, -} - -impl Filter { - /// Request that the filter accumulate output messages into a batch. - /// - /// This causes each [receive](Listener::receive) operation to allocate an array `[MO; BATCH]` - /// on the stack, where `MO` is the output message type produced by `F`. - /// Therefore, the buffer size should be chosen keeping the size of `MO` in mind. - /// Also, the amount of buffer used cannot exceed the size of the input batch, - /// so it is not useful to choose a buffer size larger than the expected batch size. - /// - /// If `MO` is a zero-sized type, then the buffer is always unbounded, - /// so `with_stack_buffer()` has no effect and is unnecessary in that case. - pub fn with_stack_buffer(self) -> Filter { - Filter { - function: self.function, - target: self.target, - } - } -} - -impl fmt::Debug for Filter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Filter") - // function's type name may be the function name - .field("function", &core::any::type_name::().refmt(&Unquote)) - .field("target", &self.target) - .finish() - } -} - -impl Listener for Filter -where - F: Fn(&MI) -> Option + Send + Sync, - T: Listener, -{ - fn receive(&self, messages: &[MI]) -> bool { - if const { size_of::() == 0 } { - // If the size of the output message is zero, then we can buffer an arbitrary number - // of them without occupying any memory or performing any allocation, and therefore - // preserve the input batching for free. - let mut filtered_messages = alloc::vec::Vec::::new(); - for message in messages { - if let Some(filtered_message) = (self.function)(message) { - filtered_messages.push(filtered_message); - } - } - // Deliver entire batch of ZST messages. - self.target.receive(filtered_messages.as_slice()) - } else { - let mut buffer: arrayvec::ArrayVec = arrayvec::ArrayVec::new(); - for message in messages { - if let Some(filtered_message) = (self.function)(message) { - // Note that we do this fullness check before, not after, pushing a message, - // so if the buffer fills up exactly, we will use the receive() call after - // the end of the loop, not this one. - if buffer.is_full() { - let alive = self.target.receive(&buffer); - if !alive { - // Target doesn’t want any more messages, so we don’t need to filter - // them. - return false; - } - buffer.clear(); - } - buffer.push(filtered_message); - } - } - // Deliver final partial batch, if any, and final liveness check. - self.target.receive(&buffer) - } - } -} - -/// Breaks a [`Listener`] connection when dropped. -/// -/// Construct this using [`Listener::gate()`], or if a placeholder instance with no -/// effect is required, [`Gate::default()`]. Then, drop the [`Gate`] when no more messages -/// should be delivered. -#[derive(Clone, Default)] -pub struct Gate { - /// By owning this we keep its [`Weak`] peers alive, and thus the [`GateListener`] active. - _strong: Arc<()>, -} - -impl fmt::Debug for Gate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Gate") - } -} - -impl Gate { - pub(super) fn new(listener: L) -> (Gate, GateListener) { - let signaller = Arc::new(()); - let weak = Arc::downgrade(&signaller); - ( - Gate { _strong: signaller }, - GateListener { - weak, - target: listener, - }, - ) - } -} - -/// A [`Listener`] which forwards messages to another, -/// until the corresponding [`Gate`] is dropped. -/// -/// Construct this using [`Listener::gate()`]. -#[derive(Clone, Debug)] -pub struct GateListener { - weak: Weak<()>, - target: T, -} -impl Listener for GateListener -where - T: Listener, -{ - fn receive(&self, messages: &[M]) -> bool { - if self.weak.strong_count() > 0 { - self.target.receive(messages) - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::listen::{Listen as _, Notifier, Sink}; - use alloc::vec::Vec; - - /// Breaks the listener rules for testing by recording batch boundaries. - #[derive(Debug)] - struct CaptureBatch(L); - impl Listener for CaptureBatch - where - L: Listener>, - { - fn receive(&self, messages: &[M]) -> bool { - self.0.receive(&[Vec::from(messages)]) - } - } - - #[test] - fn filter_filtering_and_drop() { - let notifier: Notifier> = Notifier::new(); - let sink = Sink::new(); - notifier.listen(sink.listener().filter(|&x| x)); - assert_eq!(notifier.count(), 1); - - // Try delivering messages - notifier.notify(&Some(1)); - notifier.notify(&None); - assert_eq!(sink.drain(), vec![1]); - - // Drop the sink and the notifier should observe it gone - drop(sink); - assert_eq!(notifier.count(), 0); - } - - /// Test the behavior when `with_stack_buffer()` is not called, - /// leaving the buffer size implicitly at 1. - #[test] - fn filter_batch_size_1() { - let notifier: Notifier = Notifier::new(); - let sink: Sink> = Sink::new(); - notifier.listen(CaptureBatch(sink.listener()).filter(|&x: &i32| Some(x))); - - // Send some batches - notifier.notify_many(&[0, 1]); - notifier.notify_many(&[]); - notifier.notify_many(&[2, 3]); - - // Expect the batches to be of size at most 1 - assert_eq!( - sink.drain(), - vec![vec![], vec![0], vec![1], vec![], vec![2], vec![3]] - ); - } - - #[test] - fn filter_batching_nzst() { - let notifier: Notifier = Notifier::new(); - let sink: Sink> = Sink::new(); - notifier.listen( - CaptureBatch(sink.listener()) - .filter(|&x: &i32| Some(x)) - .with_stack_buffer::<2>(), - ); - - // Send some batches - notifier.notify_many(&[0, 1]); - notifier.notify_many(&[]); - notifier.notify_many(&[2, 3, 4]); - - // Expect the batches to be of size at most 2 - assert_eq!( - sink.drain(), - vec![vec![], vec![0, 1], vec![], vec![2, 3], vec![4]] - ); - } - - /// If the message value is a ZST, then batches are unbounded. - #[test] - fn filter_batching_zst() { - let notifier: Notifier = Notifier::new(); - let sink: Sink> = Sink::new(); - notifier.listen( - CaptureBatch(sink.listener()).filter(|&x: &i32| if x == 2 { None } else { Some(()) }), - ); - - // Send some batches - notifier.notify_many(&[0, 1]); - notifier.notify_many(&[]); - notifier.notify_many(&[2, 3, 4, 5]); - - // Expect batches to be preserved and filtered, even though we didn’t set a batch size. - assert_eq!( - sink.drain(), - vec![ - vec![], // initial liveness check on listen() - vec![(), ()], // first nonempty batch - vec![], // empty batch - vec![(), (), ()], // second nonempty batch, with 1 item dropped - ] - ); - } - - #[test] - fn gate() { - let notifier: Notifier = Notifier::new(); - let sink = Sink::new(); - let (gate, listener) = Gate::new(sink.listener()); - notifier.listen(listener); - assert_eq!(notifier.count(), 1); - - // Try delivering messages - notifier.notify(&1); - assert_eq!(sink.drain(), vec![1]); - - // Drop the gate and messages should stop passing immediately - // (even though we didn't even trigger notifier cleanup by calling count()) - drop(gate); - notifier.notify(&2); - assert_eq!(sink.drain(), Vec::::new()); - - assert_eq!(notifier.count(), 0); - } -} diff --git a/all-is-cubes/src/raytracer/updating.rs b/all-is-cubes/src/raytracer/updating.rs index ffaf6f600..9e48512f1 100644 --- a/all-is-cubes/src/raytracer/updating.rs +++ b/all-is-cubes/src/raytracer/updating.rs @@ -15,7 +15,7 @@ use hashbrown::HashSet as HbHashSet; use crate::block::AIR; use crate::camera::GraphicsOptions; use crate::content::palette; -use crate::listen::{self, Listen as _, ListenableSource, Listener}; +use crate::listen::{self, Listen as _, Listener}; use crate::math::Cube; use crate::raytracer::{RtBlockData, RtOptionsRef, SpaceRaytracer, TracingBlock, TracingCubeData}; use crate::space::{self, BlockIndex, Space, SpaceChange}; @@ -25,8 +25,8 @@ use crate::universe::{Handle, HandleError}; /// changed. pub struct UpdatingSpaceRaytracer { space: Handle, - graphics_options: ListenableSource>, - custom_options: ListenableSource>, + graphics_options: listen::DynSource>, + custom_options: listen::DynSource>, state: SpaceRaytracer, todo: listen::StoreLock, } @@ -58,8 +58,8 @@ where /// so the space is accessed on a consistent schedule.) pub fn new( space: Handle, - graphics_options: ListenableSource>, - custom_options: ListenableSource>, + graphics_options: listen::DynSource>, + custom_options: listen::DynSource>, ) -> Self { let todo = listen::StoreLock::new(SrtTodo { listener: true, @@ -115,7 +115,7 @@ where // so no deadlock can actually occur. If we change this to block on the space lock, // we must reorder the actions here (or perhaps acquire the todo lock twice) to // avoid deadlock. - let todo: &mut SrtTodo = &mut self.todo.lock().unwrap(); + let todo: &mut SrtTodo = &mut self.todo.lock(); if todo.is_empty() { // Nothing to do return Ok(false); @@ -241,8 +241,8 @@ mod tests { struct EquivalenceTester { camera: Camera, space: Handle, - graphics_options: ListenableSource>, - custom_options: ListenableSource>, + graphics_options: listen::DynSource>, + custom_options: listen::DynSource>, updating: UpdatingSpaceRaytracer, } @@ -251,8 +251,9 @@ mod tests { let bounds = space.read().unwrap().bounds(); // TODO: add tests of changing the options - let graphics_options = ListenableSource::constant(Arc::new(GraphicsOptions::default())); - let custom_options = ListenableSource::constant(Arc::new(())); + let graphics_options: listen::DynSource<_> = + listen::constant(Arc::new(GraphicsOptions::default())); + let custom_options: listen::DynSource<_> = listen::constant(Arc::new(())); let mut camera = Camera::new( (*graphics_options.get()).clone(), diff --git a/all-is-cubes/src/space.rs b/all-is-cubes/src/space.rs index ed0d09146..09acacc48 100644 --- a/all-is-cubes/src/space.rs +++ b/all-is-cubes/src/space.rs @@ -802,7 +802,9 @@ impl Space { } /// Returns the source of [fluff](Fluff) occurring in this space. - pub fn fluff(&self) -> impl Listen + '_ { + pub fn fluff( + &self, + ) -> impl Listen> + '_ { &self.fluff_notifier } @@ -1003,7 +1005,8 @@ impl VisitHandles for Space { /// Registers a listener for mutations of this space. impl Listen for Space { type Msg = SpaceChange; - fn listen_raw(&self, listener: listen::DynListener) { + type Listener = as Listen>::Listener; + fn listen_raw(&self, listener: Self::Listener) { self.change_notifier.listen_raw(listener) } } @@ -1424,7 +1427,8 @@ impl<'s> Extract<'s> { } // TODO: Tune this buffer size parameter, and validate it isn't overly large on the stack. -type ChangeBuffer<'notifier> = listen::Buffer<'notifier, SpaceChange, 16>; +type ChangeBuffer<'notifier> = + listen::Buffer<'notifier, SpaceChange, listen::DynListener, 16>; /// Argument passed to [`Space`] mutation methods that are used in bulk mutations. struct MutationCtx<'a, 'n> { diff --git a/all-is-cubes/src/space/palette.rs b/all-is-cubes/src/space/palette.rs index aec3c4ccf..1d9c1c112 100644 --- a/all-is-cubes/src/space/palette.rs +++ b/all-is-cubes/src/space/palette.rs @@ -11,7 +11,7 @@ use core::fmt; use itertools::Itertools as _; use crate::block::{self, Block, BlockChange, EvaluatedBlock, AIR, AIR_EVALUATED}; -use crate::listen::{self, Listener as _}; +use crate::listen::{self, IntoDynListener as _, Listener as _}; use crate::math::{self, OpacityCategory}; use crate::space::{BlockIndex, ChangeBuffer, SetCubeError, SpaceChange}; use crate::time::Instant; @@ -460,11 +460,18 @@ impl SpaceBlockData { } } - fn new(block: Block, listener: impl listen::Listener + 'static) -> Self { + fn new(block: Block, listener: L) -> Self + where + L: listen::Listener, + listen::GateListener: listen::IntoDynListener< + BlockChange, + as listen::Listen>::Listener, + >, + { // Note: Block evaluation also happens in `Space::step()`. let (gate, block_listener) = listener.gate(); - let block_listener = block_listener.erased(); + let block_listener: listen::DynListener = block_listener.into_dyn_listener(); let original_budget = block::Budget::default(); let filter = block::EvalFilter { diff --git a/all-is-cubes/src/universe/handle.rs b/all-is-cubes/src/universe/handle.rs index d6531e9b3..abe7e78c5 100644 --- a/all-is-cubes/src/universe/handle.rs +++ b/all-is-cubes/src/universe/handle.rs @@ -801,7 +801,7 @@ mod tests { block: Block { primitive: Atom { \ color: Rgba(1.0, 1.0, 1.0, 1.0), \ collision: Hard } }, \ - cache_dirty: DirtyFlag(false), \ + cache_dirty: Flag(false), \ listeners_ok: true, \ notifier: Notifier(0), .. })" ); @@ -815,7 +815,7 @@ mod tests { collision: Hard, }, }, - cache_dirty: DirtyFlag(false), + cache_dirty: Flag(false), listeners_ok: true, notifier: Notifier(0), .. diff --git a/all-is-cubes/src/universe/universe_txn.rs b/all-is-cubes/src/universe/universe_txn.rs index d16742b72..a360944e4 100644 --- a/all-is-cubes/src/universe/universe_txn.rs +++ b/all-is-cubes/src/universe/universe_txn.rs @@ -1053,7 +1053,7 @@ mod tests { block: Block { primitive: Air, }, - cache_dirty: DirtyFlag(false), + cache_dirty: Flag(false), listeners_ok: true, notifier: Notifier(0), .. diff --git a/all-is-cubes/src/util/maybe_sync.rs b/all-is-cubes/src/util/maybe_sync.rs index fca0fc182..a73818ec1 100644 --- a/all-is-cubes/src/util/maybe_sync.rs +++ b/all-is-cubes/src/util/maybe_sync.rs @@ -120,6 +120,10 @@ impl RwLock { } impl RwLock { + #[expect( + dead_code, + reason = "part of a complete wrapper, but happens to be unused" + )] pub fn read(&self) -> Result, LockError>> { cfg_if::cfg_if! { if #[cfg(feature = "std")] { @@ -133,6 +137,10 @@ impl RwLock { result.map(RwLockReadGuard) } + #[expect( + dead_code, + reason = "part of a complete wrapper, but happens to be unused" + )] pub fn write(&self) -> Result, LockError>> { cfg_if::cfg_if! { if #[cfg(feature = "std")] { diff --git a/test-renderers/src/render.rs b/test-renderers/src/render.rs index 45c7ba8bb..6377d7f9e 100644 --- a/test-renderers/src/render.rs +++ b/test-renderers/src/render.rs @@ -81,7 +81,7 @@ impl RendererFactory for RtFactory { Box::new(all_is_cubes_render::raytracer::RtRenderer::new( cameras, Box::new(|v| v), - listen::ListenableSource::constant(Arc::new(())), + listen::constant(Arc::new(())), )) } diff --git a/test-renderers/src/test_cases.rs b/test-renderers/src/test_cases.rs index f897e09cc..102303b84 100644 --- a/test-renderers/src/test_cases.rs +++ b/test-renderers/src/test_cases.rs @@ -13,7 +13,7 @@ use all_is_cubes::block::{Block, Resolution::*, AIR}; use all_is_cubes::character::{Character, Spawn}; use all_is_cubes::color_block; use all_is_cubes::euclid::{point3, size2, size3, vec2, vec3, Point2D, Size2D, Size3D, Vector3D}; -use all_is_cubes::listen::{ListenableCell, ListenableSource}; +use all_is_cubes::listen::{self, ListenableCell}; use all_is_cubes::math::{ ps32, rgb_const, rgba_const, zo32, Axis, Cube, Face6, FreeCoordinate, GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, Rgb, Rgba, Vol, @@ -450,10 +450,10 @@ async fn follow_character_change(context: RenderTestContext) { let c2 = character_of_a_color(rgb_const!(0.0, 1.0, 0.0)); let character_cell = ListenableCell::new(Some(c1)); let cameras: StandardCameras = StandardCameras::new( - ListenableSource::constant(Arc::new(GraphicsOptions::UNALTERED_COLORS)), - ListenableSource::constant(COMMON_VIEWPORT), + listen::constant(Arc::new(GraphicsOptions::UNALTERED_COLORS)), + listen::constant(COMMON_VIEWPORT), character_cell.as_source(), - ListenableSource::constant(Arc::new(UiViewState::default())), + listen::constant(Arc::new(UiViewState::default())), ); let mut renderer = context.renderer(cameras); @@ -507,9 +507,9 @@ async fn follow_options_change(mut context: RenderTestContext) { let options_cell = ListenableCell::new(Arc::new(options_1)); let cameras: StandardCameras = StandardCameras::new( options_cell.as_source(), - ListenableSource::constant(COMMON_VIEWPORT), - ListenableSource::constant(universe.get_default_character()), - ListenableSource::constant(Arc::new(UiViewState::default())), + listen::constant(COMMON_VIEWPORT), + listen::constant(universe.get_default_character()), + listen::constant(Arc::new(UiViewState::default())), ); // Render the image once. This isn't that interesting a comparison test, @@ -646,7 +646,7 @@ async fn icons(mut context: RenderTestContext) { ] .map(|(label_key, state)| { block_from_widget(&vui::leaf_widget(widgets::ToggleButton::new( - ListenableSource::constant(state), + listen::constant(state), |state| *state, ui_blocks_p[label_key].clone(), &widget_theme, @@ -730,10 +730,10 @@ async fn layers_all_show_ui(mut context: RenderTestContext, show_ui: bool) { options.lighting_display = LightingOption::Flat; options.show_ui = show_ui; let cameras: StandardCameras = StandardCameras::new( - ListenableSource::constant(Arc::new(options.clone())), - ListenableSource::constant(COMMON_VIEWPORT), - ListenableSource::constant(universe.get_default_character()), - ListenableSource::constant(Arc::new(UiViewState { + listen::constant(Arc::new(options.clone())), + listen::constant(COMMON_VIEWPORT), + listen::constant(universe.get_default_character()), + listen::constant(Arc::new(UiViewState { space: Some(ui_space(&mut universe)), view_transform: ViewTransform::identity(), graphics_options: options, @@ -777,10 +777,10 @@ async fn layers_none_but_text(mut context: RenderTestContext) { async fn layers_ui_only(mut context: RenderTestContext) { let mut universe = Universe::new(); let cameras: StandardCameras = StandardCameras::new( - ListenableSource::constant(Arc::new(GraphicsOptions::UNALTERED_COLORS)), - ListenableSource::constant(COMMON_VIEWPORT), - ListenableSource::constant(None), - ListenableSource::constant(Arc::new(UiViewState { + listen::constant(Arc::new(GraphicsOptions::UNALTERED_COLORS)), + listen::constant(COMMON_VIEWPORT), + listen::constant(None), + listen::constant(Arc::new(UiViewState { space: Some(ui_space(&mut universe)), view_transform: ViewTransform::identity(), graphics_options: GraphicsOptions::UNALTERED_COLORS, @@ -978,10 +978,10 @@ async fn viewport_zero(mut context: RenderTestContext) { let zero = Viewport::with_scale(1.00, [0, 0]); let viewport_cell = ListenableCell::new(zero); let cameras: StandardCameras = StandardCameras::new( - ListenableSource::constant(Arc::new(GraphicsOptions::default())), + listen::constant(Arc::new(GraphicsOptions::default())), viewport_cell.as_source(), - ListenableSource::constant(universe.get_default_character()), - ListenableSource::constant(Arc::new(UiViewState::default())), + listen::constant(universe.get_default_character()), + listen::constant(Arc::new(UiViewState::default())), ); let overlays = Overlays { cursor: None, diff --git a/test-renderers/tests/ui.rs b/test-renderers/tests/ui.rs index 99773b94b..f8c78aa9d 100644 --- a/test-renderers/tests/ui.rs +++ b/test-renderers/tests/ui.rs @@ -8,7 +8,7 @@ use clap::Parser as _; use all_is_cubes::arcstr::literal; use all_is_cubes::linking::BlockProvider; -use all_is_cubes::listen::{ListenableCell, ListenableSource}; +use all_is_cubes::listen::{self, ListenableCell}; use all_is_cubes::math::Face6; use all_is_cubes::time::NoTime; use all_is_cubes::transaction::Transaction as _; @@ -167,7 +167,7 @@ async fn widget_progress_bar(mut context: RenderTestContext) { // --- Test helpers ------------------------------------------------------------------------------- async fn create_session() -> Session { - let viewport = ListenableSource::constant(Viewport::with_scale(1.0, [256, 192])); + let viewport = listen::constant(Viewport::with_scale(1.0, [256, 192])); let start_time = Instant::now(); let session: Session = Session::builder().ui(viewport).build().await; log::trace!(