From eb09da609b227a0646c5430c6846d6878ddb3cda Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sun, 5 Jan 2025 09:03:04 -0800 Subject: [PATCH] ui: Add `Settings` type. This will be the platform-independent interface to persistent user settings. For now, it just wraps the existing `GraphicsOptions`. `ControlMessage::ModifyGraphicsOptions` is now `ModifySettings`, which is straightforwardly one fewer TODO! --- CHANGELOG.md | 13 +++ .../src/bin/all-is-cubes/main.rs | 9 +- all-is-cubes-desktop/src/record/rmain.rs | 6 +- all-is-cubes-ui/src/apps/input.rs | 39 ++++--- all-is-cubes-ui/src/apps/mod.rs | 3 + all-is-cubes-ui/src/apps/session.rs | 68 +++++++----- all-is-cubes-ui/src/apps/settings.rs | 102 ++++++++++++++++++ all-is-cubes-ui/src/ui_content/options.rs | 24 ++--- all-is-cubes-wasm/src/web_session.rs | 4 +- 9 files changed, 194 insertions(+), 74 deletions(-) create mode 100644 all-is-cubes-ui/src/apps/settings.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aee8dc5e..7a397e02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased + +### Added + +- `all-is-cubes-ui` library: + - `apps::Settings` manages user-editable settings that eventually will be more than just the graphics options. + - `apps::SessionBuilder::settings()` links a possibly-shared `Settings` to the created session. + +### Changed + +- `all-is-cubes-ui` library: + - `apps::Session::settings()` replaces `graphics_options_mut()`. + ## 0.9.0 (2025-01-01) ### Added diff --git a/all-is-cubes-desktop/src/bin/all-is-cubes/main.rs b/all-is-cubes-desktop/src/bin/all-is-cubes/main.rs index 3155663e1..549527b15 100644 --- a/all-is-cubes-desktop/src/bin/all-is-cubes/main.rs +++ b/all-is-cubes-desktop/src/bin/all-is-cubes/main.rs @@ -111,9 +111,7 @@ fn main() -> Result<(), anyhow::Error> { .build(), ); // TODO: this code should live in the lib - session - .graphics_options_mut() - .set(Arc::new(graphics_options)); + session.settings().set_graphics_options(graphics_options); universe_task.attach_to_session(&mut session); let session_done_time = Instant::now(); log::debug!( @@ -179,9 +177,8 @@ fn main() -> Result<(), anyhow::Error> { .context("failed to create session")?; if graphics_type == GraphicsType::WindowRt { // TODO: improve on this kludge by just having a general cmdline graphics config - dsession.session.graphics_options_mut().update_mut(|o| { - Arc::make_mut(o).render_method = - all_is_cubes_render::camera::RenderMethod::Reference; + dsession.session.settings().mutate_graphics_options(|o| { + o.render_method = all_is_cubes_render::camera::RenderMethod::Reference; }); } Ok(dsession) diff --git a/all-is-cubes-desktop/src/record/rmain.rs b/all-is-cubes-desktop/src/record/rmain.rs index 22717fd15..25990753f 100644 --- a/all-is-cubes-desktop/src/record/rmain.rs +++ b/all-is-cubes-desktop/src/record/rmain.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::sync::Arc; use std::time::Duration; use all_is_cubes::character::{self, Character}; @@ -37,9 +36,8 @@ where // override it if they do want to record the UI. dsession .session - .graphics_options_mut() - .update_mut(|graphics_options| { - let graphics_options = Arc::make_mut(graphics_options); + .settings() + .mutate_graphics_options(|graphics_options| { graphics_options.show_ui = false; graphics_options.debug_info_text = false; }); diff --git a/all-is-cubes-ui/src/apps/input.rs b/all-is-cubes-ui/src/apps/input.rs index bb3bb18c6..e01449df6 100644 --- a/all-is-cubes-ui/src/apps/input.rs +++ b/all-is-cubes-ui/src/apps/input.rs @@ -3,7 +3,6 @@ reason = "false positive; TODO: remove after Rust 1.84 is released" )] -use alloc::sync::Arc; use alloc::vec::Vec; use core::time::Duration; use std::collections::{HashMap, HashSet}; @@ -15,11 +14,10 @@ use all_is_cubes::math::{zo32, FreeCoordinate, FreeVector}; use all_is_cubes::time::Tick; use all_is_cubes::universe::{Handle, Universe}; use all_is_cubes_render::camera::{ - FogOption, GraphicsOptions, LightingOption, NdcPoint2, NominalPixel, RenderMethod, - TransparencyOption, Viewport, + FogOption, LightingOption, NdcPoint2, NominalPixel, RenderMethod, TransparencyOption, Viewport, }; -use crate::apps::ControlMessage; +use crate::apps::{ControlMessage, Settings}; type MousePoint = Point2D; @@ -295,7 +293,7 @@ impl InputProcessor { universe, character: character_opt, paused: paused_opt, - graphics_options, + settings, control_channel, ui, } = targets; @@ -349,10 +347,9 @@ impl InputProcessor { } } Key::Character('i') => { - if let Some(cell) = graphics_options { - cell.update_mut(|options| { - Arc::make_mut(options).lighting_display = match options.lighting_display - { + if let Some(settings) = settings { + settings.mutate_graphics_options(|options| { + options.lighting_display = match options.lighting_display { LightingOption::None => LightingOption::Flat, LightingOption::Flat => LightingOption::Smooth, LightingOption::Smooth => LightingOption::None, @@ -371,9 +368,9 @@ impl InputProcessor { } } Key::Character('o') => { - if let Some(cell) = graphics_options { - cell.update_mut(|options| { - Arc::make_mut(options).transparency = match options.transparency { + if let Some(settings) = settings { + settings.mutate_graphics_options(|options| { + options.transparency = match options.transparency { TransparencyOption::Surface => TransparencyOption::Volumetric, TransparencyOption::Volumetric => { TransparencyOption::Threshold(zo32(0.5)) @@ -391,9 +388,9 @@ impl InputProcessor { } } Key::Character('u') => { - if let Some(cell) = graphics_options { - cell.update_mut(|options| { - Arc::make_mut(options).fog = match options.fog { + if let Some(settings) = settings { + settings.mutate_graphics_options(|options| { + options.fog = match options.fog { FogOption::None => FogOption::Abrupt, FogOption::Abrupt => FogOption::Compromise, FogOption::Compromise => FogOption::Physical, @@ -404,9 +401,9 @@ impl InputProcessor { } } Key::Character('y') => { - if let Some(cell) = graphics_options { - cell.update_mut(|options| { - Arc::make_mut(options).render_method = match options.render_method { + if let Some(settings) = settings { + settings.mutate_graphics_options(|options| { + options.render_method = match options.render_method { RenderMethod::Mesh => RenderMethod::Reference, RenderMethod::Reference => RenderMethod::Mesh, _ => RenderMethod::Reference, @@ -492,7 +489,7 @@ pub(crate) struct InputTargets<'a> { pub universe: Option<&'a mut Universe>, pub character: Option<&'a Handle>, pub paused: Option<&'a listen::Cell>, - pub graphics_options: Option<&'a listen::Cell>>, + pub settings: Option<&'a Settings>, // TODO: replace cells with control channel? // TODO: make the control channel a type alias? pub control_channel: Option<&'a flume::Sender>, @@ -534,7 +531,7 @@ mod tests { universe: Some(universe), character: Some(character), paused: None, - graphics_options: None, + settings: None, control_channel: None, ui: None, }, @@ -583,7 +580,7 @@ mod tests { &InputProcessor::new(), listen::constant(None), paused.as_source(), - listen::constant(Arc::new(GraphicsOptions::default())), + listen::constant(Default::default()), cctx, listen::constant(Viewport::ARBITRARY), listen::constant(None), diff --git a/all-is-cubes-ui/src/apps/mod.rs b/all-is-cubes-ui/src/apps/mod.rs index 041532799..dbc8a5572 100644 --- a/all-is-cubes-ui/src/apps/mod.rs +++ b/all-is-cubes-ui/src/apps/mod.rs @@ -6,5 +6,8 @@ pub use input::*; mod session; pub use session::*; +mod settings; +pub use settings::*; + mod time; pub use time::*; diff --git a/all-is-cubes-ui/src/apps/session.rs b/all-is-cubes-ui/src/apps/session.rs index 43cc01c02..71f672d48 100644 --- a/all-is-cubes-ui/src/apps/session.rs +++ b/all-is-cubes-ui/src/apps/session.rs @@ -37,7 +37,7 @@ use all_is_cubes_render::camera::{ GraphicsOptions, Layers, StandardCameras, UiViewState, Viewport, }; -use crate::apps::{FpsCounter, FrameClock, InputProcessor, InputTargets}; +use crate::apps::{FpsCounter, FrameClock, InputProcessor, InputTargets, Settings}; use crate::ui_content::notification::{self, Notification}; use crate::ui_content::Vui; @@ -106,7 +106,7 @@ pub struct Session { /// might also be moved to a background task to allow the session stepping to occur independent /// of the event loop or other owner of the `Session`. struct Shuttle { - graphics_options: listen::Cell>, + settings: Settings, game_universe: Universe, @@ -159,7 +159,7 @@ impl fmt::Debug for Session { return Ok(()); }; let Shuttle { - graphics_options, + settings, game_universe, game_universe_info, game_character, @@ -175,7 +175,7 @@ impl fmt::Debug for Session { f.debug_struct("Session") .field("frame_clock", frame_clock) .field("input_processor", input_processor) - .field("graphics_options", graphics_options) + .field("settings", settings) .field("game_universe", game_universe) .field("game_universe_info", game_universe_info) .field("game_character", game_character) @@ -285,12 +285,14 @@ impl Session { /// Allows reading, and observing changes to, the current graphics options. pub fn graphics_options(&self) -> listen::DynSource> { - self.shuttle().graphics_options.as_source() + self.shuttle().settings.as_source() } - /// Allows setting the current graphics options. - pub fn graphics_options_mut(&self) -> &listen::Cell> { - &self.shuttle().graphics_options + /// Allows changing the settings associated with this session. + /// + /// Note that these settings may be shared with other sessions. + pub fn settings(&self) -> &Settings { + &self.shuttle().settings } /// Returns a [`StandardCameras`] which may be used in rendering a view of this session, @@ -340,7 +342,7 @@ impl Session { universe: Some(&mut shuttle.game_universe), character: shuttle.game_character.get().as_ref(), paused: Some(&self.paused), - graphics_options: Some(&shuttle.graphics_options), + settings: Some(&shuttle.settings), control_channel: Some(&self.control_channel_sender), ui: shuttle.ui.as_ref(), }, @@ -493,11 +495,8 @@ impl Session { ControlMessage::ToggleMouselook => { self.input_processor.toggle_mouselook_mode(); } - ControlMessage::ModifyGraphicsOptions(f) => { - let shuttle = self.shuttle(); - shuttle - .graphics_options - .set(f(shuttle.graphics_options.get())); + ControlMessage::ModifySettings(function) => { + function(&self.shuttle().settings); } }, Err(TryRecvError::Empty) => break, @@ -632,8 +631,8 @@ impl Session { let fopt = StatusText { show: self .shuttle() - .graphics_options - .get() + .settings + .get_graphics_options() .debug_info_text_contents, }; @@ -766,6 +765,8 @@ pub struct SessionBuilder { fullscreen_state: listen::DynSource, set_fullscreen: FullscreenSetter, + settings: Option, + quit: Option, _instant: PhantomData, @@ -777,6 +778,7 @@ impl Default for SessionBuilder { viewport_for_ui: None, fullscreen_state: listen::constant(None), set_fullscreen: None, + settings: None, quit: None, _instant: PhantomData, } @@ -794,13 +796,16 @@ impl SessionBuilder { viewport_for_ui, fullscreen_state, set_fullscreen, + settings, quit: quit_fn, _instant: _, } = self; + + let settings = settings.unwrap_or_else(|| Settings::new(Default::default())); + let game_universe = Universe::new(); let game_character = listen::CellWithLocal::new(None); let input_processor = InputProcessor::new(); - let graphics_options = listen::Cell::new(Arc::new(GraphicsOptions::default())); let paused = listen::Cell::new(false); let (control_send, control_recv) = flume::bounded(100); @@ -816,7 +821,7 @@ impl SessionBuilder { &input_processor, game_character.as_source(), paused.as_source(), - graphics_options.as_source(), + settings.as_source(), control_send.clone(), viewport, fullscreen_state, @@ -828,7 +833,7 @@ impl SessionBuilder { None => None, }, - graphics_options, + settings, game_character, game_universe_info: listen::Cell::new(SessionUniverseInfo { id: game_universe.universe_id(), @@ -880,6 +885,15 @@ impl SessionBuilder { self } + /// Enable reading and writing user settings. + /// + /// If this is not called, then the session will have all default settings, + /// and they will not be persisted. + pub fn settings(mut self, settings: Settings) -> Self { + self.settings = Some(settings); + self + } + /// Enable a “quit”/“exit” command in the session's user interface. /// /// This does not cause the session to self-destruct; rather, the provided callback @@ -915,8 +929,7 @@ pub(crate) enum ControlMessage { ToggleMouselook, - /// TODO: this should be "modify user preferences", from which graphics options are derived. - ModifyGraphicsOptions(Box) -> Arc + Send>), + ModifySettings(Box), } impl fmt::Debug for ControlMessage { @@ -929,9 +942,7 @@ impl fmt::Debug for ControlMessage { Self::EnterDebug => write!(f, "EnterDebug"), Self::TogglePause => write!(f, "TogglePause"), Self::ToggleMouselook => write!(f, "ToggleMouselook"), - Self::ModifyGraphicsOptions(_func) => f - .debug_struct("ModifyGraphicsOptions") - .finish_non_exhaustive(), + Self::ModifySettings(_func) => f.debug_struct("ModifySettings").finish_non_exhaustive(), } } } @@ -1124,7 +1135,7 @@ impl MainTaskContext { pub fn create_cameras(&self, viewport_source: listen::DynSource) -> StandardCameras { self.with_ref(|shuttle| { StandardCameras::new( - shuttle.graphics_options.as_source(), + shuttle.settings.as_source(), viewport_source, shuttle.game_character.as_source(), shuttle.ui_view(), @@ -1148,6 +1159,13 @@ impl MainTaskContext { }) } + /// Allows reading or changing the settings of this session. + /// + /// Note that these settings may be shared with other sessions. + pub fn settings(&self) -> Settings { + self.with_ref(|shuttle| shuttle.settings.clone()) + } + /// Invoke the [`SessionBuilder::quit()`] callback as if the user clicked a quit button inside /// our UI. /// diff --git a/all-is-cubes-ui/src/apps/settings.rs b/all-is-cubes-ui/src/apps/settings.rs new file mode 100644 index 000000000..f48b63ebb --- /dev/null +++ b/all-is-cubes-ui/src/apps/settings.rs @@ -0,0 +1,102 @@ +use alloc::sync::Arc; +use core::fmt; + +use all_is_cubes::listen; +use all_is_cubes_render::camera::GraphicsOptions; + +/// Currently, the settings data is *only* graphics options. +/// We want to add more settings (e.g. keybindings, startup behavior options, etc), +/// and not use `GraphicsOptions`'s exact serialization, but that will come later. +type Data = Arc; + +/// User-facing interactively editable and persisted settings for All is Cubes sessions. +/// +/// Settings are user-visible configuration that is not specific to a particular session +/// or universe; for example, graphics options and key bindings. +/// +/// This is separate from [`Session`](super::Session) so that it can be shared among +/// multiple sessions without conflict. +/// All such sessions will edit the same settings. +// TODO: Add settings inheritance for session-specific settings. +/// +/// Having `&` access to a [`Settings`] grants permission to read the settings, follow +/// changes to the settings, and write the settings. +/// Read-only access may be obtained as [`Settings::as_source()`]. +/// Cloning a [`Settings`] produces a clone which shares the same state. +#[derive(Clone)] +pub struct Settings(Arc); + +struct Inner { + data: listen::Cell, + persister: Arc, +} + +impl Settings { + /// Creates a new [`Settings`] with the given initial state, and no persistence. + pub fn new(initial_state: Data) -> Self { + Self::with_persistence(initial_state, Arc::new(|_| {})) + } + + /// Creates a new [`Settings`] with the given initial state, + /// and which calls the `persister` function immediately whenever it is modified. + //--- + // TODO: Do we have any actual value for `persistence` that couldn’t be better handled + // by calling as_source()? Revisit this when we have more non-graphics settings. + pub fn with_persistence( + initial_state: Data, + persister: Arc, + ) -> Self { + Self(Arc::new(Inner { + data: listen::Cell::new(initial_state), + persister, + })) + } + + /// Returns a [`listen::Source`] of the settings. + /// This may be used to follow changes to the settings. + pub fn as_source(&self) -> listen::DynSource { + self.0.data.as_source() + } + + /// Returns the current graphics options. + pub fn get_graphics_options(&self) -> Arc { + self.0.data.get() + } + + /// Overwrites the graphics options. + pub fn set_graphics_options(&self, new_options: GraphicsOptions) { + self.set_state(Arc::new(new_options)); + } + + /// Overwrites the graphics options with a modified version. + /// + /// This operation is not atomic; that is, + /// if multiple threads are calling it, then one’s effect may be overwritten. + // TODO: Fix that (will require support from ListenableCell...compare-and-swap?) + #[doc(hidden)] + pub fn mutate_graphics_options(&self, f: impl FnOnce(&mut GraphicsOptions)) { + let mut options: Arc = self.0.data.get(); + f(Arc::make_mut(&mut options)); + self.set_state(options); + } + + fn set_state(&self, state: Data) { + (self.0.persister)(&state); + self.0.data.set(state); + } +} + +impl fmt::Debug for Settings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Settings") + .field("data", &self.0.data) + // can't print persister + .finish_non_exhaustive() + } +} + +impl Default for Settings { + fn default() -> Self { + Self::new(Default::default()) + } +} diff --git a/all-is-cubes-ui/src/ui_content/options.rs b/all-is-cubes-ui/src/ui_content/options.rs index 9063bb33b..2c2696a79 100644 --- a/all-is-cubes-ui/src/ui_content/options.rs +++ b/all-is-cubes-ui/src/ui_content/options.rs @@ -169,13 +169,10 @@ fn graphics_toggle_button( move || { let getter = getter.clone(); let setter = setter.clone(); - let _ignore_errors = cc.send(ControlMessage::ModifyGraphicsOptions(Box::new( - move |mut g| { - let mg = Arc::make_mut(&mut g); - setter(mg, !getter(mg)); - g - }, - ))); + let _ignore_errors = + cc.send(ControlMessage::ModifySettings(Box::new(move |settings| { + settings.mutate_graphics_options(|go| setter(go, !getter(go))) + }))); } }, ); @@ -219,14 +216,11 @@ fn graphics_enum_button