|
extends Node |
|
|
|
### |
|
# |
|
# Responsive |
|
# |
|
# The "media query" engine. This is the single place where layout breakpoints |
|
# live. It watches the window size and derives a LayoutMode. Whenever the mode |
|
# changes it emits SignalBus.layout_changed; |
|
# UI scripts react by reading Responsive.current_mode and reconfiguring |
|
# themselves (column counts, margins, font sizes, etc.). |
|
# |
|
# Register this script as an autoloader. Preferred name: Responsive |
|
# |
|
### |
|
|
|
enum LayoutMode { |
|
PHONE, |
|
DESKTOP, |
|
} |
|
|
|
const PHONE_MAX_WIDTH := 900.0 |
|
|
|
var current_mode: int = LayoutMode.DESKTOP: |
|
set(value): |
|
if value == current_mode: |
|
return |
|
current_mode = value |
|
SignalBus.layout_changed.emit() |
|
apply_theme_changes() |
|
|
|
|
|
### |
|
# |
|
# Override values ("the media-query rules") |
|
# |
|
# OVERRIDES is the single source of truth for every override VALUE. There is no |
|
# desktop row: the desktop value is whatever the .tscn authored, captured from |
|
# the live node on first apply (see ResponsiveScene). |
|
# |
|
# Two kinds of keys live here together: |
|
# |
|
# • node-bound keys — referenced by a scene coordinator's bindings() that wires |
|
# them to a concrete node + property (see main_menu_layout). |
|
# • global keys — read directly here, e.g. style_button_standard_fontsize |
|
# in apply_theme_changes(). |
|
# |
|
### |
|
|
|
const regular_fontsize : int = 28 |
|
|
|
const OVERRIDES := { |
|
LayoutMode.PHONE: { |
|
"style_label_normal_fontsize": 24, |
|
"style_button_standard_fontsize": regular_fontsize, |
|
"style_richtext_normal_fontsize": regular_fontsize, |
|
"style_richtext_bold_fontsize": regular_fontsize, |
|
"style_richtext_italics_fontsize": regular_fontsize, |
|
"style_richtext_bold_italics_fontsize": regular_fontsize, |
|
"style_richtext_mono_fontsize": regular_fontsize, |
|
|
|
"button_content_margin_h": 24, |
|
"button_content_margin_v": 20, |
|
|
|
"mainmenu_button_large_fontsize": 34, |
|
"mainmenu_primaryactions_sizeflagshorizontal": Control.SizeFlags.SIZE_EXPAND_FILL, |
|
}, |
|
} |
|
|
|
func has_override(key) -> bool: |
|
return OVERRIDES.get(current_mode, {}).has(key) |
|
|
|
func get_override(key): |
|
return OVERRIDES[current_mode][key] |
|
|
|
|
|
# Theme-level bindings — the global analog of a scene coordinator's bindings(). |
|
# Maps each theme-targeting OVERRIDES key to the project-theme item it drives |
|
# (data type + theme type + item name). Desktop defaults are captured generically |
|
# at startup, so adding a theme override = one row here + its phone value in |
|
# OVERRIDES, with no per-value capture/apply code. |
|
const THEME_BINDINGS := { |
|
"style_button_standard_fontsize": { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "Button", "name": "font_size", |
|
}, |
|
"style_richtext_normal_fontsize" : { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "RichTextLabel", "name": "normal_font_size", |
|
}, |
|
"style_richtext_mono_fontsize" : { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "RichTextLabel", "name": "mono_font_size", |
|
}, |
|
"style_richtext_bold_fontsize" : { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "RichTextLabel", "name": "bold_font_size", |
|
}, |
|
"style_richtext_italics_fontsize" : { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "RichTextLabel", "name": "italics_font_size", |
|
}, |
|
"style_richtext_bold_italics_fontsize" : { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "RichTextLabel", "name": "bold_italics_font_size", |
|
}, |
|
"style_label_normal_fontsize" : { |
|
"data_type": Theme.DATA_TYPE_FONT_SIZE, "theme_type": "Label", "name": "font_size", |
|
} |
|
} |
|
|
|
var _theme_defaults := {} |
|
|
|
|
|
# Stylebox bindings — the StyleBox analog of THEME_BINDINGS. A StyleBox is a |
|
# Resource, not a scalar theme item, so its properties can't go through |
|
# set_theme_item(); they're set on the stylebox itself. Each row maps an OVERRIDES |
|
# key to a set of styleboxes + the properties it drives, applying one value to every |
|
# (stylebox, property) pair via the generic Object get/set. A scalar property is just |
|
# a single-element "properties" list; sided properties (margins, corner radii) list |
|
# the sides that should move together. |
|
# |
|
# Caveat: properties like content_margin_*, corner_radius_*, bg_color only exist on |
|
# StyleBoxFlat — don't point a binding at a box that lacks the property (set() no-ops |
|
# and get() returns null, poisoning the captured default). has_stylebox guards a |
|
# missing box, not a missing property. |
|
const STYLEBOX_BINDINGS := { |
|
"button_content_margin_h": { |
|
"theme_type": "Button", |
|
"styleboxes": ["normal", "hover", "pressed", "hover_pressed", "focus"], |
|
"properties": ["content_margin_left", "content_margin_right"], |
|
}, |
|
"button_content_margin_v": { |
|
"theme_type": "Button", |
|
"styleboxes": ["normal", "hover", "pressed", "hover_pressed", "focus"], |
|
"properties": ["content_margin_top", "content_margin_bottom"], |
|
}, |
|
} |
|
|
|
# Nested snapshot of authored values: key -> stylebox_name -> property -> value. |
|
var _stylebox_defaults := {} |
|
|
|
|
|
func _ready() -> void: |
|
_capture_theme_defaults() |
|
_capture_stylebox_defaults() |
|
get_tree().root.size_changed.connect(_on_size_changed) |
|
current_mode = _compute_mode() |
|
|
|
|
|
# Snapshot each bound theme item's authored value before any override is applied, |
|
# so widening back restores the .tres default (mirrors ResponsiveScene capture). |
|
func _capture_theme_defaults() -> void: |
|
var theme := ThemeDB.get_project_theme() |
|
if theme == null: |
|
return |
|
for key in THEME_BINDINGS: |
|
var b = THEME_BINDINGS[key] |
|
_theme_defaults[key] = theme.get_theme_item(b.data_type, b.name, b.theme_type) |
|
|
|
|
|
# Snapshot each bound stylebox's authored property values. We must NOT store the |
|
# StyleBox reference: it's a shared resource we mutate in place, so a captured |
|
# reference would track the live value and couldn't restore the .tres default. |
|
func _capture_stylebox_defaults() -> void: |
|
var theme := ThemeDB.get_project_theme() |
|
if theme == null: |
|
return |
|
for key in STYLEBOX_BINDINGS: |
|
var b = STYLEBOX_BINDINGS[key] |
|
var per_box := {} |
|
for box_name in b.styleboxes: |
|
if not theme.has_stylebox(box_name, b.theme_type): |
|
continue |
|
var sb := theme.get_stylebox(box_name, b.theme_type) |
|
var per_prop := {} |
|
for prop in b.properties: |
|
per_prop[prop] = sb.get(prop) |
|
per_box[box_name] = per_prop |
|
_stylebox_defaults[key] = per_box |
|
|
|
|
|
func _on_size_changed() -> void: |
|
var new_mode := _compute_mode() |
|
if new_mode == current_mode: |
|
return |
|
current_mode = new_mode |
|
|
|
|
|
func _compute_mode() -> int: |
|
var window = get_window() |
|
var size = window.size |
|
var width = size.x |
|
|
|
if width < PHONE_MAX_WIDTH: |
|
return LayoutMode.PHONE |
|
|
|
if is_phone(): |
|
return LayoutMode.PHONE |
|
|
|
return LayoutMode.DESKTOP |
|
|
|
|
|
func is_phone() -> bool: |
|
# First gate, use Godots built in reporting - but that includes tablets. |
|
if not OS.has_feature("mobile"): |
|
return false |
|
# Second gate, check the screens short side by looking at resolution and DPI. |
|
# This should remove (most) tablets. |
|
return get_short_side_inches() < 3.5 |
|
|
|
|
|
func get_short_side_inches() -> float: |
|
var size_px = DisplayServer.screen_get_size() |
|
var dpi = DisplayServer.screen_get_dpi() |
|
return minf(size_px.x, size_px.y) / float(dpi) |
|
|
|
|
|
### |
|
# |
|
# Theme changes |
|
# |
|
### |
|
func apply_theme_changes() -> void: |
|
var theme := ThemeDB.get_project_theme() |
|
if theme == null: |
|
return |
|
for key in THEME_BINDINGS: |
|
var b = THEME_BINDINGS[key] |
|
var value = get_override(key) if has_override(key) else _theme_defaults[key] |
|
theme.set_theme_item(b.data_type, b.name, b.theme_type, value) |
|
_apply_stylebox_properties(theme) |
|
|
|
|
|
# StyleBox properties can't go through set_theme_item (they live on the resource, not |
|
# the theme dict). For each binding, push the mode override onto every |
|
# (stylebox, property), or restore the captured .tres default. Mutating the stylebox |
|
# in place is enough — it's the same resource every Button renders from. |
|
func _apply_stylebox_properties(theme: Theme) -> void: |
|
for key in STYLEBOX_BINDINGS: |
|
var b = STYLEBOX_BINDINGS[key] |
|
var overriding := has_override(key) |
|
var override_value = get_override(key) if overriding else null |
|
for box_name in b.styleboxes: |
|
if not theme.has_stylebox(box_name, b.theme_type): |
|
continue |
|
var sb := theme.get_stylebox(box_name, b.theme_type) |
|
for prop in b.properties: |
|
var value = override_value if overriding else _stylebox_defaults[key][box_name][prop] |
|
sb.set(prop, value) |