627 lines
27 KiB
GDScript
627 lines
27 KiB
GDScript
class_name SpeechShowToolbar
|
|
extends Control
|
|
## Stage-side controls that apply presentation choices to an entire speech.
|
|
|
|
signal choice_selected(key: StringName, value: Variant)
|
|
signal layout_preset_requested(preset: StringName)
|
|
signal preview_speech_requested()
|
|
signal done_speech_requested()
|
|
signal cancel_speech_requested()
|
|
|
|
var _theme: SandboxTheme
|
|
var _action: Dictionary = {}
|
|
var _group_rect: Rect2 = Rect2()
|
|
var _stage_rect: Rect2 = Rect2()
|
|
var _badge_avoidance_rects: Array[Rect2] = []
|
|
var _busy: bool = false
|
|
var _pin_top_while_more_open: bool = false
|
|
var _preserve_top_on_layout: bool = false
|
|
var _pinned_panel_top: float = 0.0
|
|
var _panel: PanelContainer
|
|
var _content: VBoxContainer
|
|
var _body_scroll: ScrollContainer
|
|
var _body_content: VBoxContainer
|
|
var _title_ribbon: PanelContainer
|
|
var _buttons_by_key: Dictionary = {}
|
|
var _button_values_by_key: Dictionary = {}
|
|
var _more_button: Button
|
|
var _more_content: VBoxContainer
|
|
var _keep_acting_button: Button
|
|
var _preview_button: Button
|
|
var _done_button: Button
|
|
var _cancel_button: Button
|
|
var _layout_preset_buttons: Array[Button] = []
|
|
var _layout_preset_values: Array[StringName] = []
|
|
var _connect_bubbles_button: Button
|
|
|
|
|
|
class BubbleLayoutSample extends Control:
|
|
var same_spot: bool = false
|
|
|
|
func _ready() -> void:
|
|
custom_minimum_size = Vector2(58.0, 42.0)
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
|
|
func _draw() -> void:
|
|
var boxes: Array[Rect2] = []
|
|
if same_spot:
|
|
boxes = [Rect2(4.0, 5.0, 34.0, 22.0), Rect2(10.0, 9.0, 34.0, 22.0), Rect2(16.0, 13.0, 34.0, 22.0)]
|
|
else:
|
|
boxes = [Rect2(2.0, 20.0, 32.0, 20.0), Rect2(13.0, 10.0, 32.0, 20.0), Rect2(24.0, 0.0, 32.0, 20.0)]
|
|
for index: int in boxes.size():
|
|
var bubble_style: StyleBoxFlat = StyleBoxFlat.new()
|
|
bubble_style.bg_color = Color("#FFFDF6") if index == boxes.size() - 1 else Color("#DDE7F0")
|
|
bubble_style.border_color = Color("#202733")
|
|
bubble_style.set_border_width_all(2)
|
|
bubble_style.set_corner_radius_all(5)
|
|
draw_style_box(bubble_style, boxes[index])
|
|
|
|
|
|
func _ready() -> void:
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
z_index = 79
|
|
_build_ui()
|
|
visible = false
|
|
|
|
|
|
func configure(theme_data: SandboxTheme) -> void:
|
|
_theme = theme_data
|
|
if is_inside_tree() and _panel != null:
|
|
_apply_theme()
|
|
|
|
|
|
func set_context(action: Dictionary, _reactive_options: bool = false) -> void:
|
|
_action = action.duplicate(true)
|
|
if _more_button != null:
|
|
_more_button.visible = true
|
|
_more_button.text = "▾ More speech settings" if _more_content != null and _more_content.visible else "▸ More speech settings"
|
|
if _keep_acting_button != null:
|
|
_keep_acting_button.set_pressed_no_signal(bool(_action.get("keep_acting", false)))
|
|
_refresh_choices()
|
|
_refresh_layout_buttons()
|
|
_layout_panel()
|
|
|
|
|
|
func set_anchor_rect(group_viewport_rect: Rect2, stage_viewport_rect: Rect2) -> void:
|
|
var anchor_changed: bool = _rect_changed(_group_rect, group_viewport_rect) or _rect_changed(_stage_rect, stage_viewport_rect)
|
|
_group_rect = group_viewport_rect
|
|
_stage_rect = stage_viewport_rect
|
|
if anchor_changed and _more_content != null and _more_content.visible:
|
|
_pin_top_while_more_open = false
|
|
visible = not _busy and _group_rect.size.x > 0.0 and _group_rect.size.y > 0.0 and _stage_rect.size.x > 0.0 and _stage_rect.size.y > 0.0
|
|
if visible:
|
|
_layout_panel()
|
|
|
|
|
|
func set_badge_avoidance_rects(rects: Array[Rect2]) -> void:
|
|
if _badge_avoidance_rects == rects:
|
|
return
|
|
_badge_avoidance_rects = rects.duplicate()
|
|
if visible:
|
|
_layout_panel()
|
|
|
|
|
|
func set_busy(busy: bool) -> void:
|
|
_busy = busy
|
|
for key: Variant in _buttons_by_key.keys():
|
|
for button: Button in _buttons_by_key[key]:
|
|
button.disabled = busy
|
|
for button: Button in [_more_button, _preview_button, _done_button, _cancel_button, _keep_acting_button]:
|
|
if button != null:
|
|
button.disabled = busy
|
|
_refresh_layout_buttons()
|
|
_refresh_choices()
|
|
visible = not busy and _group_rect.size.x > 0.0 and _group_rect.size.y > 0.0 and _stage_rect.size.x > 0.0 and _stage_rect.size.y > 0.0
|
|
if visible:
|
|
_layout_panel()
|
|
|
|
|
|
func contains_viewport_point(point: Vector2) -> bool:
|
|
return visible and _panel != null and _panel.visible and _panel.get_global_rect().has_point(point)
|
|
|
|
|
|
func panel_viewport_rect() -> Rect2:
|
|
return _panel.get_global_rect() if _panel != null and is_instance_valid(_panel) and _panel.visible else Rect2()
|
|
|
|
|
|
func _build_ui() -> void:
|
|
_panel = PanelContainer.new()
|
|
_panel.name = "SpeechShowPanel"
|
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
add_child(_panel)
|
|
var margin: MarginContainer = MarginContainer.new()
|
|
margin.add_theme_constant_override("margin_left", 10)
|
|
margin.add_theme_constant_override("margin_right", 10)
|
|
margin.add_theme_constant_override("margin_top", 9)
|
|
margin.add_theme_constant_override("margin_bottom", 9)
|
|
_panel.add_child(margin)
|
|
_content = VBoxContainer.new()
|
|
_content.add_theme_constant_override("separation", 8)
|
|
margin.add_child(_content)
|
|
_body_scroll = ScrollContainer.new()
|
|
_body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
_body_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
|
_body_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
_content.add_child(_body_scroll)
|
|
_body_content = VBoxContainer.new()
|
|
_body_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_body_content.add_theme_constant_override("separation", 8)
|
|
_body_scroll.add_child(_body_content)
|
|
_title_ribbon = PanelContainer.new()
|
|
_title_ribbon.name = "SpeechShowRibbon"
|
|
_body_content.add_child(_title_ribbon)
|
|
var title: Label = Label.new()
|
|
title.text = "Show speech"
|
|
title.add_theme_font_size_override("font_size", 22)
|
|
title.add_theme_color_override("font_color", Color.WHITE)
|
|
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_title_ribbon.add_child(title)
|
|
_add_choice_row("💬 Show bubbles", &"arrangement", [["Show all", "together"], ["One at a time", "take_turns"]])
|
|
_add_layout_preset_row()
|
|
_add_connect_bubbles_row()
|
|
_more_button = Button.new()
|
|
_more_button.text = "▸ More speech settings"
|
|
_more_button.toggle_mode = true
|
|
_more_button.custom_minimum_size = Vector2(220.0, _hit_height())
|
|
_more_button.focus_mode = Control.FOCUS_NONE
|
|
_more_button.toggled.connect(_on_more_toggled)
|
|
_body_content.add_child(_more_button)
|
|
_more_content = VBoxContainer.new()
|
|
_more_content.add_theme_constant_override("separation", 4)
|
|
_more_content.hide()
|
|
_body_content.add_child(_more_content)
|
|
_add_choice_row("🔤 Words appear", &"reveal", [["All at once", "instant"], ["Letter by letter", "letter"], ["Word by word", "word"]], _more_content)
|
|
_add_choice_row("✨ Bubble motion", &"pop_in", [["Bounce in", true], ["Just appear", false]], _more_content)
|
|
_add_choice_row("⏱ Timing", &"duration_mode", [["Normal", "just_enough"], ["Slow", "longer"], ["Slower", "longest"]], _more_content)
|
|
var keep_acting_row := HBoxContainer.new()
|
|
keep_acting_row.add_theme_constant_override("separation", 10)
|
|
_more_content.add_child(keep_acting_row)
|
|
var keep_acting_label := Label.new()
|
|
keep_acting_label.text = "🎭 Keep acting while talking"
|
|
keep_acting_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
keep_acting_label.add_theme_font_size_override("font_size", 19)
|
|
keep_acting_label.add_theme_color_override("font_color", Color("#F2F4F8"))
|
|
keep_acting_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
keep_acting_row.add_child(keep_acting_label)
|
|
_keep_acting_button = Button.new()
|
|
_keep_acting_button.toggle_mode = true
|
|
_keep_acting_button.custom_minimum_size = Vector2(88.0, _hit_height())
|
|
_keep_acting_button.focus_mode = Control.FOCUS_NONE
|
|
_keep_acting_button.add_theme_font_size_override("font_size", 17)
|
|
_keep_acting_button.toggled.connect(_on_keep_acting_toggled)
|
|
keep_acting_row.add_child(_keep_acting_button)
|
|
var footer: HFlowContainer = HFlowContainer.new()
|
|
footer.add_theme_constant_override("h_separation", 8)
|
|
footer.add_theme_constant_override("v_separation", 8)
|
|
_content.add_child(footer)
|
|
_preview_button = _action_button("▶ Preview speech")
|
|
_preview_button.pressed.connect(func() -> void: preview_speech_requested.emit())
|
|
footer.add_child(_preview_button)
|
|
_done_button = _action_button("✓ Done speech")
|
|
_done_button.pressed.connect(func() -> void: done_speech_requested.emit())
|
|
footer.add_child(_done_button)
|
|
_cancel_button = _action_button("Cancel speech")
|
|
_cancel_button.pressed.connect(func() -> void: cancel_speech_requested.emit())
|
|
footer.add_child(_cancel_button)
|
|
_apply_theme()
|
|
|
|
|
|
func _add_choice_row(label_text: String, key: StringName, choices: Array, parent: Control = null) -> void:
|
|
var row: VBoxContainer = VBoxContainer.new()
|
|
row.add_theme_constant_override("separation", 4)
|
|
var row_parent: Control = parent if parent != null else _body_content
|
|
row_parent.add_child(row)
|
|
var heading: Label = Label.new()
|
|
heading.text = label_text
|
|
heading.add_theme_font_size_override("font_size", 19)
|
|
heading.add_theme_color_override("font_color", Color("#F2F4F8"))
|
|
heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
row.add_child(heading)
|
|
var flow: HFlowContainer = HFlowContainer.new()
|
|
flow.add_theme_constant_override("h_separation", 7)
|
|
flow.add_theme_constant_override("v_separation", 7)
|
|
row.add_child(flow)
|
|
var buttons: Array[Button] = []
|
|
var values: Array[Variant] = []
|
|
for pair: Array in choices:
|
|
var choice_value: Variant = pair[1]
|
|
var button: Button = Button.new()
|
|
button.text = String(pair[0])
|
|
button.toggle_mode = true
|
|
button.custom_minimum_size = Vector2(136.0, _hit_height())
|
|
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
|
button.focus_mode = Control.FOCUS_NONE
|
|
button.add_theme_font_size_override("font_size", 17)
|
|
button.pressed.connect(_on_choice_pressed.bind(key, choice_value))
|
|
flow.add_child(button)
|
|
buttons.append(button)
|
|
values.append(choice_value)
|
|
_buttons_by_key[key] = buttons
|
|
_button_values_by_key[key] = values
|
|
|
|
|
|
func _add_layout_preset_row() -> void:
|
|
var row: VBoxContainer = VBoxContainer.new()
|
|
row.add_theme_constant_override("separation", 4)
|
|
_body_content.add_child(row)
|
|
var heading: Label = Label.new()
|
|
heading.text = "🗨️ Bubble layout"
|
|
heading.add_theme_font_size_override("font_size", 19)
|
|
heading.add_theme_color_override("font_color", Color("#F2F4F8"))
|
|
heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
row.add_child(heading)
|
|
var flow: HFlowContainer = HFlowContainer.new()
|
|
flow.add_theme_constant_override("h_separation", 7)
|
|
flow.add_theme_constant_override("v_separation", 7)
|
|
row.add_child(flow)
|
|
_add_layout_preset_button(flow, "Stack", &"stack", false, "Arrange the bubbles upward from Bubble 1.")
|
|
_add_layout_preset_button(flow, "Same spot", &"same_spot", true, "Put every bubble at Bubble 1's position.")
|
|
|
|
|
|
func _add_connect_bubbles_row() -> void:
|
|
var row: HBoxContainer = HBoxContainer.new()
|
|
row.add_theme_constant_override("separation", 8)
|
|
_body_content.add_child(row)
|
|
var label: Label = Label.new()
|
|
label.text = "🔗 Connect bubbles"
|
|
label.add_theme_font_size_override("font_size", 19)
|
|
label.add_theme_color_override("font_color", Color("#F2F4F8"))
|
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
row.add_child(label)
|
|
_connect_bubbles_button = Button.new()
|
|
_connect_bubbles_button.toggle_mode = true
|
|
_connect_bubbles_button.custom_minimum_size = Vector2(112.0, _hit_height())
|
|
_connect_bubbles_button.size_flags_horizontal = Control.SIZE_SHRINK_END
|
|
_connect_bubbles_button.focus_mode = Control.FOCUS_NONE
|
|
_connect_bubbles_button.add_theme_font_size_override("font_size", 17)
|
|
_connect_bubbles_button.pressed.connect(_on_connect_bubbles_pressed)
|
|
row.add_child(_connect_bubbles_button)
|
|
|
|
|
|
func _add_layout_preset_button(parent: HFlowContainer, caption: String, preset: StringName, same_spot: bool, hint: String) -> void:
|
|
var button: Button = Button.new()
|
|
var button_width: float = 184.0 if same_spot else 150.0
|
|
button.custom_minimum_size = Vector2(button_width, maxf(64.0, _hit_height()))
|
|
button.toggle_mode = true
|
|
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
|
button.focus_mode = Control.FOCUS_NONE
|
|
button.tooltip_text = hint
|
|
button.pressed.connect(func() -> void: layout_preset_requested.emit(preset))
|
|
var margin: MarginContainer = MarginContainer.new()
|
|
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
margin.add_theme_constant_override("margin_left", 5)
|
|
margin.add_theme_constant_override("margin_right", 5)
|
|
margin.add_theme_constant_override("margin_top", 4)
|
|
margin.add_theme_constant_override("margin_bottom", 4)
|
|
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
button.add_child(margin)
|
|
var contents: HBoxContainer = HBoxContainer.new()
|
|
contents.add_theme_constant_override("separation", 2)
|
|
contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
margin.add_child(contents)
|
|
var sample: BubbleLayoutSample = BubbleLayoutSample.new()
|
|
sample.same_spot = same_spot
|
|
contents.add_child(sample)
|
|
var label: Label = Label.new()
|
|
label.text = caption
|
|
label.add_theme_font_size_override("font_size", 16)
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
contents.add_child(label)
|
|
parent.add_child(button)
|
|
_layout_preset_buttons.append(button)
|
|
_layout_preset_values.append(preset)
|
|
|
|
|
|
func _on_choice_pressed(key: StringName, value: Variant) -> void:
|
|
if _busy:
|
|
return
|
|
_action[String(key)] = value
|
|
_refresh_choices()
|
|
choice_selected.emit(key, value)
|
|
|
|
|
|
func _on_connect_bubbles_pressed() -> void:
|
|
if _busy or _connect_bubbles_button == null:
|
|
return
|
|
var enabled: bool = not bool(_action.get("connect_bubbles", false))
|
|
_action["connect_bubbles"] = enabled
|
|
_refresh_connect_bubbles_button()
|
|
choice_selected.emit(&"connect_bubbles", enabled)
|
|
|
|
|
|
func _on_keep_acting_toggled(enabled: bool) -> void:
|
|
if _busy or _keep_acting_button == null:
|
|
return
|
|
_action["keep_acting"] = enabled
|
|
_refresh_keep_acting_toggle()
|
|
choice_selected.emit(&"keep_acting", enabled)
|
|
|
|
|
|
func _on_more_toggled(opened: bool) -> void:
|
|
if _busy:
|
|
return
|
|
_more_content.visible = opened
|
|
_more_button.text = "▾ More speech settings" if opened else "▸ More speech settings"
|
|
if opened:
|
|
_pinned_panel_top = _panel.position.y
|
|
_pin_top_while_more_open = true
|
|
else:
|
|
_pin_top_while_more_open = false
|
|
_preserve_top_on_layout = true
|
|
_layout_panel()
|
|
|
|
|
|
func _refresh_choices() -> void:
|
|
for raw_key: Variant in _buttons_by_key.keys():
|
|
var key: StringName = StringName(raw_key)
|
|
var buttons: Array = _buttons_by_key[key]
|
|
var values: Array = _button_values_by_key[key]
|
|
var current: Variant = _action.get(String(key), _default_value(key))
|
|
for index: int in buttons.size():
|
|
var button: Button = buttons[index] as Button
|
|
var selected: bool = current == values[index]
|
|
button.set_pressed_no_signal(selected)
|
|
_apply_choice_style(button, selected)
|
|
button.disabled = _busy
|
|
_refresh_connect_bubbles_button()
|
|
_refresh_keep_acting_toggle()
|
|
|
|
|
|
func _refresh_layout_buttons() -> void:
|
|
var bubbles: Array = _action.get("bubbles", [])
|
|
var disabled: bool = _busy or bubbles.size() <= 1
|
|
var selected_layout: String = String(_action.get("bubble_layout", "stack"))
|
|
for index: int in _layout_preset_buttons.size():
|
|
var button: Button = _layout_preset_buttons[index]
|
|
var selected: bool = index < _layout_preset_values.size() and String(_layout_preset_values[index]) == selected_layout
|
|
button.disabled = disabled
|
|
button.set_pressed_no_signal(selected)
|
|
_apply_choice_style(button, selected)
|
|
if _connect_bubbles_button != null:
|
|
_connect_bubbles_button.disabled = _busy or bubbles.size() < 2
|
|
_connect_bubbles_button.set_pressed_no_signal(bool(_action.get("connect_bubbles", false)))
|
|
_connect_bubbles_button.text = "On" if bool(_action.get("connect_bubbles", false)) else "Off"
|
|
_apply_choice_style(_connect_bubbles_button, bool(_action.get("connect_bubbles", false)))
|
|
|
|
|
|
func _refresh_connect_bubbles_button() -> void:
|
|
if _connect_bubbles_button == null:
|
|
return
|
|
var bubbles: Array = _action.get("bubbles", []) if _action.get("bubbles", []) is Array else []
|
|
var enabled: bool = bool(_action.get("connect_bubbles", false))
|
|
_connect_bubbles_button.disabled = _busy or bubbles.size() < 2
|
|
_connect_bubbles_button.set_pressed_no_signal(enabled)
|
|
_connect_bubbles_button.text = "On" if enabled else "Off"
|
|
_apply_choice_style(_connect_bubbles_button, enabled)
|
|
|
|
|
|
func _refresh_keep_acting_toggle() -> void:
|
|
if _keep_acting_button == null:
|
|
return
|
|
var enabled: bool = bool(_action.get("keep_acting", false))
|
|
_keep_acting_button.disabled = _busy
|
|
_keep_acting_button.set_pressed_no_signal(enabled)
|
|
_keep_acting_button.text = "ON" if enabled else "OFF"
|
|
_apply_choice_style(_keep_acting_button, enabled)
|
|
|
|
|
|
func _default_value(key: StringName) -> Variant:
|
|
match key:
|
|
&"arrangement": return "together"
|
|
&"reveal": return "instant"
|
|
&"pop_in": return false
|
|
&"duration_mode": return "just_enough"
|
|
return null
|
|
|
|
|
|
func _layout_panel() -> void:
|
|
if _panel == null or _stage_rect.size.x <= 0.0 or _stage_rect.size.y <= 0.0:
|
|
return
|
|
var gap: float = _gap()
|
|
var max_width: float = minf(_max_width(), _stage_rect.size.x)
|
|
var preserve_top: bool = _preserve_top_on_layout or (_pin_top_while_more_open and _more_content != null and _more_content.visible)
|
|
var pinned_top: float = _pinned_panel_top if _pin_top_while_more_open and _more_content != null and _more_content.visible else _panel.position.y
|
|
pinned_top = clampf(pinned_top, _stage_rect.position.y, _stage_rect.end.y)
|
|
var available_height: float = _stage_rect.size.y
|
|
if preserve_top:
|
|
available_height = maxf(1.0, _stage_rect.end.y - pinned_top)
|
|
var left_space: float = maxf(0.0, _group_rect.position.x - _stage_rect.position.x - gap)
|
|
var right_space: float = maxf(0.0, _stage_rect.end.x - _group_rect.end.x - gap)
|
|
var side_space: float = maxf(left_space, right_space)
|
|
var panel_width: float = max_width
|
|
var panel_height: float
|
|
var panel_position: Vector2
|
|
if side_space >= minf(max_width, 300.0):
|
|
panel_width = minf(max_width, side_space)
|
|
panel_height = _fit_panel_height(panel_width, available_height)
|
|
var use_right: bool = right_space >= left_space
|
|
panel_position.x = _group_rect.end.x + gap if use_right else _group_rect.position.x - panel_width - gap
|
|
panel_position.y = clampf(_group_rect.get_center().y - panel_height * 0.5, _stage_rect.position.y, maxf(_stage_rect.position.y, _stage_rect.end.y - panel_height))
|
|
else:
|
|
panel_width = max_width
|
|
panel_height = _fit_panel_height(panel_width, available_height)
|
|
var below_space: float = _stage_rect.end.y - _group_rect.end.y - gap
|
|
var above_space: float = _group_rect.position.y - _stage_rect.position.y - gap
|
|
var place_below: bool = below_space >= panel_height or below_space >= above_space
|
|
panel_position.x = clampf(_group_rect.get_center().x - panel_width * 0.5, _stage_rect.position.x, maxf(_stage_rect.position.x, _stage_rect.end.x - panel_width))
|
|
panel_position.y = _group_rect.end.y + gap if place_below else _group_rect.position.y - panel_height - gap
|
|
panel_position.x = clampf(panel_position.x, _stage_rect.position.x, maxf(_stage_rect.position.x, _stage_rect.end.x - panel_width))
|
|
if preserve_top:
|
|
panel_position.y = pinned_top
|
|
else:
|
|
panel_position.y = clampf(panel_position.y, _stage_rect.position.y, maxf(_stage_rect.position.y, _stage_rect.end.y - panel_height))
|
|
panel_position = _position_avoiding_badges(panel_position, Vector2(panel_width, panel_height), _group_rect, preserve_top, pinned_top)
|
|
_panel.position = panel_position
|
|
_panel.size = Vector2(panel_width, panel_height)
|
|
_preserve_top_on_layout = false
|
|
|
|
|
|
func _position_avoiding_badges(preferred: Vector2, size: Vector2, anchor: Rect2, lock_y: bool, locked_y: float) -> Vector2:
|
|
var candidates: Array[Vector2] = [preferred]
|
|
var gap: float = _gap()
|
|
var center: Vector2 = anchor.get_center()
|
|
if lock_y:
|
|
candidates.append(Vector2(anchor.position.x - size.x - gap, locked_y))
|
|
candidates.append(Vector2(anchor.end.x + gap, locked_y))
|
|
candidates.append(Vector2(_stage_rect.position.x, locked_y))
|
|
candidates.append(Vector2(_stage_rect.end.x - size.x, locked_y))
|
|
else:
|
|
candidates.append(Vector2(anchor.position.x - size.x - gap, center.y - size.y * 0.5))
|
|
candidates.append(Vector2(anchor.end.x + gap, center.y - size.y * 0.5))
|
|
candidates.append(Vector2(center.x - size.x * 0.5, anchor.position.y - size.y - gap))
|
|
candidates.append(Vector2(center.x - size.x * 0.5, anchor.end.y + gap))
|
|
candidates.append(Vector2(_stage_rect.position.x, _stage_rect.position.y))
|
|
candidates.append(Vector2(_stage_rect.end.x - size.x, _stage_rect.position.y))
|
|
candidates.append(Vector2(_stage_rect.position.x, _stage_rect.end.y - size.y))
|
|
candidates.append(Vector2(_stage_rect.end.x - size.x, _stage_rect.end.y - size.y))
|
|
candidates.append(_stage_rect.get_center() - size * 0.5)
|
|
var best: Vector2 = _clamp_panel_position(preferred, size, lock_y, locked_y)
|
|
var best_collisions: int = _badge_collision_count(best, size)
|
|
var best_distance: float = best.distance_squared_to(preferred)
|
|
for candidate: Vector2 in candidates:
|
|
var clamped: Vector2 = _clamp_panel_position(candidate, size, lock_y, locked_y)
|
|
var collisions: int = _badge_collision_count(clamped, size)
|
|
var distance: float = clamped.distance_squared_to(preferred)
|
|
if collisions < best_collisions or (collisions == best_collisions and distance < best_distance):
|
|
best = clamped
|
|
best_collisions = collisions
|
|
best_distance = distance
|
|
return best
|
|
|
|
|
|
func _clamp_panel_position(candidate: Vector2, size: Vector2, lock_y: bool, locked_y: float) -> Vector2:
|
|
var y: float = locked_y if lock_y else clampf(candidate.y, _stage_rect.position.y, maxf(_stage_rect.position.y, _stage_rect.end.y - size.y))
|
|
return Vector2(clampf(candidate.x, _stage_rect.position.x, maxf(_stage_rect.position.x, _stage_rect.end.x - size.x)), y)
|
|
|
|
|
|
func _badge_collision_count(position: Vector2, size: Vector2) -> int:
|
|
var panel_rect: Rect2 = Rect2(position, size)
|
|
var count: int = 0
|
|
for badge_rect: Rect2 in _badge_avoidance_rects:
|
|
if panel_rect.intersects(badge_rect):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _rect_changed(previous: Rect2, next: Rect2, epsilon: float = 2.0) -> bool:
|
|
if previous.size == Vector2.ZERO:
|
|
return true
|
|
return previous.position.distance_to(next.position) > epsilon or previous.size.distance_to(next.size) > epsilon
|
|
|
|
|
|
func _fit_panel_height(panel_width: float, available_height: float) -> float:
|
|
_panel.custom_minimum_size = Vector2(panel_width, 0.0)
|
|
_body_scroll.custom_minimum_size.y = 0.0
|
|
_panel.reset_size()
|
|
var fixed_height: float = _panel.get_combined_minimum_size().y
|
|
var body_height: float = _body_content.get_combined_minimum_size().y
|
|
var max_height: float = maxf(1.0, available_height)
|
|
var body_room: float = maxf(48.0, max_height - fixed_height)
|
|
_body_scroll.custom_minimum_size.y = minf(body_height, body_room)
|
|
_panel.reset_size()
|
|
return minf(maxf(1.0, _panel.get_combined_minimum_size().y), max_height)
|
|
|
|
|
|
func _apply_theme() -> void:
|
|
if _panel == null:
|
|
return
|
|
_panel.add_theme_stylebox_override("panel", _panel_style())
|
|
if _title_ribbon != null:
|
|
_title_ribbon.add_theme_stylebox_override("panel", _ribbon_style())
|
|
_refresh_choices()
|
|
_apply_action_button_styles()
|
|
|
|
|
|
func _apply_choice_style(button: Button, selected: bool) -> void:
|
|
button.add_theme_stylebox_override("normal", _choice_style(selected, false))
|
|
button.add_theme_stylebox_override("hover", _choice_style(selected, true))
|
|
button.add_theme_stylebox_override("pressed", _choice_style(selected, true))
|
|
button.add_theme_stylebox_override("focus", _choice_style(selected, true))
|
|
button.add_theme_color_override("font_color", Color.WHITE)
|
|
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
|
button.add_theme_color_override("font_pressed_color", Color.WHITE)
|
|
|
|
|
|
func _action_button(caption: String) -> Button:
|
|
var button: Button = Button.new()
|
|
button.text = caption
|
|
button.custom_minimum_size = Vector2(136.0, _hit_height())
|
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
button.focus_mode = Control.FOCUS_NONE
|
|
button.add_theme_font_size_override("font_size", 17)
|
|
return button
|
|
|
|
|
|
func _apply_action_button_styles() -> void:
|
|
for button: Button in [_preview_button, _done_button, _cancel_button, _more_button]:
|
|
if button == null:
|
|
continue
|
|
button.add_theme_stylebox_override("normal", _choice_style(false, false))
|
|
button.add_theme_stylebox_override("hover", _choice_style(false, true))
|
|
button.add_theme_stylebox_override("pressed", _choice_style(false, true))
|
|
button.add_theme_color_override("font_color", Color.WHITE)
|
|
for button: Button in _layout_preset_buttons:
|
|
button.add_theme_stylebox_override("normal", _choice_style(false, false))
|
|
button.add_theme_stylebox_override("hover", _choice_style(false, true))
|
|
button.add_theme_stylebox_override("pressed", _choice_style(false, true))
|
|
button.add_theme_color_override("font_color", Color.WHITE)
|
|
if _connect_bubbles_button != null:
|
|
_connect_bubbles_button.add_theme_stylebox_override("normal", _choice_style(false, false))
|
|
_connect_bubbles_button.add_theme_stylebox_override("hover", _choice_style(bool(_action.get("connect_bubbles", false)), true))
|
|
_connect_bubbles_button.add_theme_stylebox_override("pressed", _choice_style(bool(_action.get("connect_bubbles", false)), true))
|
|
_connect_bubbles_button.add_theme_color_override("font_color", Color.WHITE)
|
|
_refresh_layout_buttons()
|
|
|
|
|
|
func _panel_style() -> StyleBoxFlat:
|
|
var style: StyleBoxFlat = StyleBoxFlat.new()
|
|
style.bg_color = _theme.color("speech.toolbar.surface", Color("#1C2431F7")) if _theme != null else Color("#1C2431F7")
|
|
style.border_color = _border_color()
|
|
style.set_border_width_all(2)
|
|
style.set_corner_radius_all(12)
|
|
style.set_content_margin_all(8)
|
|
return style
|
|
|
|
|
|
func _ribbon_style() -> StyleBoxFlat:
|
|
var style: StyleBoxFlat = StyleBoxFlat.new()
|
|
style.bg_color = _theme.color("speech.toolbar.ribbon", Color("#2385A0")) if _theme != null else Color("#2385A0")
|
|
style.set_corner_radius_all(7)
|
|
style.set_content_margin(SIDE_LEFT, 9.0)
|
|
style.set_content_margin(SIDE_TOP, 3.0)
|
|
style.set_content_margin(SIDE_RIGHT, 9.0)
|
|
style.set_content_margin(SIDE_BOTTOM, 3.0)
|
|
return style
|
|
|
|
|
|
func _choice_style(selected: bool, hovered: bool) -> StyleBoxFlat:
|
|
var style: StyleBoxFlat = StyleBoxFlat.new()
|
|
style.bg_color = Color("#176B83") if selected else (Color("#344154") if hovered else Color("#202733"))
|
|
style.border_color = _selection_color() if selected else (Color("#D8E0EA") if hovered else _border_color())
|
|
style.set_border_width_all(4 if selected else 2)
|
|
style.set_corner_radius_all(8)
|
|
style.set_content_margin_all(6)
|
|
return style
|
|
|
|
|
|
func _hit_height() -> float:
|
|
return _theme.speech_toolbar_number("min_hit_height", 56.0, 44.0, 80.0) if _theme != null else 56.0
|
|
|
|
|
|
func _max_width() -> float:
|
|
return _theme.speech_toolbar_number("max_width", 440.0, 260.0, 620.0) if _theme != null else 440.0
|
|
|
|
|
|
func _gap() -> float:
|
|
return _theme.speech_toolbar_number("gap", 12.0, 6.0, 24.0) if _theme != null else 12.0
|
|
|
|
|
|
func _border_color() -> Color:
|
|
return _theme.color("speech.toolbar.border", Color("#9EADBF")) if _theme != null else Color("#9EADBF")
|
|
|
|
|
|
func _selection_color() -> Color:
|
|
return _theme.color("speech.toolbar.selection_halo", Color("#FFD84D")) if _theme != null else Color("#FFD84D")
|