ui(ui): 💄 Update UI scripting component behavior for improved interactivity
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
636824015a
commit
d44c019a2f
5 changed files with 811 additions and 0 deletions
462
godot/scripts/ui/chat_window.gd
Normal file
462
godot/scripts/ui/chat_window.gd
Normal 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
|
||||
1
godot/scripts/ui/chat_window.gd.uid
Normal file
1
godot/scripts/ui/chat_window.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://5fo765xukpeo
|
||||
78
godot/scripts/ui/context_menu.gd
Normal file
78
godot/scripts/ui/context_menu.gd
Normal 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()
|
||||
269
godot/scripts/ui/sound_settings_window.gd
Normal file
269
godot/scripts/ui/sound_settings_window.gd
Normal 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)
|
||||
1
godot/scripts/ui/sound_settings_window.gd.uid
Normal file
1
godot/scripts/ui/sound_settings_window.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bcasxf0b3usgv
|
||||
Loading…
Add table
Reference in a new issue