UI pass: medieval-warm Theme + real Build drawer thumbnails

Theme (scenes/ui/medieval_theme.gd):
- Procedural builder for an app-wide Theme: parchment buttons on
  dark-wood border, tan panels, ink text, gold focus ring.
- Applied on the root Window in Main._ready, with a tree-walk that
  re-seeds every CanvasLayer's topmost Control (CanvasLayer
  interrupts the root-Window theme cascade in Godot 4).
- Late-mounting popups + modals get themed via a child_entered_tree
  hook on each CanvasLayer.

Build drawer thumbnails (scenes/ui/build_drawer_thumb.gd):
- New BuildDrawerThumb Control that dispatches on tool_id and draws
  a recognisable silhouette of the entity each tool builds. 17 tools
  covered: chop/mine/dig_grave/no_roof (Designate), stone+wood walls,
  wood+stone floors, door, crate, bed, torch, 5 workbenches
  (Carpenter/Smelter/Millstone/Hearth/Cremation Pyre), stockpile,
  graveyard.
- Replaces the flat ColorRect placeholder. _add_tool_btn signature
  changed from (label, color, callback) to (label, tool_id, callback).
This commit is contained in:
megaproxy 2026-05-16 16:09:56 +01:00
parent 413054157a
commit 53cb92041c
4 changed files with 614 additions and 30 deletions

198
scenes/ui/medieval_theme.gd Normal file
View file

@ -0,0 +1,198 @@
class_name MedievalTheme extends RefCounted
## Builds a Theme resource implementing the "Medieval warm" palette:
## Panel: tan (198, 168, 128)
## Border: dark wood (90, 55, 30)
## Button: parchment (230, 210, 170), hover lighter, pressed inset shadow
## Text: ink black with off-white on dark panels
##
## Applied on Main scene root so it cascades to every Control descendant.
## Per-panel overrides are still possible — only nodes that don't explicitly
## override a style pick up the global Theme.
# ── palette ──────────────────────────────────────────────────────────────────
const C_PANEL := Color(0.776, 0.659, 0.502) # tan
const C_PANEL_DARK := Color(0.353, 0.216, 0.118) # dark wood
const C_BUTTON := Color(0.902, 0.824, 0.667) # parchment
const C_BUTTON_HOV := Color(0.961, 0.902, 0.769) # warm parchment
const C_BUTTON_PRESS:= Color(0.776, 0.694, 0.529) # pressed shadow
const C_BUTTON_DIS := Color(0.690, 0.620, 0.510) # disabled
const C_INK := Color(0.106, 0.078, 0.039)
const C_INK_DIM := Color(0.353, 0.275, 0.196)
const C_ACCENT := Color(0.580, 0.180, 0.110) # wax-seal red, for selected tabs
const C_ACCENT_GOLD := Color(0.831, 0.620, 0.149)
static func build() -> Theme:
var theme := Theme.new()
# Default font size — small UI, but legible on phone.
theme.default_font_size = 14
# ── Button (drives OptionButton + MenuButton too) ─────────────────────────
theme.set_stylebox("normal", "Button", _btn_box(C_BUTTON, false))
theme.set_stylebox("hover", "Button", _btn_box(C_BUTTON_HOV, false))
theme.set_stylebox("pressed", "Button", _btn_box(C_BUTTON_PRESS, true))
theme.set_stylebox("disabled", "Button", _btn_box(C_BUTTON_DIS, false))
theme.set_stylebox("focus", "Button", _focus_box())
theme.set_color("font_color", "Button", C_INK)
theme.set_color("font_hover_color", "Button", C_INK)
theme.set_color("font_pressed_color", "Button", C_INK)
theme.set_color("font_disabled_color", "Button", C_INK_DIM)
theme.set_constant("h_separation", "Button", 6)
# OptionButton inherits Button styling automatically via class fallback in
# Godot, but we set explicit copies in case overrides land.
theme.set_stylebox("normal", "OptionButton", _btn_box(C_BUTTON, false))
theme.set_stylebox("hover", "OptionButton", _btn_box(C_BUTTON_HOV, false))
theme.set_stylebox("pressed", "OptionButton", _btn_box(C_BUTTON_PRESS, true))
theme.set_stylebox("focus", "OptionButton", _focus_box())
theme.set_color("font_color", "OptionButton", C_INK)
# ── CheckBox ──────────────────────────────────────────────────────────────
theme.set_color("font_color", "CheckBox", C_INK)
theme.set_color("font_hover_color", "CheckBox", C_INK)
theme.set_color("font_pressed_color", "CheckBox", C_INK)
# ── PanelContainer ────────────────────────────────────────────────────────
theme.set_stylebox("panel", "PanelContainer", _panel_box())
# ── Panel (raw) — same look as PanelContainer ─────────────────────────────
theme.set_stylebox("panel", "Panel", _panel_box())
# ── PopupMenu (recipe picker) ─────────────────────────────────────────────
theme.set_stylebox("panel", "PopupMenu", _panel_box())
theme.set_stylebox("hover", "PopupMenu", _btn_box(C_BUTTON_HOV, false))
theme.set_color("font_color", "PopupMenu", C_INK)
theme.set_color("font_hover_color", "PopupMenu", C_INK)
# ── Label — defaults to ink-on-tan; per-label modulate still works ────────
theme.set_color("font_color", "Label", C_INK)
# ── SpinBox / LineEdit ────────────────────────────────────────────────────
theme.set_stylebox("normal", "LineEdit", _btn_box(C_BUTTON, true))
theme.set_stylebox("focus", "LineEdit", _focus_box())
theme.set_color("font_color", "LineEdit", C_INK)
theme.set_color("caret_color", "LineEdit", C_INK)
# ── Slider (audio sliders in SettingsMenu) ────────────────────────────────
theme.set_stylebox("slider", "HSlider", _slider_track())
theme.set_stylebox("grabber_area", "HSlider", _slider_fill())
theme.set_stylebox("grabber_area_highlight", "HSlider", _slider_fill())
# ── ScrollContainer scrollbar ─────────────────────────────────────────────
theme.set_stylebox("scroll", "VScrollBar", _scrollbar_track())
theme.set_stylebox("grabber", "VScrollBar", _btn_box(C_PANEL_DARK, false))
theme.set_stylebox("grabber_pressed","VScrollBar", _btn_box(C_PANEL_DARK, true))
# ── HSeparator / VSeparator (used between bill rows in WorkbenchPanel) ────
var sep := StyleBoxFlat.new()
sep.bg_color = C_PANEL_DARK
sep.content_margin_top = 1
sep.content_margin_bottom = 1
theme.set_stylebox("separator", "HSeparator", sep)
theme.set_stylebox("separator", "VSeparator", sep)
return theme
# ── helpers ──────────────────────────────────────────────────────────────────
static func _btn_box(fill: Color, pressed: bool) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = fill
s.border_color = C_PANEL_DARK
s.border_width_left = 1
s.border_width_right = 1
s.border_width_top = 1
s.border_width_bottom = 1
s.corner_radius_top_left = 4
s.corner_radius_top_right = 4
s.corner_radius_bottom_left = 4
s.corner_radius_bottom_right = 4
if pressed:
# Inset shadow on top + left (pressed-in look).
s.shadow_color = Color(0, 0, 0, 0.35)
s.shadow_size = 0
# Visually offset content slightly down/right when pressed.
s.content_margin_top = 4
s.content_margin_left = 7
s.content_margin_right = 5
s.content_margin_bottom = 2
else:
# Subtle drop shadow for depth.
s.shadow_color = Color(0, 0, 0, 0.20)
s.shadow_size = 2
s.shadow_offset = Vector2(0, 1)
s.content_margin_top = 3
s.content_margin_left = 6
s.content_margin_right = 6
s.content_margin_bottom = 3
return s
static func _panel_box() -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = C_PANEL
s.border_color = C_PANEL_DARK
s.border_width_left = 2
s.border_width_right = 2
s.border_width_top = 2
s.border_width_bottom = 2
s.corner_radius_top_left = 6
s.corner_radius_top_right = 6
s.corner_radius_bottom_left = 6
s.corner_radius_bottom_right = 6
s.shadow_color = Color(0, 0, 0, 0.35)
s.shadow_size = 4
s.shadow_offset = Vector2(0, 2)
s.content_margin_top = 8
s.content_margin_left = 10
s.content_margin_right = 10
s.content_margin_bottom = 8
return s
static func _focus_box() -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = Color(0, 0, 0, 0) # transparent
s.border_color = C_ACCENT_GOLD
s.border_width_left = 2
s.border_width_right = 2
s.border_width_top = 2
s.border_width_bottom = 2
s.corner_radius_top_left = 4
s.corner_radius_top_right = 4
s.corner_radius_bottom_left = 4
s.corner_radius_bottom_right = 4
return s
static func _slider_track() -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = C_PANEL_DARK
s.corner_radius_top_left = 4
s.corner_radius_top_right = 4
s.corner_radius_bottom_left = 4
s.corner_radius_bottom_right = 4
s.content_margin_top = 4
s.content_margin_bottom = 4
return s
static func _slider_fill() -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = C_ACCENT_GOLD
s.corner_radius_top_left = 4
s.corner_radius_top_right = 4
s.corner_radius_bottom_left = 4
s.corner_radius_bottom_right = 4
return s
static func _scrollbar_track() -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = Color(C_PANEL_DARK.r, C_PANEL_DARK.g, C_PANEL_DARK.b, 0.3)
s.corner_radius_top_left = 3
s.corner_radius_top_right = 3
s.corner_radius_bottom_left = 3
s.corner_radius_bottom_right = 3
return s