ui(ui): 💄 Update UI scripting component behavior for improved interactivity

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-27 21:03:09 -07:00
parent 636824015a
commit d44c019a2f
5 changed files with 811 additions and 0 deletions

View file

@ -0,0 +1,462 @@
extends Window
## Miku-themed chat window for text-based discussions.
## Displays voice transcripts and LLM responses; accepts typed input.
const EMOTION_COLORS: Dictionary = {
"happy": Color("#4ECDC4"),
"sad": Color("#7B9EBD"),
"angry": Color("#D45F5F"),
"surprised": Color("#00E5FF"),
"relaxed": Color("#80CBC4"),
"neutral": Color("#39C5BB"),
}
const STATE_LABELS: Dictionary = {
"idle": "",
"listening": "listening...",
"processing": "thinking...",
"speaking": "speaking",
"interrupted": "",
}
const BG_DARK := Color("#0D1117")
const BG_PANEL := Color("#111822")
const MIKU_TEAL := Color("#39C5BB")
const USER_BUBBLE_COLOR := Color("#1E2D45")
const MIKU_BUBBLE_COLOR := Color("#0D2F2D")
const TEXT_PRIMARY := Color("#E8F4F3")
const TEXT_MUTED := Color("#6B8E8B")
const BORDER_COLOR := Color("#1A3330")
var _message_list: VBoxContainer
var _scroll: ScrollContainer
var _status_label: Label
var _input: LineEdit
var _current_miku_label: RichTextLabel
var _current_miku_panel: PanelContainer
var _drag_start: Vector2i
var _dragging: bool = false
func setup() -> void:
force_native = true
EventBus.transcript_ready.connect(_on_transcript_ready)
EventBus.text_submitted.connect(_on_text_submitted_display)
EventBus.sentence_ready.connect(_on_sentence_ready)
EventBus.state_changed.connect(_on_state_changed)
EventBus.emotion_changed.connect(_on_emotion_changed)
close_requested.connect(hide)
func _ready() -> void:
title = ""
borderless = true
always_on_top = true
min_size = Vector2i(340, 480)
size = Vector2i(380, 560)
_build_ui()
_position_beside_companion()
func _position_beside_companion() -> void:
var companion_pos := DisplayServer.window_get_position(0)
var companion_size := DisplayServer.window_get_size(0)
var screen_idx := DisplayServer.get_primary_screen()
var screen_rect := DisplayServer.screen_get_usable_rect(screen_idx)
var chat_width := 380
var chat_height := 560
var gap := 8
# Calculate available space on right and left
var space_right := screen_rect.position.x + screen_rect.size.x - (companion_pos.x + companion_size.x + gap)
var space_left := companion_pos.x - screen_rect.position.x - gap
var x: int
if space_right >= chat_width:
# Enough space on right
x = companion_pos.x + companion_size.x + gap
elif space_left >= chat_width:
# Enough space on left
x = companion_pos.x - chat_width - gap
else:
# No ideal placement, prefer right and clamp
x = companion_pos.x + companion_size.x + gap
x = clampi(x, screen_rect.position.x, screen_rect.position.x + screen_rect.size.x - chat_width)
# Vertical: align bottom of chat with bottom of companion
var y := companion_pos.y + companion_size.y - chat_height
y = clampi(y, screen_rect.position.y, screen_rect.position.y + screen_rect.size.y - chat_height)
position = Vector2i(x, y)
func _build_ui() -> void:
var bg := PanelContainer.new()
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = BG_DARK
bg_style.set_border_width_all(1)
bg_style.border_color = BORDER_COLOR
bg_style.set_corner_radius_all(10)
bg.add_theme_stylebox_override("panel", bg_style)
add_child(bg)
var root := VBoxContainer.new()
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
bg.add_child(root)
root.add_child(_build_title_bar())
root.add_child(_build_divider())
_scroll = ScrollContainer.new()
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
root.add_child(_scroll)
var list_margin := MarginContainer.new()
list_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
list_margin.add_theme_constant_override("margin_left", 12)
list_margin.add_theme_constant_override("margin_right", 12)
list_margin.add_theme_constant_override("margin_top", 12)
list_margin.add_theme_constant_override("margin_bottom", 12)
_scroll.add_child(list_margin)
_message_list = VBoxContainer.new()
_message_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_message_list.add_theme_constant_override("separation", 10)
list_margin.add_child(_message_list)
root.add_child(_build_divider())
root.add_child(_build_input_bar())
func _build_title_bar() -> Control:
var bar := PanelContainer.new()
var style := StyleBoxFlat.new()
style.bg_color = BG_PANEL
style.corner_radius_top_left = 10
style.corner_radius_top_right = 10
bar.add_theme_stylebox_override("panel", style)
bar.custom_minimum_size.y = 40
bar.gui_input.connect(_on_titlebar_input)
var margin := MarginContainer.new()
margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin.mouse_filter = Control.MOUSE_FILTER_PASS
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_right", 8)
margin.add_theme_constant_override("margin_top", 0)
margin.add_theme_constant_override("margin_bottom", 0)
bar.add_child(margin)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 8)
hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.mouse_filter = Control.MOUSE_FILTER_PASS
margin.add_child(hbox)
var title_lbl := Label.new()
title_lbl.text = "✦ MIKU"
title_lbl.add_theme_color_override("font_color", MIKU_TEAL)
title_lbl.add_theme_font_size_override("font_size", 13)
hbox.add_child(title_lbl)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(spacer)
_status_label = Label.new()
_status_label.text = ""
_status_label.add_theme_color_override("font_color", TEXT_MUTED)
_status_label.add_theme_font_size_override("font_size", 11)
hbox.add_child(_status_label)
var close_btn := Button.new()
close_btn.text = "×"
close_btn.flat = true
close_btn.custom_minimum_size = Vector2(32, 32)
close_btn.add_theme_color_override("font_color", TEXT_MUTED)
close_btn.add_theme_color_override("font_hover_color", TEXT_PRIMARY)
close_btn.add_theme_font_size_override("font_size", 18)
close_btn.pressed.connect(hide)
hbox.add_child(close_btn)
return bar
func _build_divider() -> Control:
var line := ColorRect.new()
line.color = BORDER_COLOR
line.custom_minimum_size.y = 1
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return line
func _build_input_bar() -> Control:
var bar := PanelContainer.new()
var style := StyleBoxFlat.new()
style.bg_color = BG_PANEL
style.corner_radius_bottom_left = 10
style.corner_radius_bottom_right = 10
bar.add_theme_stylebox_override("panel", style)
bar.custom_minimum_size.y = 52
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
bar.add_child(margin)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 8)
margin.add_child(hbox)
_input = LineEdit.new()
_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_input.placeholder_text = "Type a message..."
_input.add_theme_color_override("font_color", TEXT_PRIMARY)
_input.add_theme_color_override("font_placeholder_color", TEXT_MUTED)
_input.add_theme_color_override("caret_color", MIKU_TEAL)
var input_normal := StyleBoxFlat.new()
input_normal.bg_color = Color("#0A1628")
input_normal.set_border_width_all(1)
input_normal.border_color = BORDER_COLOR
input_normal.set_corner_radius_all(6)
input_normal.content_margin_left = 10
input_normal.content_margin_right = 10
input_normal.content_margin_top = 6
input_normal.content_margin_bottom = 6
_input.add_theme_stylebox_override("normal", input_normal)
var input_focus := input_normal.duplicate() as StyleBoxFlat
input_focus.border_color = MIKU_TEAL
_input.add_theme_stylebox_override("focus", input_focus)
_input.text_submitted.connect(_on_input_text_submitted)
hbox.add_child(_input)
var send_btn := Button.new()
send_btn.text = ""
send_btn.custom_minimum_size = Vector2(40, 36)
send_btn.add_theme_color_override("font_color", BG_DARK)
send_btn.add_theme_font_size_override("font_size", 18)
var btn_normal := StyleBoxFlat.new()
btn_normal.bg_color = MIKU_TEAL
btn_normal.set_corner_radius_all(6)
send_btn.add_theme_stylebox_override("normal", btn_normal)
var btn_hover := btn_normal.duplicate() as StyleBoxFlat
btn_hover.bg_color = Color("#4ECDC4")
send_btn.add_theme_stylebox_override("hover", btn_hover)
var btn_pressed := btn_normal.duplicate() as StyleBoxFlat
btn_pressed.bg_color = Color("#2BA8A0")
send_btn.add_theme_stylebox_override("pressed", btn_pressed)
send_btn.pressed.connect(_on_send_pressed)
hbox.add_child(send_btn)
return bar
func _add_user_bubble(text: String) -> void:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 0)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(spacer)
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_SHRINK_END
var style := StyleBoxFlat.new()
style.bg_color = USER_BUBBLE_COLOR
style.set_corner_radius_all(8)
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 8
style.content_margin_bottom = 8
panel.add_theme_stylebox_override("panel", style)
var label := RichTextLabel.new()
label.bbcode_enabled = false
label.fit_content = true
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.custom_minimum_size.x = 40
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.add_theme_color_override("default_color", TEXT_PRIMARY)
label.add_theme_font_size_override("normal_font_size", 13)
label.selection_enabled = true
label.text = text
panel.add_child(label)
row.add_child(panel)
_message_list.add_child(row)
_scroll_to_bottom()
func _add_miku_bubble(emotion: String) -> void:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 0)
_current_miku_panel = PanelContainer.new()
_current_miku_panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
var style := StyleBoxFlat.new()
style.bg_color = MIKU_BUBBLE_COLOR
style.set_corner_radius_all(8)
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 8
style.content_margin_bottom = 8
style.border_width_left = 3
style.border_color = EMOTION_COLORS.get(emotion, MIKU_TEAL)
_current_miku_panel.add_theme_stylebox_override("panel", style)
_current_miku_label = RichTextLabel.new()
_current_miku_label.bbcode_enabled = false
_current_miku_label.fit_content = true
_current_miku_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_current_miku_label.custom_minimum_size.x = 40
_current_miku_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_current_miku_label.add_theme_color_override("default_color", TEXT_PRIMARY)
_current_miku_label.add_theme_font_size_override("normal_font_size", 13)
_current_miku_label.selection_enabled = true
_current_miku_panel.add_child(_current_miku_label)
row.add_child(_current_miku_panel)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(spacer)
_message_list.add_child(row)
_scroll_to_bottom()
func _update_miku_accent(emotion: String) -> void:
if _current_miku_panel == null:
return
var style := _current_miku_panel.get_theme_stylebox("panel") as StyleBoxFlat
if style == null:
return
style.border_color = EMOTION_COLORS.get(emotion, MIKU_TEAL)
func _scroll_to_bottom() -> void:
call_deferred("_do_scroll")
func _do_scroll() -> void:
_scroll.scroll_vertical = _scroll.get_v_scroll_bar().max_value
func _on_titlebar_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_LEFT:
_dragging = mb.pressed
if _dragging:
_drag_start = DisplayServer.mouse_get_position() - position
elif event is InputEventMouseMotion and _dragging:
position = DisplayServer.mouse_get_position() - _drag_start
func _on_input_text_submitted(text: String) -> void:
text = text.strip_edges()
if text.is_empty():
return
_input.text = ""
EventBus.text_submitted.emit(text)
func _on_send_pressed() -> void:
_on_input_text_submitted(_input.text)
func _on_transcript_ready(text: String) -> void:
_add_user_bubble(text)
_current_miku_label = null
_current_miku_panel = null
func _on_text_submitted_display(text: String) -> void:
_add_user_bubble(text)
_current_miku_label = null
_current_miku_panel = null
func _on_sentence_ready(text: String, emotion: String) -> void:
if _current_miku_label == null:
_add_miku_bubble(emotion)
else:
var trimmed := text.strip_edges()
if not trimmed.is_empty():
_current_miku_label.text += " " + trimmed
_scroll_to_bottom()
func _on_state_changed(_from: String, to: String) -> void:
if _status_label != null:
_status_label.text = STATE_LABELS.get(to, "")
if to == "idle":
_current_miku_label = null
_current_miku_panel = null
func _on_emotion_changed(emotion: String) -> void:
_update_miku_accent(emotion)
func show_error(message: String) -> void:
_add_error_bubble(message)
func _add_error_bubble(message: String) -> void:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 0)
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
var style := StyleBoxFlat.new()
style.bg_color = Color("#3D1F1F")
style.set_corner_radius_all(8)
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 8
style.content_margin_bottom = 8
style.border_width_left = 3
style.border_color = Color("#D45F5F")
panel.add_theme_stylebox_override("panel", style)
var label := RichTextLabel.new()
label.bbcode_enabled = false
label.fit_content = true
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.custom_minimum_size.x = 40
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.add_theme_color_override("default_color", Color("#FF9999"))
label.add_theme_font_size_override("normal_font_size", 13)
label.selection_enabled = true
label.text = "" + message
panel.add_child(label)
row.add_child(panel)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(spacer)
_message_list.add_child(row)
_scroll_to_bottom()
_current_miku_label = null
_current_miku_panel = null

View file

@ -0,0 +1 @@
uid://5fo765xukpeo

View file

@ -0,0 +1,78 @@
extends PopupMenu
## Right-click context menu for the companion avatar.
## Appears at cursor position with Miku dark theme styling.
const BG_DARK := Color("#0D1117")
const BG_PANEL := Color("#111822")
const MIKU_TEAL := Color("#39C5BB")
const BORDER_COLOR := Color("#1A3330")
const TEXT_PRIMARY := Color("#E8F4F3")
const TEXT_MUTED := Color("#6B8E8B")
enum Item {
SOUND_SETTINGS = 0,
CHAT = 1,
QUIT = 3,
}
func _ready() -> void:
borderless = true
always_on_top = true
_build_menu()
_apply_theme()
id_pressed.connect(_on_item_pressed)
func _build_menu() -> void:
add_item("Sound Settings", Item.SOUND_SETTINGS)
add_item("Chat", Item.CHAT)
add_separator()
add_item("Quit", Item.QUIT)
func _apply_theme() -> void:
var panel := StyleBoxFlat.new()
panel.bg_color = BG_DARK
panel.set_border_width_all(1)
panel.border_color = BORDER_COLOR
panel.set_corner_radius_all(8)
panel.content_margin_left = 4
panel.content_margin_right = 4
panel.content_margin_top = 4
panel.content_margin_bottom = 4
add_theme_stylebox_override("panel", panel)
var hover := StyleBoxFlat.new()
hover.bg_color = BG_PANEL
hover.set_corner_radius_all(5)
hover.content_margin_left = 8
hover.content_margin_right = 8
hover.content_margin_top = 4
hover.content_margin_bottom = 4
add_theme_stylebox_override("hover", hover)
add_theme_color_override("font_color", TEXT_PRIMARY)
add_theme_color_override("font_hover_color", MIKU_TEAL)
add_theme_color_override("font_separator_color", BORDER_COLOR)
add_theme_font_size_override("font_size", 13)
var separator := StyleBoxFlat.new()
separator.bg_color = BORDER_COLOR
separator.content_margin_top = 1
separator.content_margin_bottom = 1
add_theme_stylebox_override("separator", separator)
func show_at(pos: Vector2i) -> void:
popup(Rect2i(pos, Vector2i.ZERO))
func _on_item_pressed(id: int) -> void:
match id:
Item.SOUND_SETTINGS:
EventBus.sound_settings_opened.emit()
Item.CHAT:
EventBus.chat_opened.emit()
Item.QUIT:
get_tree().quit()

View file

@ -0,0 +1,269 @@
extends Window
## Sound settings panel — maps companion events to sounds.
## Miku dark theme, built entirely in GDScript.
const BG_DARK := Color("#0D1117")
const BG_PANEL := Color("#111822")
const MIKU_TEAL := Color("#39C5BB")
const BORDER_COLOR := Color("#1A3330")
const TEXT_PRIMARY := Color("#E8F4F3")
const TEXT_MUTED := Color("#6B8E8B")
var _sound_config: Node
var _sound_engine: Node
var _drag_start: Vector2i
var _dragging: bool = false
# Maps slot key → OptionButton, so we can read current selection for ▶
var _option_buttons: Dictionary = {}
func setup(sound_config: Node, sound_engine: Node) -> void:
_sound_config = sound_config
_sound_engine = sound_engine
close_requested.connect(hide)
func _ready() -> void:
title = ""
borderless = true
always_on_top = true
force_native = true
min_size = Vector2i(420, 300)
size = Vector2i(460, 380)
_build_ui()
_position_beside_companion()
func _position_beside_companion() -> void:
var companion_pos := DisplayServer.window_get_position(0)
var companion_size := DisplayServer.window_get_size(0)
var screen_idx := DisplayServer.get_primary_screen()
var screen_rect := DisplayServer.screen_get_usable_rect(screen_idx)
var x := companion_pos.x + companion_size.x + 8
var y := companion_pos.y
x = clampi(x, screen_rect.position.x, screen_rect.position.x + screen_rect.size.x - 460)
y = clampi(y, screen_rect.position.y, screen_rect.position.y + screen_rect.size.y - 380)
position = Vector2i(x, y)
func _build_ui() -> void:
var bg := PanelContainer.new()
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = BG_DARK
bg_style.set_border_width_all(1)
bg_style.border_color = BORDER_COLOR
bg_style.set_corner_radius_all(10)
bg.add_theme_stylebox_override("panel", bg_style)
add_child(bg)
var root := VBoxContainer.new()
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
bg.add_child(root)
root.add_child(_build_title_bar())
root.add_child(_build_divider())
root.add_child(_build_slot_list())
func _build_title_bar() -> Control:
var bar := PanelContainer.new()
var style := StyleBoxFlat.new()
style.bg_color = BG_PANEL
style.corner_radius_top_left = 10
style.corner_radius_top_right = 10
bar.add_theme_stylebox_override("panel", style)
bar.custom_minimum_size.y = 40
bar.gui_input.connect(_on_titlebar_input)
var margin := MarginContainer.new()
margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin.add_theme_constant_override("margin_left", 14)
margin.add_theme_constant_override("margin_right", 8)
margin.add_theme_constant_override("margin_top", 0)
margin.add_theme_constant_override("margin_bottom", 0)
bar.add_child(margin)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 8)
hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
margin.add_child(hbox)
var title_lbl := Label.new()
title_lbl.text = "✦ SOUND SETTINGS"
title_lbl.add_theme_color_override("font_color", MIKU_TEAL)
title_lbl.add_theme_font_size_override("font_size", 13)
hbox.add_child(title_lbl)
var spacer := Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(spacer)
var close_btn := Button.new()
close_btn.text = "×"
close_btn.flat = true
close_btn.custom_minimum_size = Vector2(32, 32)
close_btn.add_theme_color_override("font_color", TEXT_MUTED)
close_btn.add_theme_color_override("font_hover_color", TEXT_PRIMARY)
close_btn.add_theme_font_size_override("font_size", 18)
close_btn.pressed.connect(hide)
hbox.add_child(close_btn)
return bar
func _build_divider() -> Control:
var line := ColorRect.new()
line.color = BORDER_COLOR
line.custom_minimum_size.y = 1
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
return line
func _build_slot_list() -> Control:
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
var outer := MarginContainer.new()
outer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
outer.add_theme_constant_override("margin_left", 12)
outer.add_theme_constant_override("margin_right", 12)
outer.add_theme_constant_override("margin_top", 12)
outer.add_theme_constant_override("margin_bottom", 12)
scroll.add_child(outer)
var vbox := VBoxContainer.new()
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
vbox.add_theme_constant_override("separation", 8)
outer.add_child(vbox)
# Guard: if SoundConfig not yet wired (e.g., opened before setup() called)
if _sound_config == null:
var warn := Label.new()
warn.text = "SoundConfig not available."
warn.add_theme_color_override("font_color", TEXT_MUTED)
vbox.add_child(warn)
return scroll
var slots: Dictionary = _sound_config.get_slots()
var sound_names: Array[String] = []
if _sound_engine != null:
sound_names = _sound_engine.get_sound_names()
for slot_key: String in slots.keys():
vbox.add_child(_build_row(slot_key, slots[slot_key], sound_names))
return scroll
func _build_row(slot_key: String, label_text: String, sound_names: Array[String]) -> Control:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_theme_constant_override("separation", 8)
# Label
var lbl := Label.new()
lbl.text = label_text
lbl.custom_minimum_size.x = 140
lbl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
lbl.add_theme_color_override("font_color", TEXT_PRIMARY)
lbl.add_theme_font_size_override("font_size", 13)
row.add_child(lbl)
# OptionButton
var opt := OptionButton.new()
opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_style_option_button(opt)
# Populate choices: "(none)" first, then all sounds
opt.add_item("(none)", 0)
for i: int in range(sound_names.size()):
opt.add_item(sound_names[i], i + 1)
# Select current value
var current: String = _sound_config.get_sound(slot_key)
if current.is_empty():
opt.select(0)
else:
var idx: int = sound_names.find(current)
opt.select(idx + 1 if idx >= 0 else 0)
# Capture slot_key in closure via a bound callable
opt.item_selected.connect(_on_option_changed.bind(slot_key, opt, sound_names))
row.add_child(opt)
_option_buttons[slot_key] = opt
# Play button
var play_btn := Button.new()
play_btn.text = ""
play_btn.flat = false
play_btn.custom_minimum_size = Vector2(36, 30)
play_btn.add_theme_color_override("font_color", BG_DARK)
play_btn.add_theme_font_size_override("font_size", 13)
_style_play_button(play_btn)
play_btn.pressed.connect(_on_play_pressed.bind(slot_key, opt, sound_names))
row.add_child(play_btn)
return row
func _on_option_changed(_index: int, slot_key: String, opt: OptionButton, sound_names: Array[String]) -> void:
var selected_idx: int = opt.selected
var sound: String = "" if selected_idx == 0 else sound_names[selected_idx - 1]
if _sound_config != null:
_sound_config.set_sound(slot_key, sound)
func _on_play_pressed(slot_key: String, opt: OptionButton, sound_names: Array[String]) -> void:
var selected_idx: int = opt.selected
if selected_idx == 0 or _sound_engine == null:
return
var sound: String = sound_names[selected_idx - 1]
_sound_engine.play_sound(sound)
func _on_titlebar_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_LEFT:
_dragging = mb.pressed
if _dragging:
_drag_start = DisplayServer.mouse_get_position() - position
elif event is InputEventMouseMotion and _dragging:
position = DisplayServer.mouse_get_position() - _drag_start
func _style_option_button(opt: OptionButton) -> void:
var style := StyleBoxFlat.new()
style.bg_color = Color("#0A1628")
style.set_border_width_all(1)
style.border_color = BORDER_COLOR
style.set_corner_radius_all(5)
style.content_margin_left = 8
style.content_margin_right = 8
style.content_margin_top = 4
style.content_margin_bottom = 4
opt.add_theme_stylebox_override("normal", style)
opt.add_theme_color_override("font_color", TEXT_PRIMARY)
var hover := style.duplicate() as StyleBoxFlat
hover.border_color = MIKU_TEAL
opt.add_theme_stylebox_override("hover", hover)
opt.add_theme_stylebox_override("focus", hover)
func _style_play_button(btn: Button) -> void:
var normal := StyleBoxFlat.new()
normal.bg_color = MIKU_TEAL
normal.set_corner_radius_all(5)
btn.add_theme_stylebox_override("normal", normal)
var hover := normal.duplicate() as StyleBoxFlat
hover.bg_color = Color("#4ECDC4")
btn.add_theme_stylebox_override("hover", hover)
var pressed := normal.duplicate() as StyleBoxFlat
pressed.bg_color = Color("#2BA8A0")
btn.add_theme_stylebox_override("pressed", pressed)

View file

@ -0,0 +1 @@
uid://bcasxf0b3usgv