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")