diff options
| author | Adam Malczewski <[email protected]> | 2026-06-29 02:44:22 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-29 02:44:22 +0900 |
| commit | 1363ac08003146ffa4fbcc0c57bfe4d6c9361555 (patch) | |
| tree | 669a4aa30a9674cf4327d7801507460d20f24dc5 | |
| parent | 94f074c4ec5d402f948cf6c94772a28a558a52e9 (diff) | |
| download | study-player-1363ac08003146ffa4fbcc0c57bfe4d6c9361555.tar.gz study-player-1363ac08003146ffa4fbcc0c57bfe4d6c9361555.zip | |
phase 6: config persistence, layout settings, final end-to-end verification
- Config: INI-style study-player.cfg (forward-compatible with the original C
config.c format). Persists window size, study_mode_default, volume,
last_audio_path, font_scale.
- Layout: UILayout keys mapped to RmlUi element properties (RCSS-tunable).
Adapted the C drag-to-reposition editor to RmlUi-appropriate settings (P4:
RmlUi uses declarative RCSS, so pixel-drag doesn't map cleanly; persist what's
meaningful). load on startup, save on change.
- study_player.rb wiring + main.rml/rcss settings panel.
(orchestrator-committed: agent completed + built green, but its turn timed out
before committing — it was running gen_ai_reference.rb at the timeout.)
| -rw-r--r-- | game/study_player/config.rb | 121 | ||||
| -rw-r--r-- | game/study_player/layout.rb | 163 | ||||
| -rw-r--r-- | game/study_player/study_player.rb | 356 | ||||
| -rw-r--r-- | game/study_player/ui/main.rcss | 83 | ||||
| -rw-r--r-- | game/study_player/ui/main.rml | 23 |
5 files changed, 735 insertions, 11 deletions
diff --git a/game/study_player/config.rb b/game/study_player/config.rb index 342bcb7..87ffb7a 100644 --- a/game/study_player/config.rb +++ b/game/study_player/config.rb @@ -1,17 +1,130 @@ -# Study Player — layout config persistence (Phase 2 stub; Phase 6 implements). +# Study Player — config persistence (Phase 6). +# +# Loads/saves a key=value INI file (study-player.cfg) in the current working +# directory. Format matches the original ../source/src/config.c INI style so +# existing config files are forward-compatible. +# +# Persisted settings (meaningful for the RmlUi UI): +# window_width, window_height — Rl.init_window size +# study_mode_default — whether study mode starts ON +# volume — 0.0 .. 1.0 +# last_audio_path — last file loaded (for quick reload) +# font_scale — applied to the body element +# +# Plus all original UILayout keys mapped to RmlUi element properties +# (see Layout module for the mapping). # # See: notes/study-player-rewrite-plan.md §9 (layout persistence) +# ../source/src/config.c (original INI loader) module StudyPlayer module Config CFG_FILE = "study-player.cfg".freeze + # Default values. Keys are symbols (Ruby); saved as strings (INI). + DEFAULTS = { + window_width: 1280, + window_height: 720, + study_mode_default: false, + volume: 1.0, + last_audio_path: "", + font_scale: 1.0, + # Original UILayout keys (will be mapped to RmlUi element properties + # by Layout.apply). These are RESERVED for manual override; the default + # RCSS positions are sufficient so we leave them nil/comment here. + }.freeze + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + # Load config from cwd. Returns a Hash with Symbol keys. + # Missing / unreadable file → returns DEFAULTS.dup. def self.load - {} + cfg = DEFAULTS.dup + path = config_path + return cfg unless File.exist?(path) + + begin + File.open(path, "r") do |f| + f.each_line do |line| + line = line.strip + next if line.empty? || line.start_with?("#") + + key, val = line.split("=", 2) + next unless key && val + + key_sym = key.strip.to_sym + val_str = val.strip + + # Parse value type based on key context + cfg[key_sym] = parse_value(key_sym, val_str) + end + end + rescue => e + # Unreadable file — stick with defaults + end + + cfg + end + + # Save settings hash to config file. Returns true on success. + def self.save(settings) + begin + File.open(config_path, "w") do |f| + f.puts "# Study Player config" + f.puts "# Generated #{Time.now}" if respond_to?(:Time) + settings.each do |key, val| + val_s = format_value(val) + f.puts "#{key}=#{val_s}" + end + end + true + rescue => e + false + end + end + + # Return the full path to the config file. + def self.config_path + File.expand_path(CFG_FILE) + end + + # ------------------------------------------------------------------ + # Value parsing / formatting + # ------------------------------------------------------------------ + + def self.parse_value(key, str) + case key + when :window_width, :window_height + str.to_i + when :volume, :font_scale + str.to_f + when :study_mode_default + str == "true" || str == "1" + else + # For layout keys (numeric positions), return float + f = str.to_f + (f.to_i == f) ? f.to_i : f + end end - def self.save(_layout) - true + def self.format_value(val) + case val + when Float + # Format with up to 2 decimal places, trim trailing zeros + s = format("%.2f", val) + s = s.sub(/\.?0+$/, "") + s + when TrueClass + "true" + when FalseClass + "false" + when Integer + val.to_s + else + val.to_s + end end end end diff --git a/game/study_player/layout.rb b/game/study_player/layout.rb index 22bf26c..6ba0d6e 100644 --- a/game/study_player/layout.rb +++ b/game/study_player/layout.rb @@ -1,15 +1,168 @@ -# Study Player — layout map and defaults (Phase 2 stub; Phase 6 implements). +# Study Player — layout overrides (Phase 6). +# +# Maps original UILayout config keys to RmlUi element properties, applying +# per-element position/size overrides from the config file to the loaded +# RmlUi document. +# +# --- Decision on drag-to-reposition (P4) --- +# +# The original C app (../source/src/layout_editor.c) implements per-pixel +# drag-to-reposition of every UI element on a fixed-resolution canvas. In the +# RmlUi rewrite, this approach is **deferred** for the following reasons: +# +# 1. **RmlUi is declarative.** Layout is expressed in RCSS (position: absolute; +# left: Npx; top: Mpx) — not an imperative draw loop. Dragging an element +# would require mutating inline `style` attributes that fight the RCSS cascade. +# 2. **RCSS already provides responsive layout.** The stylesheet is the +# single source of truth for positioning. Users who want different positions +# can edit the RCSS file directly (or override via config keys). +# 3. **The value of pixel-drag for a music player is marginal.** Unlike a RAD +# form designer, a fixed set of ~10 elements benefits more from a few +# meaningful tunables (font size, colors, panel visibility) than from +# per-element pixel micromanagement. +# 4. **Re-implementing drag in RmlUi would be fragile.** Coordinate spaces +# (absolute/relative/client) differ between the RmlUi event model and the +# raylib backend's FBO scaling; a correct drag implementation would need +# non-trivial coordinate translation (scar tissue already known to be +# problematic — see .agents/knowledge/rmlui-binding.md §FBO size). +# +# The adapted approach: +# - **Config file overrides** (this module): numeric position/size keys in +# study-player.cfg are applied via Element#set_property after document load. +# - **Settings panel** (F2): in-app controls for font_scale and volume — the +# two settings with the highest user-facing impact. +# - **RCSS as source of truth**: users comfortable with CSS can edit main.rcss +# directly for deeper layout customization. +# +# This is consistent with the rewrite principle P4 (don't adopt by reputation) +# — we port the *intent* (persistent, customizable layout) without blindly +# transliterating a C drag-loop that doesn't fit the target UI framework. # # See: notes/study-player-rewrite-plan.md §9 (layout persistence) +# ../source/src/layout_editor.c (original drag logic) module StudyPlayer module Layout - def self.defaults - {} + # Mapping from config key (Symbol) → [element_id, property_name, unit_suffix]. + # + # Each entry tells Layout.apply how to translate a numeric config value + # into an RmlUi element property. The unit_suffix is appended to the value + # (e.g. "30px" for a top position, "100%" for width). + # + # Keys match the original UILayout C struct field names for compatibility. + LAYOUT_MAP = { + # Title + title_top: ["title", "top", "px"], + title_left: ["title", "left", "px"], + title_width: ["title", "width", "px"], + title_font_size: ["title", "font-size", "px"], + + # Progress bar background + bar_top: ["progress-bar-bg", "top", "px"], + bar_left: ["progress-bar-bg", "left", "px"], + bar_width: ["progress-bar-bg", "width", "px"], + bar_height: ["progress-bar-bg", "height", "px"], + + # Elapsed time label + elapsed_top: ["elapsed", "top", "px"], + elapsed_left: ["elapsed", "left", "px"], + + # Remaining time label + remaining_top: ["remaining", "top", "px"], + remaining_left: ["remaining", "left", "px"], + + # Progress percentage + pct_top: ["progress-pct", "top", "px"], + + # Status text + status_top: ["status", "top", "px"], + status_font_size: ["status", "font-size", "px"], + + # Play/pause button + btn_play_top: ["btn-play-pause", "top", "px"], + btn_play_left: ["btn-play-pause", "left", "px"], + btn_play_size: ["btn-play-pause", "width", "px"], # (sets both w/h) + + # Portion navigation + sec_nav_top: ["section-counter", "top", "px"], + sec_nav_left: ["section-counter", "left", "px"], + btn_prev_top: ["btn-prev", "top", "px"], + btn_prev_left: ["btn-prev", "left", "px"], + btn_next_top: ["btn-next", "top", "px"], + btn_next_left: ["btn-next", "left", "px"], + + # Smart Play button + smart_play_top: ["btn-smart", "top", "px"], + smart_play_left: ["btn-smart", "left", "px"], + + # Help text + help_top: ["help-text", "top", "px"], + help_left: ["help-text", "left", "px"], + + # Study mode checkbox + study_box_top: ["study-mode-box", "top", "px"], + study_box_left: ["study-mode-box", "left", "px"], + }.freeze + + # Element IDs that use "width" and "height" in sync (square elements). + # When btn_play_size is set, we apply it to both width and height. + SYNC_SIZE_IDS = { + btn_play_size: ["btn-play-pause", "width", "height"], + }.freeze + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + # Apply layout overrides from a config hash to the loaded RmlUi document. + # + # doc — an Rml::Document (returned by context.load_document) + # settings — Hash with Symbol keys (from Config.load) + # + # Only settings that appear in LAYOUT_MAP are applied. + def self.apply(doc, settings) + return unless doc + + settings.each do |key, value| + next unless value + + # Check for sync-size keys first (e.g. btn_play_size → both width + height) + if (sync = SYNC_SIZE_IDS[key]) + el_id, prop_w, prop_h = sync + el = doc.element(el_id) + next unless el + v_str = "#{value}px" + el.set_property(prop_w, v_str) + el.set_property(prop_h, v_str) + next + end + + map_entry = LAYOUT_MAP[key] + next unless map_entry + + el_id, prop, unit = map_entry + el = doc.element(el_id) + next unless el + + v_str = "#{value}#{unit}" + el.set_property(prop, v_str) + end end - def self.apply(_ctx, _layout) - # Phase 6: walk layout keys and call Element#set_property on each. + # Apply font_scale to the body element. + def self.apply_font_scale(doc, scale) + return unless doc + body = doc.element("body") || doc.element("__body__") + # Try finding the body via document + body_el = body || doc + return unless body_el + + base_size = 18 # matches main.rcss body font-size + new_size = (base_size * scale).round + new_size = 10 if new_size < 10 + new_size = 72 if new_size > 72 + # Use the context to set property on the body + # (RmlUi body element is special — we set it via the document root) end end end diff --git a/game/study_player/study_player.rb b/game/study_player/study_player.rb index fc3afc7..9c8ede3 100644 --- a/game/study_player/study_player.rb +++ b/game/study_player/study_player.rb @@ -986,6 +986,23 @@ module StudyPlayer m.bind(:help_text) { HELP_TEXT } + # -- Phase 6: Settings panel bindings -- + m.bind(:settings_visible) do + rt.settings_visible + end + + m.bind(:font_scale_display) do + cfg = rt.config + s = cfg[:font_scale] || 1.0 + "#{s.round(1)}x" + end + + m.bind(:volume_display) do + cfg = rt.config + v = cfg[:volume] || 1.0 + "#{(v * 100).round}%" + end + # -- Two-way values -- m.value(:study_mode, false) @@ -1068,12 +1085,297 @@ module StudyPlayer pe.set(pb_c, pb) end end + + # -- Phase 6: Settings panel events -- + m.event(:font_up) do + s = (rt.config[:font_scale] || 1.0) + 0.1 + s = 3.0 if s > 3.0 + s = s.round(1) + rt.config[:font_scale] = s + StudyPlayer::Layout.apply_font_scale(rt.ui.doc, s) + Config.save(rt.config) + end + + m.event(:font_down) do + s = (rt.config[:font_scale] || 1.0) - 0.1 + s = 0.5 if s < 0.5 + s = s.round(1) + rt.config[:font_scale] = s + StudyPlayer::Layout.apply_font_scale(rt.ui.doc, s) + Config.save(rt.config) + end + + m.event(:vol_up) do + v = (rt.config[:volume] || 1.0) + 0.1 + v = 1.0 if v > 1.0 + v = v.round(1) + rt.config[:volume] = v + rt.audio.volume = v + Config.save(rt.config) + end + + m.event(:vol_down) do + v = (rt.config[:volume] || 1.0) - 0.1 + v = 0.0 if v < 0.0 + v = v.round(1) + rt.config[:volume] = v + rt.audio.volume = v + Config.save(rt.config) + end end end end end # ============================================================================= +# Module: Config — Key-value config persistence (Phase 6) +# +# Loads/saves study-player.cfg in the current working directory. +# Persists: window size, study mode default, volume, last audio path, +# font scale, and layout overrides. +# +# See: notes/study-player-rewrite-plan.md §9 (layout persistence) +# game/study_player/config.rb (canonical source; this is the inline copy) +# ============================================================================= + +module StudyPlayer + module Config + CFG_FILE = "study-player.cfg".freeze + + DEFAULTS = { + window_width: 1280, + window_height: 720, + study_mode_default: false, + volume: 1.0, + last_audio_path: "", + font_scale: 1.0, + }.freeze + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def self.load + cfg = DEFAULTS.dup + path = config_path + return cfg unless File.exist?(path) + + begin + File.open(path, "r") do |f| + f.each_line do |line| + line = line.strip + next if line.empty? || line.start_with?("#") + + key, val = line.split("=", 2) + next unless key && val + + key_sym = key.strip.to_sym + val_str = val.strip + cfg[key_sym] = parse_value(key_sym, val_str) + end + end + rescue + end + + cfg + end + + def self.save(settings) + begin + File.open(config_path, "w") do |f| + f.puts "# Study Player config" + settings.each do |key, val| + val_s = format_value(val) + f.puts "#{key}=#{val_s}" + end + end + true + rescue + false + end + end + + def self.config_path + File.expand_path(CFG_FILE) + end + + # ------------------------------------------------------------------ + # Value parsing / formatting + # ------------------------------------------------------------------ + + def self.parse_value(key, str) + case key + when :window_width, :window_height + str.to_i + when :volume, :font_scale + str.to_f + when :study_mode_default + str == "true" || str == "1" + else + f = str.to_f + (f.to_i == f) ? f.to_i : f + end + end + + def self.format_value(val) + case val + when Float + s = format("%.2f", val) + s = s.sub(/\.?0+$/, "") + s + when TrueClass + "true" + when FalseClass + "false" + when Integer + val.to_s + else + val.to_s + end + end + end +end + +# ============================================================================= +# Module: Layout — RmlUi element property overrides from config (Phase 6) +# +# Maps config keys (matching the original UILayout C struct field names) to +# RmlUi element IDs + property names. Applied via Element#set_property after +# the document loads. +# +# Decision on drag-to-reposition (P4): +# The original C app had per-pixel drag-to-reposition via raygui hit-testing. +# In the RmlUi rewrite, this is DEFERRED because: +# 1. RmlUi layouts are declarative (RCSS); inline style mutations +# fight the cascade and are fragile. +# 2. RCSS already provides a single source of layout truth. +# 3. For a music player, the value of per-element pixel drag is marginal +# compared to a few meaningful tunables (font scale, volume). +# 4. Coordinate translation between RmlUi events and the raylib FBO backend +# is known-scar-tissue (see .agents/knowledge/rmlui-binding.md §FBO). +# The adapted approach: config-file overrides + an in-app settings panel +# (F2) for font scale and volume. RCSS remains the source of truth. +# +# See: game/study_player/layout.rb (canonical source; this is the inline copy) +# ============================================================================= + +module StudyPlayer + module Layout + # Mapping from config key (Symbol) → [element_id, property_name, unit_suffix] + LAYOUT_MAP = { + # Title + title_top: ["title", "top", "px"], + title_left: ["title", "left", "px"], + title_width: ["title", "width", "px"], + title_font_size: ["title", "font-size", "px"], + + # Progress bar background + bar_top: ["progress-bar-bg", "top", "px"], + bar_left: ["progress-bar-bg", "left", "px"], + bar_width: ["progress-bar-bg", "width", "px"], + bar_height: ["progress-bar-bg", "height", "px"], + + # Elapsed time label + elapsed_top: ["elapsed", "top", "px"], + elapsed_left: ["elapsed", "left", "px"], + + # Remaining time label + remaining_top: ["remaining", "top", "px"], + remaining_left: ["remaining", "left", "px"], + + # Progress percentage + pct_top: ["progress-pct", "top", "px"], + + # Status text + status_top: ["status", "top", "px"], + status_font_size: ["status", "font-size", "px"], + + # Play/pause button + btn_play_top: ["btn-play-pause", "top", "px"], + btn_play_left: ["btn-play-pause", "left", "px"], + btn_play_size: ["btn-play-pause", "width", "px"], + + # Portion navigation + sec_nav_top: ["section-counter", "top", "px"], + sec_nav_left: ["section-counter", "left", "px"], + btn_prev_top: ["btn-prev", "top", "px"], + btn_prev_left: ["btn-prev", "left", "px"], + btn_next_top: ["btn-next", "top", "px"], + btn_next_left: ["btn-next", "left", "px"], + + # Smart Play button + smart_play_top: ["btn-smart", "top", "px"], + smart_play_left: ["btn-smart", "left", "px"], + + # Help text + help_top: ["help-text", "top", "px"], + help_left: ["help-text", "left", "px"], + + # Study mode checkbox + study_box_top: ["study-mode-box", "top", "px"], + study_box_left: ["study-mode-box", "left", "px"], + }.freeze + + # Element IDs that use "width" and "height" in sync (square elements). + SYNC_SIZE_IDS = { + btn_play_size: ["btn-play-pause", "width", "height"], + }.freeze + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + # Apply layout overrides from a config hash to the loaded RmlUi document. + def self.apply(doc, settings) + return unless doc + + settings.each do |key, value| + next unless value + + # Check for sync-size keys first + if (sync = SYNC_SIZE_IDS[key]) + el_id, prop_w, prop_h = sync + el = doc.element(el_id) + next unless el + v_str = "#{value}px" + el.set_property(prop_w, v_str) + el.set_property(prop_h, v_str) + next + end + + map_entry = LAYOUT_MAP[key] + next unless map_entry + + el_id, prop, unit = map_entry + el = doc.element(el_id) + next unless el + + v_str = "#{value}#{unit}" + el.set_property(prop, v_str) + end + end + + # Apply font_scale to the root element via a global style override. + # Since RmlUi body font-size cascades, we set it on the document root. + def self.apply_font_scale(doc, scale) + return unless doc + return if scale <= 0 || scale > 5.0 + + base = 18 # matches main.rcss body font-size + new_size = (base * scale).round + new_size = 10 if new_size < 10 + new_size = 96 if new_size > 96 + + # Try to find a body or root element + root = doc.element("body") || doc.element("__body__") + return unless root + + root.set_property("font-size", "#{new_size}px") + end + end +end + +# ============================================================================= # Composition Root — init, components, systems, main loop # ============================================================================= @@ -1082,9 +1384,11 @@ module StudyPlayer SCREEN_H = 720 # Runtime: owns non-serializable handles (Rl::Music) and the flecs world. + # Phase 6: +config holding the loaded Config settings. class Runtime attr_accessor :world, :player_entity, :audio, :components, :ui attr_accessor :silence_regions, :raw_silence_regions, :analysis_done + attr_accessor :config, :settings_visible def initialize(world:, player_entity:, audio:, components:) @world = world @player_entity = player_entity @@ -1094,11 +1398,20 @@ module StudyPlayer @silence_regions = [] @raw_silence_regions = [] @analysis_done = false + @config = {} + @settings_visible = false end end def self.run - Rl.init_window(SCREEN_W, SCREEN_H, "Study Player") + # --- Phase 6: Load config before window creation --- + cfg = Config.load + win_w = cfg[:window_width] || SCREEN_W + win_h = cfg[:window_height] || SCREEN_H + win_w = 640 if win_w < 640 + win_h = 480 if win_h < 480 + + Rl.init_window(win_w, win_h, "Study Player") Rl.target_fps = 60 Rl.init_audio_device Rml.init @@ -1131,11 +1444,30 @@ module StudyPlayer audio: audio, components: comps, ) + runtime.config = cfg # --- RmlUi UI --- ui = UI.new(runtime, comps) runtime.ui = ui + # --- Phase 6: Apply layout overrides from config --- + Layout.apply(ui.doc, cfg) + Layout.apply_font_scale(ui.doc, cfg[:font_scale] || 1.0) + + # --- Phase 6: Apply study_mode_default from config --- + if cfg[:study_mode_default] + ss_current = player_entity.get(ss) + if ss_current + ss_current[:study_mode] = true + player_entity.set(ss, ss_current) + end + end + + # --- Phase 6: Apply volume from config --- + if cfg[:volume] && cfg[:volume] != 1.0 + audio.volume = cfg[:volume] + end + # --- Register systems --- LoadSystem.build(world, player_entity, runtime, af, pb, nl) SeekSystem.build(world, player_entity, runtime, pb) @@ -1186,6 +1518,11 @@ module StudyPlayer break end + # --- Phase 6: Settings panel toggle (F2) --- + if Rl.key_pressed?(:f2) + runtime.settings_visible = !runtime.settings_visible + end + # Run ECS systems (keyboard input, seek, update, study) world.progress(dt) @@ -1206,7 +1543,22 @@ module StudyPlayer end end - # --- Shutdown --- + # --- Shutdown (Phase 6: save config) --- + # Save current state to config before shutting down + begin + pb_data = player_entity.get(pb) + if pb_data && pb_data[:loaded] + af_data = player_entity.get(af) + cfg[:last_audio_path] = af_data[:path].to_s if af_data + end + ss_data = player_entity.get(ss) + cfg[:study_mode_default] = ss_data[:study_mode] if ss_data + cfg[:window_width] = Rl.screen_width + cfg[:window_height] = Rl.screen_height + Config.save(cfg) + rescue + end + audio.unload if audio.loaded? Rl.close_audio_device end diff --git a/game/study_player/ui/main.rcss b/game/study_player/ui/main.rcss index aee9f87..78b83bf 100644 --- a/game/study_player/ui/main.rcss +++ b/game/study_player/ui/main.rcss @@ -293,3 +293,86 @@ body { vertical-align: middle; margin-left: 8px; } + +/* ------------------------------------------------------------------ */ +/* Settings panel overlay (Phase 6) */ +/* ------------------------------------------------------------------ */ +#settings-overlay { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + z-index: 1000; +} + +#settings-panel { + position: absolute; + top: 140px; + left: 340px; + width: 600px; + height: 260px; + background-color: #252540; + border-width: 2px; + border-color: #e94560; + border-radius: 12px; + padding: 24px; +} + +#settings-title { + font-size: 28px; + color: #e94560; + text-align: center; + margin-bottom: 24px; +} + +#settings-controls { + display: block; + margin-left: 40px; +} + +#settings-row { + display: block; + margin-bottom: 20px; + height: 40px; +} + +#settings-label { + display: inline-block; + font-size: 20px; + color: #eaeaea; + width: 140px; + text-align: right; + margin-right: 10px; + vertical-align: middle; +} + +#settings-val { + display: inline-block; + font-size: 20px; + color: #e94560; + width: 80px; + text-align: center; + vertical-align: middle; +} + +.settings-btn { + display: inline-block; + width: 36px; + height: 36px; + border-width: 2px; + border-color: #8c8ca0; + border-radius: 18px; + background-color: transparent; + color: #eaeaea; + font-size: 20px; + text-align: center; + cursor: pointer; + vertical-align: middle; +} + +.settings-btn:hover { + border-color: #e94560; + background-color: rgba(255, 255, 255, 0.06); +} diff --git a/game/study_player/ui/main.rml b/game/study_player/ui/main.rml index f19f45c..5ef6c19 100644 --- a/game/study_player/ui/main.rml +++ b/game/study_player/ui/main.rml @@ -62,5 +62,28 @@ <span id="study-label">Study Mode</span> </div> </div> + + <!-- ================================================================ --> + <!-- Settings panel overlay (Phase 6) — F2 to toggle --> + <!-- ================================================================ --> + <div id="settings-overlay" data-if="settings_visible"> + <div id="settings-panel"> + <div id="settings-title">⚙ Settings</div> + <div id="settings-controls"> + <div id="settings-row"> + <span id="settings-label">Font Scale:</span> + <button class="settings-btn" data-event-click="font_down()">−</button> + <span id="settings-val">{{font_scale_display}}</span> + <button class="settings-btn" data-event-click="font_up()">+</button> + </div> + <div id="settings-row"> + <span id="settings-label">Volume:</span> + <button class="settings-btn" data-event-click="vol_down()">−</button> + <span id="settings-val">{{volume_display}}</span> + <button class="settings-btn" data-event-click="vol_up()">+</button> + </div> + </div> + </div> + </div> </body> </rml> |
