Add persistent light and dark themes
This commit is contained in:
@@ -18,6 +18,7 @@ config/icon="res://icon.svg"
|
|||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
AuthState="*res://scripts/autoload/auth_state.gd"
|
AuthState="*res://scripts/autoload/auth_state.gd"
|
||||||
|
UIThemeService="*res://scripts/autoload/ui_theme_service.gd"
|
||||||
UISoundService="*res://scripts/autoload/ui_sound_service.gd"
|
UISoundService="*res://scripts/autoload/ui_sound_service.gd"
|
||||||
QuestionBankService="*res://scripts/autoload/question_bank_service.gd"
|
QuestionBankService="*res://scripts/autoload/question_bank_service.gd"
|
||||||
ProgressionService="*res://scripts/autoload/progression_service.gd"
|
ProgressionService="*res://scripts/autoload/progression_service.gd"
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
signal theme_changed(is_dark: bool)
|
||||||
|
|
||||||
|
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||||
|
const SETTINGS_PATH := "user://settings.cfg"
|
||||||
|
const SETTINGS_SECTION := "appearance"
|
||||||
|
const SETTINGS_KEY := "color_scheme"
|
||||||
|
|
||||||
|
var _is_dark := true
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
_is_dark = _load_preference()
|
||||||
|
Style.set_dark_mode(_is_dark)
|
||||||
|
|
||||||
|
|
||||||
|
func is_dark_mode() -> bool:
|
||||||
|
return _is_dark
|
||||||
|
|
||||||
|
|
||||||
|
func set_dark_mode(value: bool) -> void:
|
||||||
|
if _is_dark == value:
|
||||||
|
return
|
||||||
|
_is_dark = value
|
||||||
|
Style.set_dark_mode(_is_dark)
|
||||||
|
_save_preference()
|
||||||
|
theme_changed.emit(_is_dark)
|
||||||
|
|
||||||
|
|
||||||
|
func toggle_theme() -> void:
|
||||||
|
set_dark_mode(not _is_dark)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_preference() -> bool:
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
if config.load(SETTINGS_PATH) == OK:
|
||||||
|
var saved := str(config.get_value(SETTINGS_SECTION, SETTINGS_KEY, ""))
|
||||||
|
if saved == "dark" or saved == "light":
|
||||||
|
return saved == "dark"
|
||||||
|
return _system_prefers_dark()
|
||||||
|
|
||||||
|
|
||||||
|
func _system_prefers_dark() -> bool:
|
||||||
|
if OS.has_feature("web"):
|
||||||
|
var result: Variant = JavaScriptBridge.eval(
|
||||||
|
"window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return bool(result)
|
||||||
|
if DisplayServer.is_dark_mode_supported():
|
||||||
|
return DisplayServer.is_dark_mode()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _save_preference() -> void:
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
config.load(SETTINGS_PATH)
|
||||||
|
config.set_value(SETTINGS_SECTION, SETTINGS_KEY, "dark" if _is_dark else "light")
|
||||||
|
if config.save(SETTINGS_PATH) != OK:
|
||||||
|
push_warning("Unable to save color scheme preference.")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dgfxwfgg7bapl
|
||||||
@@ -9,11 +9,15 @@ var user_label: Label
|
|||||||
var app_menu: PanelContainer
|
var app_menu: PanelContainer
|
||||||
var menu_exit_button: Button
|
var menu_exit_button: Button
|
||||||
var sound_toggle: CheckButton
|
var sound_toggle: CheckButton
|
||||||
|
var theme_button: Button
|
||||||
|
var theme_service
|
||||||
var current_screen_name := ""
|
var current_screen_name := ""
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
theme_service = get_node("/root/UIThemeService")
|
||||||
Style.apply_app_theme(self)
|
Style.apply_app_theme(self)
|
||||||
_build_ui()
|
_build_ui()
|
||||||
|
theme_service.theme_changed.connect(_on_theme_changed)
|
||||||
resized.connect(_update_responsive_layout)
|
resized.connect(_update_responsive_layout)
|
||||||
_update_responsive_layout()
|
_update_responsive_layout()
|
||||||
show_screen("selection" if AuthState.is_logged_in else "login", "exam")
|
show_screen("selection" if AuthState.is_logged_in else "login", "exam")
|
||||||
@@ -92,6 +96,14 @@ func _build_ui() -> void:
|
|||||||
user_label.gui_input.connect(_on_user_label_gui_input)
|
user_label.gui_input.connect(_on_user_label_gui_input)
|
||||||
header.add_child(user_label)
|
header.add_child(user_label)
|
||||||
|
|
||||||
|
theme_button = Button.new()
|
||||||
|
theme_button.custom_minimum_size = Vector2(44, 44)
|
||||||
|
theme_button.add_theme_font_size_override("font_size", 22)
|
||||||
|
UISoundService.set_button_sound(theme_button, "toggle")
|
||||||
|
theme_button.pressed.connect(theme_service.toggle_theme)
|
||||||
|
header.add_child(theme_button)
|
||||||
|
_refresh_theme_button()
|
||||||
|
|
||||||
var screens_container := MarginContainer.new()
|
var screens_container := MarginContainer.new()
|
||||||
screens_container.name = "Screens"
|
screens_container.name = "Screens"
|
||||||
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
@@ -176,6 +188,22 @@ func _refresh_header_stats() -> void:
|
|||||||
user_label.text = AuthState.username if AuthState.is_logged_in else ""
|
user_label.text = AuthState.username if AuthState.is_logged_in else ""
|
||||||
|
|
||||||
|
|
||||||
|
func _on_theme_changed(is_dark_mode: bool) -> void:
|
||||||
|
Style.refresh_tree(self, not is_dark_mode)
|
||||||
|
_refresh_theme_button()
|
||||||
|
var player = screens.get("player")
|
||||||
|
if player != null and player.has_method("refresh_theme_controls"):
|
||||||
|
player.refresh_theme_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_theme_button() -> void:
|
||||||
|
if theme_button == null:
|
||||||
|
return
|
||||||
|
theme_button.text = "☀" if theme_service.is_dark_mode() else "☾"
|
||||||
|
theme_button.tooltip_text = "Switch to light mode" if theme_service.is_dark_mode() else "Switch to dark mode"
|
||||||
|
Style.style_chip(theme_button, Style.CYAN)
|
||||||
|
|
||||||
|
|
||||||
func _logout() -> void:
|
func _logout() -> void:
|
||||||
_close_app_menu()
|
_close_app_menu()
|
||||||
if BlockSessionService.block_started:
|
if BlockSessionService.block_started:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const REPORT_CATEGORIES := [
|
|||||||
]
|
]
|
||||||
|
|
||||||
var main: Control
|
var main: Control
|
||||||
|
var theme_service
|
||||||
var choice_buttons: Array[Button] = []
|
var choice_buttons: Array[Button] = []
|
||||||
var choice_percent_labels: Array[Label] = []
|
var choice_percent_labels: Array[Label] = []
|
||||||
var choice_top_labels: Array[Label] = []
|
var choice_top_labels: Array[Label] = []
|
||||||
@@ -55,6 +56,7 @@ var text_larger_button: Button
|
|||||||
var labs_button: Button
|
var labs_button: Button
|
||||||
var flag_button: Button
|
var flag_button: Button
|
||||||
var report_button: Button
|
var report_button: Button
|
||||||
|
var theme_button: Button
|
||||||
var question_toolbar: HBoxContainer
|
var question_toolbar: HBoxContainer
|
||||||
var nav_palette: HBoxContainer
|
var nav_palette: HBoxContainer
|
||||||
var question_content: VBoxContainer
|
var question_content: VBoxContainer
|
||||||
@@ -86,6 +88,7 @@ var shake_time := 0.0
|
|||||||
var shake_origin := Vector2.ZERO
|
var shake_origin := Vector2.ZERO
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
theme_service = get_node("/root/UIThemeService")
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
add_theme_constant_override("separation", 10)
|
add_theme_constant_override("separation", 10)
|
||||||
if not QuestionBankService.choice_percentages_updated.is_connected(_on_choice_percentages_updated):
|
if not QuestionBankService.choice_percentages_updated.is_connected(_on_choice_percentages_updated):
|
||||||
@@ -131,6 +134,14 @@ func _ready() -> void:
|
|||||||
question_toolbar.add_theme_constant_override("separation", 8)
|
question_toolbar.add_theme_constant_override("separation", 8)
|
||||||
question_toolbar.alignment = BoxContainer.ALIGNMENT_END
|
question_toolbar.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
|
||||||
|
theme_button = Button.new()
|
||||||
|
theme_button.custom_minimum_size = Vector2(44, 44)
|
||||||
|
theme_button.add_theme_font_size_override("font_size", 22)
|
||||||
|
UISoundService.set_button_sound(theme_button, "toggle")
|
||||||
|
theme_button.pressed.connect(theme_service.toggle_theme)
|
||||||
|
question_toolbar.add_child(theme_button)
|
||||||
|
_refresh_theme_button()
|
||||||
|
|
||||||
text_smaller_button = Button.new()
|
text_smaller_button = Button.new()
|
||||||
text_smaller_button.text = "A-"
|
text_smaller_button.text = "A-"
|
||||||
text_smaller_button.tooltip_text = "Decrease text size"
|
text_smaller_button.tooltip_text = "Decrease text size"
|
||||||
@@ -622,12 +633,12 @@ func _submit_or_next() -> void:
|
|||||||
if current_answer.is_empty():
|
if current_answer.is_empty():
|
||||||
if selected_choice == "":
|
if selected_choice == "":
|
||||||
return
|
return
|
||||||
|
var question_id_for_attempt := str(BlockSessionService.get_current_question().get("id", ""))
|
||||||
if BlockSessionService.session_kind == "core":
|
if BlockSessionService.session_kind == "core":
|
||||||
_collapse_resolved_followups()
|
_collapse_resolved_followups()
|
||||||
var attempted_choice := selected_choice
|
var attempted_choice := selected_choice
|
||||||
var attempted_question_id := str(BlockSessionService.get_current_question().get("id", ""))
|
|
||||||
QuestionBankService.record_first_attempt(
|
QuestionBankService.record_first_attempt(
|
||||||
attempted_question_id,
|
question_id_for_attempt,
|
||||||
attempted_choice,
|
attempted_choice,
|
||||||
_presented_choice_ids()
|
_presented_choice_ids()
|
||||||
)
|
)
|
||||||
@@ -649,9 +660,8 @@ func _submit_or_next() -> void:
|
|||||||
_trigger_wrong_feedback()
|
_trigger_wrong_feedback()
|
||||||
_apply_saved_choice_state()
|
_apply_saved_choice_state()
|
||||||
return
|
return
|
||||||
var attempted_question_id := str(BlockSessionService.get_current_question().get("id", ""))
|
|
||||||
QuestionBankService.record_first_attempt(
|
QuestionBankService.record_first_attempt(
|
||||||
attempted_question_id,
|
question_id_for_attempt,
|
||||||
selected_choice,
|
selected_choice,
|
||||||
_presented_choice_ids()
|
_presented_choice_ids()
|
||||||
)
|
)
|
||||||
@@ -742,7 +752,7 @@ func _trigger_wrong_feedback() -> void:
|
|||||||
green_fog_time = 0.0
|
green_fog_time = 0.0
|
||||||
red_fog_time = FEEDBACK_FADE_SECONDS
|
red_fog_time = FEEDBACK_FADE_SECONDS
|
||||||
if feedback_fog != null:
|
if feedback_fog != null:
|
||||||
feedback_fog.color = Color(1.0, 0.0, 0.0, 0.26)
|
feedback_fog.color = Color(1.0, 0.0, 0.0, _feedback_overlay_alpha(false))
|
||||||
|
|
||||||
|
|
||||||
func _trigger_correct_feedback() -> void:
|
func _trigger_correct_feedback() -> void:
|
||||||
@@ -757,7 +767,7 @@ func _trigger_correct_feedback() -> void:
|
|||||||
for button in choice_buttons:
|
for button in choice_buttons:
|
||||||
button.disabled = true
|
button.disabled = true
|
||||||
if feedback_fog != null:
|
if feedback_fog != null:
|
||||||
feedback_fog.color = Color(0.25, 1.0, 0.25, 0.23)
|
feedback_fog.color = Color(0.25, 1.0, 0.25, _feedback_overlay_alpha(true))
|
||||||
|
|
||||||
|
|
||||||
func _update_answer_feedback(delta: float) -> void:
|
func _update_answer_feedback(delta: float) -> void:
|
||||||
@@ -770,11 +780,11 @@ func _update_answer_feedback(delta: float) -> void:
|
|||||||
|
|
||||||
if red_fog_time > 0.0 and feedback_fog != null:
|
if red_fog_time > 0.0 and feedback_fog != null:
|
||||||
red_fog_time = max(0.0, red_fog_time - delta)
|
red_fog_time = max(0.0, red_fog_time - delta)
|
||||||
var red_alpha := 0.26 * (red_fog_time / FEEDBACK_FADE_SECONDS)
|
var red_alpha := _feedback_overlay_alpha(false) * (red_fog_time / FEEDBACK_FADE_SECONDS)
|
||||||
feedback_fog.color = Color(1.0, 0.0, 0.0, red_alpha)
|
feedback_fog.color = Color(1.0, 0.0, 0.0, red_alpha)
|
||||||
elif green_fog_time > 0.0 and feedback_fog != null:
|
elif green_fog_time > 0.0 and feedback_fog != null:
|
||||||
green_fog_time = max(0.0, green_fog_time - delta)
|
green_fog_time = max(0.0, green_fog_time - delta)
|
||||||
var green_alpha := 0.23 * (green_fog_time / FEEDBACK_FADE_SECONDS)
|
var green_alpha := _feedback_overlay_alpha(true) * (green_fog_time / FEEDBACK_FADE_SECONDS)
|
||||||
feedback_fog.color = Color(0.25, 1.0, 0.25, green_alpha)
|
feedback_fog.color = Color(0.25, 1.0, 0.25, green_alpha)
|
||||||
|
|
||||||
if pending_correct_advance:
|
if pending_correct_advance:
|
||||||
@@ -787,6 +797,12 @@ func _update_answer_feedback(delta: float) -> void:
|
|||||||
_advance_or_finish()
|
_advance_or_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _feedback_overlay_alpha(is_correct: bool) -> float:
|
||||||
|
if Style.is_dark:
|
||||||
|
return 0.23 if is_correct else 0.26
|
||||||
|
return 0.07
|
||||||
|
|
||||||
|
|
||||||
func _reset_wrong_feedback() -> void:
|
func _reset_wrong_feedback() -> void:
|
||||||
if shake_time > 0.0:
|
if shake_time > 0.0:
|
||||||
position = shake_origin
|
position = shake_origin
|
||||||
@@ -853,6 +869,7 @@ func _toggle_report_current_question() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _refresh_toolbar_state() -> void:
|
func _refresh_toolbar_state() -> void:
|
||||||
|
_refresh_theme_button()
|
||||||
_refresh_labs_button()
|
_refresh_labs_button()
|
||||||
if flag_button != null:
|
if flag_button != null:
|
||||||
Style.style_chip(flag_button, Style.AMBER if flagged else Style.CYAN)
|
Style.style_chip(flag_button, Style.AMBER if flagged else Style.CYAN)
|
||||||
@@ -862,6 +879,30 @@ func _refresh_toolbar_state() -> void:
|
|||||||
report_button.tooltip_text = "Report submitted; report another issue" if reported_question_ids.has(question_id) else "Report question"
|
report_button.tooltip_text = "Report submitted; report another issue" if reported_question_ids.has(question_id) else "Report question"
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_theme_controls() -> void:
|
||||||
|
_refresh_theme_button()
|
||||||
|
if labs_button != null:
|
||||||
|
labs_button.icon = _make_labs_icon()
|
||||||
|
if flag_button != null:
|
||||||
|
flag_button.icon = _make_flag_icon()
|
||||||
|
if report_button != null:
|
||||||
|
report_button.icon = _make_report_icon()
|
||||||
|
if question_toolbar != null:
|
||||||
|
for child in question_toolbar.get_children():
|
||||||
|
if child is Button and child.tooltip_text == "Clear highlights":
|
||||||
|
child.icon = _make_clear_highlights_icon()
|
||||||
|
_refresh_toolbar_state()
|
||||||
|
_apply_saved_choice_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_theme_button() -> void:
|
||||||
|
if theme_button == null:
|
||||||
|
return
|
||||||
|
theme_button.text = "☀" if theme_service.is_dark_mode() else "☾"
|
||||||
|
theme_button.tooltip_text = "Switch to light mode" if theme_service.is_dark_mode() else "Switch to dark mode"
|
||||||
|
Style.style_chip(theme_button, Style.CYAN)
|
||||||
|
|
||||||
|
|
||||||
func _refresh_player_stats() -> void:
|
func _refresh_player_stats() -> void:
|
||||||
if player_stats_label == null:
|
if player_stats_label == null:
|
||||||
return
|
return
|
||||||
@@ -895,6 +936,7 @@ func _set_scaled_button_font(button: Button, base_size: int) -> void:
|
|||||||
|
|
||||||
func _set_scaled_rich_text(label: RichTextLabel, base_size: int) -> void:
|
func _set_scaled_rich_text(label: RichTextLabel, base_size: int) -> void:
|
||||||
label.set_meta("question_base_font_size", base_size)
|
label.set_meta("question_base_font_size", base_size)
|
||||||
|
label.add_theme_color_override("default_color", Style.TEXT)
|
||||||
label.add_theme_font_size_override("normal_font_size", _scaled_font_size(base_size))
|
label.add_theme_font_size_override("normal_font_size", _scaled_font_size(base_size))
|
||||||
label.add_theme_font_size_override("bold_font_size", _scaled_font_size(base_size))
|
label.add_theme_font_size_override("bold_font_size", _scaled_font_size(base_size))
|
||||||
|
|
||||||
@@ -1493,7 +1535,7 @@ func _apply_saved_choice_state() -> void:
|
|||||||
choice_buttons[index].disabled = locked_wrong_choice_ids.has(id) and not has_followup_panel
|
choice_buttons[index].disabled = locked_wrong_choice_ids.has(id) and not has_followup_panel
|
||||||
_style_choice_button_for_state(index, id)
|
_style_choice_button_for_state(index, id)
|
||||||
if locked_wrong_choice_ids.has(id):
|
if locked_wrong_choice_ids.has(id):
|
||||||
choice_buttons[index].modulate = Color(1.0, 0.25, 0.25)
|
choice_buttons[index].modulate = Color(1.0, 0.25, 0.25) if Style.is_dark else Color.WHITE
|
||||||
elif eliminated.has(id):
|
elif eliminated.has(id):
|
||||||
choice_buttons[index].modulate = Color(0.45, 0.45, 0.45)
|
choice_buttons[index].modulate = Color(0.45, 0.45, 0.45)
|
||||||
elif id == selected_choice:
|
elif id == selected_choice:
|
||||||
@@ -1754,6 +1796,8 @@ func _style_followup_result_button(button: Button, is_correct: bool) -> void:
|
|||||||
button.add_theme_stylebox_override("hover", Style.button_box(fill, border))
|
button.add_theme_stylebox_override("hover", Style.button_box(fill, border))
|
||||||
button.add_theme_stylebox_override("disabled", Style.button_box(fill, border))
|
button.add_theme_stylebox_override("disabled", Style.button_box(fill, border))
|
||||||
button.add_theme_color_override("font_color", Style.TEXT)
|
button.add_theme_color_override("font_color", Style.TEXT)
|
||||||
|
button.add_theme_color_override("font_hover_color", Style.TEXT)
|
||||||
|
button.add_theme_color_override("font_disabled_color", Style.TEXT)
|
||||||
|
|
||||||
|
|
||||||
func _unlock_after_followup() -> void:
|
func _unlock_after_followup() -> void:
|
||||||
|
|||||||
+130
-17
@@ -1,22 +1,77 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const BACKGROUND := Color("121827")
|
static var BACKGROUND := Color("121827")
|
||||||
const SURFACE := Color("1d2638")
|
static var SURFACE := Color("1d2638")
|
||||||
const SURFACE_DARK := Color("151c2b")
|
static var SURFACE_DARK := Color("151c2b")
|
||||||
const SURFACE_LIGHT := Color("27334a")
|
static var SURFACE_LIGHT := Color("27334a")
|
||||||
const TEXT := Color("f6f8ff")
|
static var TEXT := Color("f6f8ff")
|
||||||
const MUTED := Color("aeb8ca")
|
static var MUTED := Color("aeb8ca")
|
||||||
const CYAN := Color("4de1ff")
|
static var CYAN := Color("4de1ff")
|
||||||
const LIME := Color("a6ff5f")
|
static var LIME := Color("a6ff5f")
|
||||||
const MAGENTA := Color("ff4fd8")
|
static var MAGENTA := Color("ff4fd8")
|
||||||
const AMBER := Color("ffc857")
|
static var AMBER := Color("ffc857")
|
||||||
const RED := Color("ff5f6d")
|
static var RED := Color("ff5f6d")
|
||||||
|
static var BORDER := Color("33415c")
|
||||||
|
static var is_dark := true
|
||||||
|
|
||||||
|
const DARK_PALETTE := {
|
||||||
|
"background": Color("121827"), "surface": Color("1d2638"),
|
||||||
|
"surface_dark": Color("151c2b"), "surface_light": Color("27334a"),
|
||||||
|
"text": Color("f6f8ff"), "muted": Color("aeb8ca"),
|
||||||
|
"cyan": Color("4de1ff"), "lime": Color("a6ff5f"),
|
||||||
|
"magenta": Color("ff4fd8"), "amber": Color("ffc857"),
|
||||||
|
"red": Color("ff5f6d"), "border": Color("33415c")
|
||||||
|
}
|
||||||
|
const LIGHT_PALETTE := {
|
||||||
|
"background": Color("f4f7fb"), "surface": Color("ffffff"),
|
||||||
|
"surface_dark": Color("e8edf5"), "surface_light": Color("dce5f0"),
|
||||||
|
"text": Color("172033"), "muted": Color("59677c"),
|
||||||
|
"cyan": Color("007a99"), "lime": Color("347a18"),
|
||||||
|
"magenta": Color("a32187"), "amber": Color("986400"),
|
||||||
|
"red": Color("b42335"), "border": Color("aeb9c8")
|
||||||
|
}
|
||||||
|
const CUSTOM_DARK_TO_LIGHT := {
|
||||||
|
Color("111a2a"): Color("f8fafc"), Color("172337"): Color("fffffe"),
|
||||||
|
Color("182235"): Color("fefeff"), Color("26151a"): Color("fff1f2"),
|
||||||
|
Color("263752"): Color("aeb9c8"), Color("344463"): Color("aeb9c8"),
|
||||||
|
Color("405475"): Color("96a5b8"), Color("3b4964"): Color("aeb9c8"),
|
||||||
|
Color("303c55"): Color("b9c4d2"), Color("33425d"): Color("d2dce8"),
|
||||||
|
Color("26354f"): Color("e1e8f1"), Color("20384d"): Color("d8e8ed"),
|
||||||
|
Color("18465a"): Color("d4f2f7"), Color("20647e"): Color("b9e7ef"),
|
||||||
|
Color("153b4d"): Color("c6e8ed"), Color("21344a"): Color("d8e8ed"),
|
||||||
|
Color("202637"): Color("e5e9ef"), Color("2c3445"): Color("c8d0dc"),
|
||||||
|
Color("232b3d"): Color("e1e7ef"), Color("303b53"): Color("b7c2d0"),
|
||||||
|
Color("596982"): Color("66758a"), Color("1f7a3a"): Color("dff4e5"),
|
||||||
|
Color("8f1d2c"): Color("f8dce1")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func set_dark_mode(value: bool) -> void:
|
||||||
|
is_dark = value
|
||||||
|
var palette: Dictionary = DARK_PALETTE if value else LIGHT_PALETTE
|
||||||
|
BACKGROUND = palette.background
|
||||||
|
SURFACE = palette.surface
|
||||||
|
SURFACE_DARK = palette.surface_dark
|
||||||
|
SURFACE_LIGHT = palette.surface_light
|
||||||
|
TEXT = palette.text
|
||||||
|
MUTED = palette.muted
|
||||||
|
CYAN = palette.cyan
|
||||||
|
LIME = palette.lime
|
||||||
|
MAGENTA = palette.magenta
|
||||||
|
AMBER = palette.amber
|
||||||
|
RED = palette.red
|
||||||
|
BORDER = palette.border
|
||||||
|
|
||||||
static func apply_app_theme(root: Control) -> void:
|
static func apply_app_theme(root: Control) -> void:
|
||||||
var theme := Theme.new()
|
var theme := Theme.new()
|
||||||
theme.set_color("font_color", "Label", TEXT)
|
theme.set_color("font_color", "Label", TEXT)
|
||||||
theme.set_color("font_color", "Button", TEXT)
|
theme.set_color("font_color", "Button", TEXT)
|
||||||
theme.set_color("font_color", "RichTextLabel", TEXT)
|
theme.set_color("font_hover_color", "Button", TEXT)
|
||||||
|
theme.set_color("font_pressed_color", "Button", TEXT)
|
||||||
|
theme.set_color("font_disabled_color", "Button", MUTED)
|
||||||
|
theme.set_color("default_color", "RichTextLabel", TEXT)
|
||||||
|
theme.set_color("font_selected_color", "RichTextLabel", TEXT)
|
||||||
|
theme.set_color("selection_color", "RichTextLabel", Color(CYAN, 0.28))
|
||||||
theme.set_font_size("font_size", "Label", 17)
|
theme.set_font_size("font_size", "Label", 17)
|
||||||
theme.set_font_size("font_size", "Button", 16)
|
theme.set_font_size("font_size", "Button", 16)
|
||||||
theme.set_stylebox("normal", "Button", button_box(SURFACE_LIGHT, Color("3b4964")))
|
theme.set_stylebox("normal", "Button", button_box(SURFACE_LIGHT, Color("3b4964")))
|
||||||
@@ -27,10 +82,10 @@ static func apply_app_theme(root: Control) -> void:
|
|||||||
root.theme = theme
|
root.theme = theme
|
||||||
|
|
||||||
|
|
||||||
static func panel_box(color: Color = SURFACE, border: Color = Color("33415c")) -> StyleBoxFlat:
|
static func panel_box(color: Color = SURFACE, border: Color = BORDER) -> StyleBoxFlat:
|
||||||
var box := StyleBoxFlat.new()
|
var box := StyleBoxFlat.new()
|
||||||
box.bg_color = color
|
box.bg_color = _color_for_current_theme(color)
|
||||||
box.border_color = border
|
box.border_color = _color_for_current_theme(border)
|
||||||
box.set_border_width_all(1)
|
box.set_border_width_all(1)
|
||||||
box.corner_radius_top_left = 8
|
box.corner_radius_top_left = 8
|
||||||
box.corner_radius_top_right = 8
|
box.corner_radius_top_right = 8
|
||||||
@@ -45,8 +100,8 @@ static func panel_box(color: Color = SURFACE, border: Color = Color("33415c")) -
|
|||||||
|
|
||||||
static func button_box(color: Color, border: Color) -> StyleBoxFlat:
|
static func button_box(color: Color, border: Color) -> StyleBoxFlat:
|
||||||
var box := StyleBoxFlat.new()
|
var box := StyleBoxFlat.new()
|
||||||
box.bg_color = color
|
box.bg_color = _color_for_current_theme(color)
|
||||||
box.border_color = border
|
box.border_color = _color_for_current_theme(border)
|
||||||
box.set_border_width_all(1)
|
box.set_border_width_all(1)
|
||||||
box.corner_radius_top_left = 6
|
box.corner_radius_top_left = 6
|
||||||
box.corner_radius_top_right = 6
|
box.corner_radius_top_right = 6
|
||||||
@@ -76,9 +131,67 @@ static func style_answer_button(button: Button) -> void:
|
|||||||
button.add_theme_stylebox_override("hover", button_box(Color("26354f"), CYAN))
|
button.add_theme_stylebox_override("hover", button_box(Color("26354f"), CYAN))
|
||||||
button.add_theme_stylebox_override("pressed", button_box(Color("20384d"), CYAN))
|
button.add_theme_stylebox_override("pressed", button_box(Color("20384d"), CYAN))
|
||||||
button.add_theme_color_override("font_color", TEXT)
|
button.add_theme_color_override("font_color", TEXT)
|
||||||
|
button.add_theme_color_override("font_disabled_color", MUTED)
|
||||||
|
|
||||||
|
|
||||||
static func style_chip(button: Button, color: Color) -> void:
|
static func style_chip(button: Button, color: Color) -> void:
|
||||||
button.add_theme_stylebox_override("normal", button_box(Color(color, 0.22), color))
|
button.add_theme_stylebox_override("normal", button_box(Color(color, 0.22), color))
|
||||||
button.add_theme_stylebox_override("hover", button_box(Color(color, 0.34), color))
|
button.add_theme_stylebox_override("hover", button_box(Color(color, 0.34), color))
|
||||||
button.add_theme_color_override("font_color", TEXT)
|
button.add_theme_color_override("font_color", TEXT)
|
||||||
|
button.add_theme_color_override("font_disabled_color", MUTED)
|
||||||
|
|
||||||
|
|
||||||
|
static func refresh_tree(root: Control, was_dark: bool) -> void:
|
||||||
|
apply_app_theme(root)
|
||||||
|
_translate_control(root, DARK_PALETTE if was_dark else LIGHT_PALETTE, LIGHT_PALETTE if was_dark else DARK_PALETTE)
|
||||||
|
|
||||||
|
|
||||||
|
static func _translate_control(control: Control, source: Dictionary, target: Dictionary) -> void:
|
||||||
|
if control is ColorRect:
|
||||||
|
control.color = _translate_color(control.color, source, target)
|
||||||
|
for color_name in ["font_color", "font_hover_color", "font_pressed_color", "font_disabled_color", "font_selected_color", "default_color", "font_uneditable_color"]:
|
||||||
|
if control.has_theme_color_override(color_name):
|
||||||
|
control.add_theme_color_override(color_name, _translate_color(control.get_theme_color(color_name), source, target))
|
||||||
|
for style_name in ["panel", "normal", "hover", "pressed", "disabled", "focus", "fill", "background", "read_only"]:
|
||||||
|
if control.has_theme_stylebox_override(style_name):
|
||||||
|
var box := control.get_theme_stylebox(style_name)
|
||||||
|
if box is StyleBoxFlat:
|
||||||
|
var translated := box.duplicate() as StyleBoxFlat
|
||||||
|
translated.bg_color = _translate_color(translated.bg_color, source, target)
|
||||||
|
translated.border_color = _translate_color(translated.border_color, source, target)
|
||||||
|
control.add_theme_stylebox_override(style_name, translated)
|
||||||
|
for child in control.get_children():
|
||||||
|
if child is Control:
|
||||||
|
_translate_control(child, source, target)
|
||||||
|
|
||||||
|
|
||||||
|
static func _translate_color(color: Color, source: Dictionary, target: Dictionary) -> Color:
|
||||||
|
for key in source:
|
||||||
|
var candidate: Color = source[key]
|
||||||
|
if color.is_equal_approx(candidate):
|
||||||
|
var replacement: Color = target[key]
|
||||||
|
replacement.a = color.a
|
||||||
|
return replacement
|
||||||
|
for dark_color in CUSTOM_DARK_TO_LIGHT:
|
||||||
|
if color.is_equal_approx(dark_color):
|
||||||
|
var replacement: Color = CUSTOM_DARK_TO_LIGHT[dark_color]
|
||||||
|
replacement.a = color.a
|
||||||
|
return replacement
|
||||||
|
for dark_color in CUSTOM_DARK_TO_LIGHT:
|
||||||
|
var light_color: Color = CUSTOM_DARK_TO_LIGHT[dark_color]
|
||||||
|
if color.is_equal_approx(light_color):
|
||||||
|
var replacement: Color = dark_color
|
||||||
|
replacement.a = color.a
|
||||||
|
return replacement
|
||||||
|
return color
|
||||||
|
|
||||||
|
|
||||||
|
static func _color_for_current_theme(color: Color) -> Color:
|
||||||
|
if is_dark:
|
||||||
|
return color
|
||||||
|
for dark_color in CUSTOM_DARK_TO_LIGHT:
|
||||||
|
if color.is_equal_approx(dark_color):
|
||||||
|
var replacement: Color = CUSTOM_DARK_TO_LIGHT[dark_color]
|
||||||
|
replacement.a = color.a
|
||||||
|
return replacement
|
||||||
|
return color
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
Style.set_dark_mode(true)
|
||||||
|
var root := Control.new()
|
||||||
|
var background := ColorRect.new()
|
||||||
|
background.color = Style.BACKGROUND
|
||||||
|
root.add_child(background)
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475")))
|
||||||
|
root.add_child(panel)
|
||||||
|
|
||||||
|
Style.set_dark_mode(false)
|
||||||
|
Style.refresh_tree(root, true)
|
||||||
|
_assert(background.color.is_equal_approx(Style.BACKGROUND), "background should switch to light mode")
|
||||||
|
_assert(root.theme.get_color("default_color", "RichTextLabel").is_equal_approx(Style.TEXT), "rich text should use the light text color")
|
||||||
|
var light_box := panel.get_theme_stylebox("panel") as StyleBoxFlat
|
||||||
|
_assert(light_box.bg_color.is_equal_approx(Color("fffffe")), "custom panel surface should switch to light mode")
|
||||||
|
|
||||||
|
Style.set_dark_mode(true)
|
||||||
|
Style.refresh_tree(root, false)
|
||||||
|
_assert(background.color.is_equal_approx(Style.BACKGROUND), "background should switch back to dark mode")
|
||||||
|
_assert(root.theme.get_color("default_color", "RichTextLabel").is_equal_approx(Style.TEXT), "rich text should use the dark text color")
|
||||||
|
var dark_box := panel.get_theme_stylebox("panel") as StyleBoxFlat
|
||||||
|
_assert(dark_box.bg_color.is_equal_approx(Color("172337")), "custom panel surface should switch back to dark mode")
|
||||||
|
|
||||||
|
root.free()
|
||||||
|
quit(1 if failures > 0 else 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _assert(condition: bool, message: String) -> void:
|
||||||
|
if condition:
|
||||||
|
return
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bm47u1leamc1h
|
||||||
Reference in New Issue
Block a user