Implement speech style toolbar live-edit refinements and whole speech show controls

- Added a new specification for the speech style toolbar live-edit, detailing immediate style application, scoped effect demonstration, and hover feedback.
- Introduced refinements to the speech style toolbar, including improved chooser sizing, exclusive stage editing during speech composition, and removal of redundant controls.
- Documented new stage controls for managing how the whole speech shows, including options for bubble arrangement, word appearance, and bubble motion.
- Created a new log file for speech padding syntax to track engine versioning.
This commit is contained in:
2026-10-03 14:49:22 -04:00
parent 6a1359f244
commit 6666173c4f
42 changed files with 9163 additions and 165 deletions
+626
View File
@@ -0,0 +1,626 @@
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")