diff --git a/crates/anyps5-launcher/src/app/panels.rs b/crates/anyps5-launcher/src/app/panels.rs index c731aa0..1f8110f 100644 --- a/crates/anyps5-launcher/src/app/panels.rs +++ b/crates/anyps5-launcher/src/app/panels.rs @@ -1019,13 +1019,15 @@ impl PanelHost { let events = self.take_events(wanted); let kind = QuitKind::CancelUnlock; if cx.model.cancel_ask.is_none() { + // The question is answered (or gone): the card still has to play its exit, so this + // does not return before `show`. Returning here left the panel in its closing state + // with nothing drawing it, so it vanished at once and, on the next question, showed + // for one frame at full strength, left again and only then opened. self.cancel_unlock.close(); if wanted { cx.model.dismiss_overlay(); } - return; - } - if wanted && !self.cancel_unlock.is_active() { + } else if wanted && !self.cancel_unlock.is_active() { self.cancel_unlock.open(kind); } else if !wanted && self.cancel_unlock.is_active() { if cx.model.overlay.is_some_and(Overlay::is_side_panel) { @@ -1050,14 +1052,19 @@ impl PanelHost { /// The question before the window closes while games are unlocked or removed. fn quit_ui(&mut self, cx: &mut OverlayCx<'_>, wanted: bool) { let events = self.take_events(wanted); - let Some(kind) = cx.model.quit_ask else { + // Once the question is answered `quit_ask` is gone, but the card still has to play its + // exit, so this does not return before `show` (the card draws from what it showed last; + // the kind only matters while it is open). Returning here left the panel in its closing + // state with nothing drawing it: it vanished at once and, on the next question, showed + // for one frame at full strength, left again and only then opened. + let asked = cx.model.quit_ask; + let kind = asked.unwrap_or(QuitKind::Unlocking); + if asked.is_none() { self.quit.close(); if wanted { cx.model.dismiss_overlay(); } - return; - }; - if wanted && !self.quit.is_active() { + } else if wanted && !self.quit.is_active() { self.quit.open(kind); } else if !wanted && self.quit.is_active() { if cx.model.overlay.is_some_and(Overlay::is_side_panel) { diff --git a/crates/anyps5-launcher/src/screens/dialog.rs b/crates/anyps5-launcher/src/screens/dialog.rs index c98be20..0982c95 100644 --- a/crates/anyps5-launcher/src/screens/dialog.rs +++ b/crates/anyps5-launcher/src/screens/dialog.rs @@ -27,6 +27,7 @@ use crate::ui::glass::{GlassStyle, paint_glass}; use crate::ui::i18n::Language; use crate::ui::icons::Icon; use crate::ui::input::Command; +use crate::ui::pageblur; use crate::ui::sound::Cue; use crate::ui::text; use crate::ui::theme::{self, TextStyle, color, radius, shadows, size, space, ty}; @@ -269,6 +270,7 @@ fn paint( Motion::Overlay, ); if v <= 0.001 && !open { + pageblur::release(ctx, id); return None; } let v = v.clamp(0.0, 1.0); @@ -286,7 +288,10 @@ fn paint( // Text carries explicit colours, so only the painter's opacity fades it with the card. let mut text_painter = painter.clone(); text_painter.set_opacity(v); + // The flat dim holds until the blurred page arrives (a frame or two) and stays under + // High Contrast; the blur then fades in over it. paint_scrim(&painter, screen, SCRIM * v); + pageblur::paint(ctx, &painter, screen, id, open, v); let width = card_width(screen.width()); let inner = width - 2.0 * CARD_PADDING; @@ -336,10 +341,14 @@ fn paint( ..shadows::toast() }, ); - let mut style = GlassStyle::glass(radius::LARGE, 0.6); - style.black *= v; - style.alpha = v; - paint_glass(ctx, &painter, rect, style); + // The card is a translucent pane over the blurred page; until that page has been + // captured (and under High Contrast) it is the usual glass panel. + if !pageblur::paint_card(ctx, &painter, rect, radius::LARGE, id, v) { + let mut style = GlassStyle::glass(radius::LARGE, 0.6); + style.black *= v; + style.alpha = v; + paint_glass(ctx, &painter, rect, style); + } let mut y = rect.min.y + CARD_PADDING; let x = rect.min.x + CARD_PADDING; diff --git a/crates/anyps5-launcher/src/screens/settings.rs b/crates/anyps5-launcher/src/screens/settings.rs index b9fa9d7..abdf355 100644 --- a/crates/anyps5-launcher/src/screens/settings.rs +++ b/crates/anyps5-launcher/src/screens/settings.rs @@ -2656,7 +2656,11 @@ fn draw( area.max, ); let mut child = ui.new_child(UiBuilder::new().max_rect(list_rect)); - child.set_opacity(fade_in.clamp(0.0, 1.0)); + // Multiplied, not set: the panel fades its whole content (title, rail, rows) with its own + // openness, and `set_opacity` would replace that fade with the rows' own. The rows then stayed + // fully opaque while the page closed (the rest of the panel was already gone) and vanished in + // the last frame. + child.multiply_opacity(fade_in.clamp(0.0, 1.0)); let options = ListOptions { accent, language, diff --git a/crates/anyps5-launcher/src/ui/background.rs b/crates/anyps5-launcher/src/ui/background.rs index 25f9b50..5c12b5d 100644 --- a/crates/anyps5-launcher/src/ui/background.rs +++ b/crates/anyps5-launcher/src/ui/background.rs @@ -184,6 +184,13 @@ impl Backdrop { let light = self.theme.is_light() && !self.window_glass; let t = ctx.animate_to("backdrop/light", f32::from(light), Motion::Fade); crate::ui::theme::set_light(t); + // The question cards wear the colour of the look on screen (Game Colors: the game's). + let tones = if self.theme == Theme::GameColors { + self.game_palette + } else { + self.theme.palette() + }; + crate::ui::theme::set_card_wash(tones.card_wash()); let dt = f64::from(ctx.input(|i| i.unstable_dt)); let paused = self.paused || ctx.reduced_motion() || self.window_glass; self.clock.advance(dt, paused); diff --git a/crates/anyps5-launcher/src/ui/chrome.rs b/crates/anyps5-launcher/src/ui/chrome.rs index d3c9c27..95e8bfc 100644 --- a/crates/anyps5-launcher/src/ui/chrome.rs +++ b/crates/anyps5-launcher/src/ui/chrome.rs @@ -1781,7 +1781,16 @@ pub fn fullscreen_panel( add_contents: impl FnOnce(&mut Ui, f32) -> R, ) -> Option { let id = Id::unique(id); - let v = ctx.animate_to((id, "open"), if open { 1.0 } else { 0.0 }, Motion::Overlay); + // `animate_from_to`, not `animate_to`: a panel is only drawn while it is active, so its tracker + // is born on the very frame it opens, and `animate_to` starts a new tracker at its target (no + // entrance). The first opening (the "..." of a game, say) then appeared at once; from the + // second on the tracker existed, parked at 0, and the spring played. + let v = ctx.animate_from_to( + (id, "open"), + 0.0, + if open { 1.0 } else { 0.0 }, + Motion::Overlay, + ); if v <= 0.001 && !open { return None; } @@ -1799,6 +1808,10 @@ pub fn fullscreen_panel( black: style.black * v.clamp(0.0, 1.0), overlay: fade(style.overlay, v), material_mix: lerp(0.0, style.material_mix, v.clamp(0.0, 1.0)), + // The glass covers the page it opens over in the first half of the opening and + // lets it show again on the way out; before, it was opaque from the first frame, + // so the page vanished in one frame and the panel seemed to cut in. + alpha: style.alpha * (v * 2.0).clamp(0.0, 1.0), ..style }; paint_glass(ctx, &painter, screen, faded); @@ -1810,7 +1823,9 @@ pub fn fullscreen_panel( }; let area = scale_rect(screen, scale); let mut child = ui.new_child(UiBuilder::new().max_rect(area)); - child.set_opacity(v.clamp(0.0, 1.0)); + // The contents come in once the glass has mostly covered the page, so the two pages' + // texts never overlap. + child.set_opacity(((v - 0.25) / 0.75).clamp(0.0, 1.0)); result = Some(add_contents(&mut child, v)); }); result diff --git a/crates/anyps5-launcher/src/ui/mod.rs b/crates/anyps5-launcher/src/ui/mod.rs index 0a4028c..c6468f7 100644 --- a/crates/anyps5-launcher/src/ui/mod.rs +++ b/crates/anyps5-launcher/src/ui/mod.rs @@ -53,6 +53,7 @@ pub mod os_emoji; pub mod osk; pub mod pad_hub; pub mod pad_info; +pub mod pageblur; pub mod press; pub mod ps_button; pub mod reveal; diff --git a/crates/anyps5-launcher/src/ui/pageblur.rs b/crates/anyps5-launcher/src/ui/pageblur.rs new file mode 100644 index 0000000..2bcb231 --- /dev/null +++ b/crates/anyps5-launcher/src/ui/pageblur.rs @@ -0,0 +1,257 @@ +//! The page behind a card, blurred (docs/DESIGN.md 5.9.1). +//! +//! A question stands on a soft blur of the page it covers (the tiles, the hero, the text), not on +//! the theme's backdrop alone. egui draws the page in one pass, so the page cannot be blurred while +//! it is drawn; instead the window is captured ONCE when the card opens, shrunk to an eighth, +//! blurred on the CPU (a few thousand pixels) and kept as a small texture that the card's veil +//! stretches over the screen. While the card is open nothing is recomputed: a frame costs one +//! textured quad. The texture is dropped when the card is gone. +//! +//! Until the capture arrives (one or two frames) and under High Contrast the caller keeps its flat +//! scrim. + +use std::sync::{Arc, Mutex}; + +use egui::epaint::RectShape; +use egui::{ + Color32, ColorImage, Context, Id, Painter, Rect, ScreenshotCallback, Stroke, StrokeKind, + TextureHandle, TextureOptions, ViewportCommand, pos2, +}; + +use crate::ui::theme::{self, color}; + +/// How much the page is shrunk before it is blurred (the blur's strength grows with it). +const SHRINK: usize = 8; +/// Box blur radius, in shrunk pixels, and the number of passes (two boxes are close to a gaussian). +const RADIUS: usize = 3; +const PASSES: usize = 2; +/// How dark the blurred page is under the card (1 = as it is). +const DIM: f32 = 0.60; +/// Seconds for the blur to fade in once the capture has arrived. +const READY_FADE: f32 = 0.15; +/// How bright the blurred page shows through the card itself (the page around it is dimmed to +/// `DIM`, so the card reads as a lighter frosted pane). +const CARD_BRIGHT: f32 = 0.92; +/// How much of the card's tint (the look's own colour) lies over the blurred page (0 = clear glass, 1 = opaque). +const CARD_TINT: f32 = 0.38; + +/// Where the screenshot callback drops the capture (egui 0.37 answers with a one-shot callback, +/// not with an event, so each request has its own slot and no other screenshot is mistaken for it). +type Slot = Arc>>>; + +#[derive(Clone, Default)] +struct Held { + texture: Option, + asked: bool, + slot: Slot, +} + +/// Paints the blurred page over `rect` for the card `id`; `v` is the card's openness. +pub fn paint(ctx: &Context, painter: &Painter, rect: Rect, id: Id, open: bool, v: f32) { + if crate::ui::theme::high_contrast() { + return; + } + let key = id.with("page-blur"); + let mut held = ctx.data(|d| d.get_temp::(key)).unwrap_or_default(); + if open && !held.asked { + // The card is at about 0 on its first frame, so the capture shows the page alone. + held.asked = true; + let slot = held.slot.clone(); + let repaint = ctx.clone(); + ctx.send_viewport_cmd(ViewportCommand::Screenshot(ScreenshotCallback::new( + move |image| { + if let Ok(mut guard) = slot.lock() { + *guard = Some(image); + } + repaint.request_repaint(); + }, + ))); + ctx.request_repaint(); + } + if held.asked && held.texture.is_none() { + let shot = held.slot.lock().ok().and_then(|mut guard| guard.take()); + if let Some(image) = shot { + held.texture = + Some(ctx.load_texture("page-blur", blurred(&image), TextureOptions::LINEAR)); + } + } + ctx.data_mut(|d| d.insert_temp(key, held.clone())); + let Some(texture) = &held.texture else { + return; + }; + let ready = ctx.animate_bool_with_time(key.with("ready"), true, READY_FADE); + let alpha = (v * ready).clamp(0.0, 1.0); + if alpha <= 0.0 { + return; + } + let a = (alpha * 255.0).round(); + let k = (a * DIM).round() as u8; + // Premultiplied: the image fades in and dims at once. + let tint = Color32::from_rgba_premultiplied(k, k, k, a as u8); + painter.image( + texture.id(), + rect, + Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), + tint, + ); +} + +/// Paints the card's own pane: the blurred page behind it, seen through a translucent blue tint. +/// Call after [`paint`] in the same frame. Returns `false` when there is no blurred page to show +/// yet (or under High Contrast), so the caller draws its usual panel instead. +pub fn paint_card( + ctx: &Context, + painter: &Painter, + rect: Rect, + corner: f32, + id: Id, + v: f32, +) -> bool { + if theme::high_contrast() { + return false; + } + let key = id.with("page-blur"); + let Some(held) = ctx.data(|d| d.get_temp::(key)) else { + return false; + }; + let Some(texture) = &held.texture else { + return false; + }; + let ready = ctx.animate_bool_with_time(key.with("ready"), true, READY_FADE); + let alpha = (v * ready).clamp(0.0, 1.0); + if alpha <= 0.0 { + return true; + } + let screen = ctx.content_rect(); + let uv = Rect::from_min_max( + pos2( + (rect.min.x - screen.min.x) / screen.width(), + (rect.min.y - screen.min.y) / screen.height(), + ), + pos2( + (rect.max.x - screen.min.x) / screen.width(), + (rect.max.y - screen.min.y) / screen.height(), + ), + ); + let a = (alpha * 255.0).round(); + let k = (a * CARD_BRIGHT).round() as u8; + // Premultiplied: the page fades in and dims at once, rounded like the card. + let tint = Color32::from_rgba_premultiplied(k, k, k, a as u8); + painter.add(RectShape::filled(rect, corner, tint).with_texture(texture.id(), uv)); + // The card's tint, left see-through: the colour of the look on screen, not a fixed blue. + let wash = if theme::is_light() { + theme::scrim(CARD_TINT * alpha) + } else { + let [r, g, b] = theme::card_wash().map(|c| (c * 255.0).round() as u8); + Color32::from_rgba_unmultiplied(r, g, b, (CARD_TINT * alpha * 255.0).round() as u8) + }; + painter.rect_filled(rect, corner, wash); + painter.add(RectShape::stroke( + rect, + corner, + Stroke::new( + 1.0 / ctx.pixels_per_point(), + theme::fade(color::hairline_glass(), alpha), + ), + StrokeKind::Inside, + )); + true +} + +/// Drops what the card kept (call once its exit is over). +pub fn release(ctx: &Context, id: Id) { + let key = id.with("page-blur"); + if ctx.data(|d| d.get_temp::(key)).is_some() { + ctx.data_mut(|d| d.remove::(key)); + } +} + +/// The capture shrunk to an eighth and blurred: averages a few samples of each block, then two +/// separable box passes with clamped edges. +fn blurred(src: &ColorImage) -> ColorImage { + let [w, h] = src.size; + let sw = (w / SHRINK).max(1); + let sh = (h / SHRINK).max(1); + let mut px = vec![[0.0_f32; 3]; sw * sh]; + for y in 0..sh { + let (y0, y1) = (y * h / sh, ((y + 1) * h / sh).max(y * h / sh + 1)); + for x in 0..sw { + let (x0, x1) = (x * w / sw, ((x + 1) * w / sw).max(x * w / sw + 1)); + let mut sum = [0.0_f32; 3]; + let mut n = 0.0_f32; + for sy in (y0..y1).step_by(2) { + for sx in (x0..x1).step_by(2) { + let c = src.pixels[sy.min(h - 1) * w + sx.min(w - 1)].to_array(); + sum[0] += f32::from(c[0]); + sum[1] += f32::from(c[1]); + sum[2] += f32::from(c[2]); + n += 1.0; + } + } + px[y * sw + x] = [sum[0] / n, sum[1] / n, sum[2] / n]; + } + } + for _ in 0..PASSES { + px = box_pass(&px, sw, sh, true); + px = box_pass(&px, sw, sh, false); + } + let mut rgba = Vec::with_capacity(sw * sh * 4); + for p in &px { + rgba.push(p[0].round().clamp(0.0, 255.0) as u8); + rgba.push(p[1].round().clamp(0.0, 255.0) as u8); + rgba.push(p[2].round().clamp(0.0, 255.0) as u8); + rgba.push(255); + } + ColorImage::from_rgba_unmultiplied([sw, sh], &rgba) +} + +/// One box blur along a row (`horizontal`) or a column, edges clamped. +fn box_pass(src: &[[f32; 3]], w: usize, h: usize, horizontal: bool) -> Vec<[f32; 3]> { + let mut out = vec![[0.0_f32; 3]; src.len()]; + let (lines, len) = if horizontal { (h, w) } else { (w, h) }; + let at = |line: usize, i: usize| { + if horizontal { + line * w + i + } else { + i * w + line + } + }; + let taps = (2 * RADIUS + 1) as f32; + for line in 0..lines { + for i in 0..len { + let mut sum = [0.0_f32; 3]; + for d in 0..=2 * RADIUS { + let j = (i + d).saturating_sub(RADIUS).min(len - 1); + let p = src[at(line, j)]; + sum[0] += p[0]; + sum[1] += p[1]; + sum[2] += p[2]; + } + out[at(line, i)] = [sum[0] / taps, sum[1] / taps, sum[2] / taps]; + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_flat_page_stays_flat() { + let src = ColorImage::from_rgba_unmultiplied([64, 32], &[40, 80, 120, 255].repeat(64 * 32)); + let out = blurred(&src); + assert_eq!(out.size, [8, 4]); + assert!( + out.pixels + .iter() + .all(|c| c.to_array() == [40, 80, 120, 255]) + ); + } + + #[test] + fn a_tiny_window_still_gives_one_pixel() { + let src = ColorImage::from_rgba_unmultiplied([3, 2], &[10, 10, 10, 255].repeat(6)); + assert_eq!(blurred(&src).size, [1, 1]); + } +} diff --git a/crates/anyps5-launcher/src/ui/theme.rs b/crates/anyps5-launcher/src/ui/theme.rs index 7411a58..42b88be 100644 --- a/crates/anyps5-launcher/src/ui/theme.rs +++ b/crates/anyps5-launcher/src/ui/theme.rs @@ -178,6 +178,30 @@ pub fn with_light(light: bool, f: impl FnOnce() -> R) -> R { out } +/// The tint of the question cards (the cancel / quit popups): the active look's own colour, as the +/// three channels of a dark tone. Set by the backdrop each frame from the palette on screen (Game +/// Colors included); read by [`crate::ui::pageblur`]. Starts as Cosmic's blue. +static CARD_WASH: [AtomicU32; 3] = [AtomicU32::new(0), AtomicU32::new(0), AtomicU32::new(0)]; + +pub fn set_card_wash(rgb: [f32; 3]) { + for (slot, v) in CARD_WASH.iter().zip(rgb) { + slot.store(v.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed); + } +} + +/// The card tint as straight sRGB floats (0..=1). +pub fn card_wash() -> [f32; 3] { + let [r, g, b] = &CARD_WASH; + let load = |a: &AtomicU32| f32::from_bits(a.load(Ordering::Relaxed)); + let wash = [load(r), load(g), load(b)]; + // Nothing set yet (all zero bits is 0.0): Cosmic's tint, as before. + if wash == [0.0, 0.0, 0.0] { + Theme::Cosmic.palette().card_wash() + } else { + wash + } +} + /// High Contrast (docs/DESIGN.md 3.14): brighter ink and edges and no frosted glass. Set by the /// shell from the preference; read by the colour tokens and the glass. static HIGH_CONTRAST: AtomicBool = AtomicBool::new(false); @@ -366,6 +390,12 @@ pub struct Palette { } impl Palette { + /// The tint a question card wears over the blurred page: the look's deep tone lifted most of + /// the way to its middle one, so a card on Sunset is wine, on Aurora teal, on Cosmic blue. + pub fn card_wash(&self) -> [f32; 3] { + let at = |i: usize| self.deep[i] + (self.middle[i] - self.deep[i]) * 0.6; + [at(0), at(1), at(2)] + } pub fn deep_color(&self) -> Color32 { rgb(self.deep[0], self.deep[1], self.deep[2]) } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index bbf974d..af64e53 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1663,11 +1663,11 @@ member's volume (0 to 200 %) stays a stepper. Every question that stands alone is one card in the middle of the dimmed page, like a console's dialog: the uninstall question, the restore question, Close Game, the missing toolkit release, Delete in Captures and every question of the online area (Block, Remove Friend, Unsend, Leave Party, Remove from Party, Leave Conversation). One component draws them (`dialog::draw`; `side::draw` hands a `question` view to it), so they look and answer the same. -- **The page behind** is dimmed with black 0.55 (a local constant); the card is glass (`radius::LARGE` 20, black 0.6, the toast's shadow), 560 wide (less on a narrow window: the screen minus 2 x 24), padding 24. The card opens with the overlay spring: a fade and a rise of 12 dp (a fade only under "Reduced"). A click on the dimmed page does nothing and plays no cue, so nothing is answered by accident. The card leaves with the same spring and fade; its buttons stay on it while it leaves but no longer answer. The one exception to the title being the question is the missing toolkit release, whose title is the game's name (the sentence under it asks). A question's "no" is flagged on its row (`Row::refusal`), never recognised by its id. +- **The page behind** is dimmed with black 0.55 (a local constant); the card is glass (`radius::LARGE` 20, black 0.6, the toast's shadow) and, over the blurred page, wears the colour of the look on screen: `Palette::card_wash` (the deep tone lifted 60 % of the way to the middle one: blue on Cosmic, teal on Aurora, wine on Sunset, copper on Ember, petrol on Tide; on Game Colors the palette the selected game gives), which the backdrop publishes every frame (`theme::set_card_wash`) and `pageblur::paint_card` lays at 0.38. Daylight keeps its paper veil (the ink is dark) and High Contrast the flat panel. It is 560 wide (less on a narrow window: the screen minus 2 x 24), padding 24. The card opens with the overlay spring: a fade and a rise of 12 dp (a fade only under "Reduced"). A click on the dimmed page does nothing and plays no cue, so nothing is answered by accident. The card leaves with the same spring and fade; its buttons stay on it while it leaves but no longer answer. The one exception to the title being the question is the missing toolkit release, whose title is the game's name (the sentence under it asks). A question's "no" is flagged on its row (`Row::refusal`), never recognised by its id. - **Content**, top to bottom with 8 between the lines: the title (28 light, `text`; the question in sentence case: "Remove this installation?", "Restore this backup?", "Close the game?", "Block Lumen?"; at most 3 lines), the sentence (17 regular, `text_label`, leading 1.45; it wraps, in Turkish too, and is bound at 8 lines), a quiet line in `text_detail` 14 only when a button carries a state (the download under way), then 24 and the buttons. Nothing else. - **Buttons** are `ConsoleButton` Large (54), 12 apart, in one row of equal widths when the widest label fits that many times with the gaps, and one under another (each the full width) when it does not. Order is SYSTEM.md 3: the primary or destructive answer first, **Cancel last**. The destructive answer is a secondary button in the danger ink, never a fill; only a question with no destructive answer has a primary (the first answer that is not a refusal: Restore, Download v0.1.2). Cancel, Keep Playing and "Follow Global Setting" are secondary. - **Focus** opens on the safe answer (Cancel, Keep Playing) and on Download where the way on changes nothing. Left and Up go to the previous button, Right and Down to the next (a bump at the ends), Select (Enter, A) runs the focused one, Back (Esc, B) cancels. The ring shows only for keys and the pad. The card brings no hint bar: Cancel is on screen, so "Back" is not repeated (SYSTEM.md 13), and Select is the button in focus. -- Gallery: `confirm` (uninstall), `restore`, `toolkit-missing`, `captures-confirm` and `friends-confirm*`. +- Gallery: `confirm` (uninstall), `restore`, `toolkit-missing`, `captures-confirm` and `friends-confirm*`; `home-cancel-ask` with `--theme=cosmic|aurora|sunset|ember|tide|daylight` shows the card's tint on each look. Opened by "..." or `O` / Y / Menu / Options. Title = game name, subtitle `gameSettings`, rows below. (S-game-settings.) Rows in order, with the condition for each: