From 26da06571608b86ee55ed76e01ff51edafed98ae Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 31 Jul 2026 14:27:27 -0400 Subject: [PATCH 1/4] Refine chat layout and controls --- main/main.gd | 3 + ui/chat_ui.gd | 415 ++++++++++++++++++++++++++++++---- ui/game_ui.gd | 16 +- ui/ui_motion.gd | 1 + ui/ui_pixelation_presenter.gd | 19 +- 5 files changed, 401 insertions(+), 53 deletions(-) diff --git a/main/main.gd b/main/main.gd index 2564315..f62c696 100644 --- a/main/main.gd +++ b/main/main.gd @@ -449,6 +449,9 @@ func _initialize_after_data_root() -> void: _game_ui.interactive_pointer_ui_changed.connect( _ui_pixelation.set_interactive_ui_open ) + _game_ui.passive_pointer_ui_changed.connect( + _ui_pixelation.set_passive_pointer_ui_enabled + ) _game_ui.player_menu_backdrop_visibility_changed.connect( _set_player_menu_backdrop_visible ) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 54c9f1a..90dff30 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -6,8 +6,27 @@ const RECENT_SECONDS: float = 8.0 const SPEECH_SECONDS: float = 6.0 const DRAFT_SAVE_DELAY: float = 0.4 const IDLE_ALPHA: float = 0.58 -const PANEL_POSITION := Vector2(30, 376) -const PANEL_SIZE := Vector2(390, 250) +const PANEL_WIDTH: float = 390.0 +const COMPACT_HEIGHT: float = 250.0 +const BOTTOM_MARGIN: float = 94.0 +const MIN_TOP_MARGIN: float = 24.0 +const MIN_HISTORY_HEIGHT: float = 92.0 +const HISTORY_FONT_SIZE: int = 17 +const INPUT_FONT_SIZE: int = 23 +const HINT_FONT_SIZE: int = 12 +const HISTORY_INPUT_GAP: int = 8 +const HANDLE_SIZE := Vector2(34, 42) +const HANDLE_GAP: float = 6.0 +const COLLAPSED_REVEAL_WIDTH: float = 8.0 +const HINT_EDGE_MARGIN: float = 4.0 + +enum PresentationState { + COLLAPSED, + COMPACT, + EXPANDED, +} + +signal text_entry_ownership_changed(active: bool) var _service: NetworkChatService var _session: NetworkSession @@ -15,20 +34,23 @@ var _spawn: PlayerSpawnService var _player: Player var _fishing_spot: FishingSpot var _settings: PlayerSettingsManager -var _history: Label +var _history: RichTextLabel var _entry: LineEdit var _status: Label var _panel: PanelContainer var _collapse_button: Button +var _height_button: Button var _unread_indicator: Label var _hint: Label var _speech_layer: Control var _speech: Dictionary[int, Dictionary] = {} var _draft_save_timer: Timer var _opacity_tween: Tween +var _height_tween: Tween var _opened: bool = false var _available: bool = false -var _collapsed: bool = false +var _presentation_state := PresentationState.COMPACT +var _visible_state_before_collapse := PresentationState.COMPACT var _panel_hovered: bool = false var _collapsed_has_unread: bool = false var _last_message_time: float = -INF @@ -43,6 +65,7 @@ func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) mouse_filter = Control.MOUSE_FILTER_IGNORE _build_ui() + resized.connect(_on_viewport_resized) set_process(true) @@ -67,7 +90,13 @@ func setup( _session.peer_removed.connect(_on_peer_removed) _entry.text = _settings.current_settings.chat_draft _entry.caret_column = _entry.text.length() - _set_collapsed(_settings.current_settings.chat_collapsed, false) + _set_presentation_state( + PresentationState.COLLAPSED + if _settings.current_settings.chat_collapsed + else PresentationState.COMPACT, + false, + false, + ) _refresh_history() @@ -77,8 +106,10 @@ func open_chat() -> void: or not _session.is_gameplay_session_active() ): return - _set_collapsed(false) + if _presentation_state == PresentationState.COLLAPSED: + _set_presentation_state(_visible_state_before_collapse, true, true) _opened = true + text_entry_ownership_changed.emit(true) _prior_movement = _player.is_movement_enabled() _prior_camera = _player.is_camera_input_enabled() _player.set_movement_enabled(false) @@ -109,6 +140,7 @@ func close_chat() -> void: if not _opened: return _opened = false + text_entry_ownership_changed.emit(false) _entry.release_focus() _entry.hide() _player.set_movement_enabled(_prior_movement) @@ -149,8 +181,7 @@ func _exit_tree() -> void: func _build_ui() -> void: _panel = PanelContainer.new() - _panel.position = PANEL_POSITION - _panel.size = PANEL_SIZE + _panel.name = "ChatPanel" _panel.mouse_filter = Control.MOUSE_FILTER_STOP _panel.add_theme_stylebox_override("panel", _chat_panel_style()) _panel.mouse_entered.connect(func() -> void: @@ -163,46 +194,93 @@ func _build_ui() -> void: ) add_child(_panel) var box := VBoxContainer.new() - box.add_theme_constant_override("separation", 6) + box.add_theme_constant_override("separation", HISTORY_INPUT_GAP) _panel.add_child(box) - _history = Label.new() - _history.custom_minimum_size = Vector2(360, 158) + _history = RichTextLabel.new() + _history.name = "ChatHistory" + _history.custom_minimum_size = Vector2(360, MIN_HISTORY_HEIGHT) + _history.size_flags_vertical = Control.SIZE_EXPAND_FILL _history.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _history.fit_content = false + _history.scroll_active = true + _history.scroll_following = false _history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM - _history.mouse_filter = Control.MOUSE_FILTER_IGNORE - _history.add_theme_font_override("font", UtilityPageStyle.TuffyFont) - _history.add_theme_color_override("font_color", UtilityPageStyle.LIGHT_TEXT) + _history.mouse_filter = Control.MOUSE_FILTER_STOP + _history.add_theme_font_override("normal_font", UtilityPageStyle.TuffyFont) + _history.add_theme_font_size_override("normal_font_size", HISTORY_FONT_SIZE) + _history.add_theme_color_override("default_color", UtilityPageStyle.LIGHT_TEXT) + _history.add_theme_stylebox_override("normal", _borderless_style(Color.TRANSPARENT)) box.add_child(_history) _status = Label.new() _status.custom_minimum_size = Vector2(360, 20) + _status.hide() _status.mouse_filter = Control.MOUSE_FILTER_IGNORE _status.add_theme_font_override("font", UtilityPageStyle.TuffyFont) _status.add_theme_color_override("font_color", Color("ffd6a1")) box.add_child(_status) _entry = LineEdit.new() + _entry.name = "ChatEntry" _entry.placeholder_text = "type a message…" _entry.max_length = NetworkChatProtocol.MAX_VISIBLE_CHARACTERS _entry.text_submitted.connect(func(_value: String) -> void: _send()) _entry.text_changed.connect(_on_draft_changed) - UtilityPageStyle.apply_line_edit(_entry) + UtilityPageStyle.apply_ocean_line_edit(_entry) + _entry.add_theme_font_size_override("font_size", INPUT_FONT_SIZE) box.add_child(_entry) _entry.hide() _collapse_button = Button.new() - _collapse_button.position = Vector2(2, PANEL_POSITION.y + 101) - _collapse_button.size = Vector2(34, 48) + _collapse_button.name = "ChatCollapseButton" + _collapse_button.size = HANDLE_SIZE + _collapse_button.mouse_filter = Control.MOUSE_FILTER_STOP + _collapse_button.focus_mode = Control.FOCUS_ALL + _collapse_button.z_index = 3 _collapse_button.tooltip_text = "Collapse chat" _collapse_button.pressed.connect(func() -> void: - _set_collapsed(not _collapsed) + if _presentation_state == PresentationState.COLLAPSED: + _set_presentation_state( + _visible_state_before_collapse, + true, + true, + ) + else: + if _opened: + close_chat() + _set_presentation_state(PresentationState.COLLAPSED, true, true) ) _collapse_button.focus_entered.connect(func() -> void: _update_panel_opacity(true) ) - UtilityPageStyle.apply_button(_collapse_button) + _collapse_button.mouse_entered.connect(_on_exterior_handle_entered) + _collapse_button.mouse_exited.connect(_on_exterior_handle_exited) + _apply_flat_chat_button(_collapse_button) add_child(_collapse_button) + _height_button = Button.new() + _height_button.name = "ChatHeightButton" + _height_button.size = HANDLE_SIZE + _height_button.mouse_filter = Control.MOUSE_FILTER_STOP + _height_button.focus_mode = Control.FOCUS_ALL + _height_button.z_index = 3 + _height_button.pressed.connect(func() -> void: + _set_presentation_state( + PresentationState.COMPACT + if _presentation_state == PresentationState.EXPANDED + else PresentationState.EXPANDED, + true, + true, + ) + ) + _height_button.focus_entered.connect(func() -> void: + _update_panel_opacity(true) + ) + _height_button.mouse_entered.connect(_on_exterior_handle_entered) + _height_button.mouse_exited.connect(_on_exterior_handle_exited) + _apply_flat_chat_button(_height_button) + add_child(_height_button) _unread_indicator = Label.new() + _unread_indicator.name = "ChatUnreadIndicator" _unread_indicator.text = "•" - _unread_indicator.position = Vector2(8, PANEL_POSITION.y + 86) _unread_indicator.size = Vector2(22, 20) + _unread_indicator.z_index = 4 _unread_indicator.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _unread_indicator.mouse_filter = Control.MOUSE_FILTER_IGNORE _unread_indicator.add_theme_font_override( @@ -212,11 +290,12 @@ func _build_ui() -> void: _unread_indicator.add_theme_color_override("font_color", Color("ffe08a")) add_child(_unread_indicator) _hint = Label.new() + _hint.name = "ChatHint" _hint.text = "Press Enter to chat" - _hint.position = Vector2(30, 655) - _hint.size = Vector2(260, 28) + _hint.size = Vector2(180, 18) _hint.mouse_filter = Control.MOUSE_FILTER_IGNORE _hint.add_theme_font_override("font", UtilityPageStyle.TuffyFont) + _hint.add_theme_font_size_override("font_size", HINT_FONT_SIZE) _hint.add_theme_color_override( "font_color", Color(0.94, 0.91, 0.80, 0.72) ) @@ -235,13 +314,14 @@ func _build_ui() -> void: add_child(_draft_save_timer) _panel.hide() _collapse_button.hide() + _height_button.hide() _unread_indicator.hide() _hint.hide() + _layout_presentation(false) func _chat_panel_style() -> StyleBoxFlat: - var style := UtilityPageStyle.button_style(Color(0.025, 0.13, 0.19, 0.94)) - style.border_color = Color(0.75, 0.84, 0.74, 0.66) + var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94)) style.set_corner_radius_all(10) style.content_margin_left = 12 style.content_margin_right = 12 @@ -250,6 +330,62 @@ func _chat_panel_style() -> StyleBoxFlat: return style +func _borderless_style(color: Color) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = color + style.set_border_width_all(0) + return style + + +func _flat_chat_button_style(color: Color) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = color + style.set_border_width_all(0) + style.set_corner_radius_all(0) + style.anti_aliasing = false + style.shadow_size = 0 + style.shadow_color = Color.TRANSPARENT + style.shadow_offset = Vector2.ZERO + style.content_margin_left = 0 + style.content_margin_right = 0 + style.content_margin_top = 0 + style.content_margin_bottom = 0 + return style + + +func _apply_flat_chat_button(button: Button) -> void: + button.add_theme_font_override("font", UtilityPageStyle.TuffyFont) + button.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY + ) + button.add_theme_color_override("font_hover_color", Color.WHITE) + button.add_theme_color_override("font_pressed_color", Color.WHITE) + button.add_theme_color_override("font_focus_color", Color.WHITE) + button.add_theme_color_override( + "font_disabled_color", + Color(UtilityPageStyle.OCEAN_TEXT_SECONDARY, 0.52), + ) + button.add_theme_stylebox_override( + "normal", _flat_chat_button_style(UtilityPageStyle.OCEAN_BUTTON) + ) + button.add_theme_stylebox_override( + "hover", _flat_chat_button_style(UtilityPageStyle.OCEAN_BUTTON_HOVER) + ) + button.add_theme_stylebox_override( + "pressed", _flat_chat_button_style(UtilityPageStyle.OCEAN_SELECTED) + ) + button.add_theme_stylebox_override( + "hover_pressed", + _flat_chat_button_style(UtilityPageStyle.OCEAN_SELECTED), + ) + button.add_theme_stylebox_override( + "focus", _flat_chat_button_style(UtilityPageStyle.OCEAN_SELECTED) + ) + button.add_theme_stylebox_override( + "disabled", _flat_chat_button_style(UtilityPageStyle.OCEAN_DISABLED) + ) + + func _speech_panel_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(UtilityPageStyle.PAPER_ALT, 0.96) @@ -278,7 +414,7 @@ func _send() -> void: _pending_send_body = "" _entry.editable = true elif _send_pending: - _status.text = "Sending…" + _set_status("Sending…") func _on_local_message_confirmed(message: Dictionary) -> void: @@ -291,7 +427,7 @@ func _on_local_message_confirmed(message: Dictionary) -> void: _pending_send_body = "" _entry.editable = true _entry.clear() - _status.text = "" + _set_status("") _flush_draft() close_chat() @@ -299,7 +435,7 @@ func _on_local_message_confirmed(message: Dictionary) -> void: func _on_message(message: Dictionary) -> void: _last_message_time = Time.get_ticks_msec() / 1000.0 _refresh_history() - if _collapsed: + if _presentation_state == PresentationState.COLLAPSED: _collapsed_has_unread = true _unread_indicator.show() _update_panel_opacity(true) @@ -348,7 +484,7 @@ func _on_rejected(message: String) -> void: _send_pending = false _pending_send_body = "" _entry.editable = true - _status.text = message + _set_status(message) if not _opened: open_chat() @@ -356,10 +492,10 @@ func _on_rejected(message: String) -> void: func _refresh_history() -> void: if _service == null: return + var scroll_state := _capture_history_scroll() var lines: Array[String] = [] var messages := _service.get_history() - var start := maxi(0, messages.size() - 8) - for message: Dictionary in messages.slice(start): + for message: Dictionary in messages: if int(message["kind"]) == NetworkChatProtocol.Kind.SYSTEM: lines.append("• %s" % str(message["body"])) else: @@ -367,38 +503,76 @@ func _refresh_history() -> void: str(message["sender_display_name"]), str(message["body"]), ]) _history.text = "\n".join(lines) + _restore_history_scroll.call_deferred(scroll_state) func _refresh_visibility() -> void: if not _available: _panel.hide() _collapse_button.hide() + _height_button.hide() _unread_indicator.hide() _hint.hide() return _collapse_button.show() - _panel.visible = not _collapsed - _unread_indicator.visible = _collapsed and _collapsed_has_unread + var collapsed := _presentation_state == PresentationState.COLLAPSED + _panel.show() + _height_button.visible = not collapsed + _unread_indicator.visible = collapsed and _collapsed_has_unread _hint.visible = not _opened -func _set_collapsed(value: bool, persist: bool = true) -> void: - _collapsed = value - _collapse_button.text = "›" if _collapsed else "‹" - _collapse_button.tooltip_text = ( - "Expand chat" if _collapsed else "Collapse chat" - ) - if not _collapsed: +func _set_presentation_state( + state: PresentationState, + persist: bool = true, + animate: bool = false, +) -> void: + _refresh_handle_labels(state) + if state == _presentation_state and not animate: + _layout_presentation(false) + _refresh_visibility() + return + var previous_state := _presentation_state + if ( + state == PresentationState.COLLAPSED + and previous_state != PresentationState.COLLAPSED + ): + _visible_state_before_collapse = previous_state + _presentation_state = state + var collapsed := state == PresentationState.COLLAPSED + if not collapsed: _collapsed_has_unread = false _unread_indicator.hide() + _layout_presentation(animate) if persist and _settings != null: - _settings.update_chat_preferences(_entry.text, _collapsed) + _settings.update_chat_preferences(_entry.text, collapsed) _draft_save_timer.start() _refresh_visibility() +func _refresh_handle_labels(state: PresentationState) -> void: + var collapsed := state == PresentationState.COLLAPSED + _collapse_button.text = ">" if collapsed else "<" + _collapse_button.tooltip_text = ( + "Show chat" if collapsed else "Hide chat" + ) + _height_button.text = "v" if state == PresentationState.EXPANDED else "^" + _height_button.tooltip_text = ( + "Compact chat" + if state == PresentationState.EXPANDED + else "Expand chat history" + ) + + func _update_panel_opacity(immediate_recheck: bool = false) -> void: - if _panel == null or _collapsed or not _available: + if _panel == null or not _available: + return + if _presentation_state == PresentationState.COLLAPSED: + if _height_tween == null or not _height_tween.is_running(): + _panel.modulate.a = IDLE_ALPHA + _collapse_button.modulate.a = ( + 1.0 if _collapsed_has_unread else 0.82 + ) return var recent := ( Time.get_ticks_msec() / 1000.0 - _last_message_time < RECENT_SECONDS @@ -407,6 +581,7 @@ func _update_panel_opacity(immediate_recheck: bool = false) -> void: var wants_full := ( _opened or recent or _panel_hovered or focus_owner == _collapse_button + or focus_owner == _height_button or focus_owner == _entry ) var desired := 1.0 if wants_full else IDLE_ALPHA @@ -416,9 +591,11 @@ func _update_panel_opacity(immediate_recheck: bool = false) -> void: if _opacity_tween != null and _opacity_tween.is_valid(): _opacity_tween.kill() _opacity_tween = create_tween() - _opacity_tween.tween_property( - _panel, "modulate:a", desired, 0.18 - ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _opacity_tween.set_parallel(true) + for control: Control in [_panel, _collapse_button, _height_button]: + _opacity_tween.tween_property( + control, "modulate:a", desired, 0.18 + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) func _update_speech() -> void: @@ -463,17 +640,163 @@ func _update_speech() -> void: func _on_draft_changed(_value: String) -> void: if _settings == null: return - _settings.update_chat_preferences(_entry.text, _collapsed) + _settings.update_chat_preferences( + _entry.text, + _presentation_state == PresentationState.COLLAPSED, + ) _draft_save_timer.start() func _flush_draft() -> void: if _settings == null or _entry == null: return - _settings.update_chat_preferences(_entry.text, _collapsed) + _settings.update_chat_preferences( + _entry.text, + _presentation_state == PresentationState.COLLAPSED, + ) _settings.save_chat_preferences() +func _set_status(text: String) -> void: + _status.text = text + _status.visible = not text.is_empty() + + +func _layout_presentation(animate: bool) -> void: + if _panel == null: + return + var viewport_height := size.y + if viewport_height <= 0.0: + viewport_height = 720.0 + var bottom_margin := minf( + BOTTOM_MARGIN, + maxf(MIN_TOP_MARGIN, (viewport_height - MIN_HISTORY_HEIGHT) * 0.25), + ) + var bottom := viewport_height - bottom_margin + var height := COMPACT_HEIGHT + var layout_state := ( + _visible_state_before_collapse + if _presentation_state == PresentationState.COLLAPSED + else _presentation_state + ) + if layout_state == PresentationState.EXPANDED: + height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin) + height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN)) + var target_position := Vector2( + -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) + if _presentation_state == PresentationState.COLLAPSED + else 0.0, + bottom - height, + ) + var target_size := Vector2(PANEL_WIDTH, height) + var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP + var handle_stack_top := ( + bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5 + ) + var collapse_position := Vector2( + COLLAPSED_REVEAL_WIDTH + if _presentation_state == PresentationState.COLLAPSED + else PANEL_WIDTH, + bottom - COMPACT_HEIGHT * 0.5 - HANDLE_SIZE.y * 0.5 + if _presentation_state == PresentationState.COLLAPSED + else handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP, + ) + var height_position := Vector2( + COLLAPSED_REVEAL_WIDTH + if _presentation_state == PresentationState.COLLAPSED + else PANEL_WIDTH, + handle_stack_top, + ) + var unread_position := collapse_position + Vector2(6.0, -18.0) + var hint_position := Vector2( + HINT_EDGE_MARGIN, + viewport_height - _hint.size.y - HINT_EDGE_MARGIN, + ) + if _height_tween != null and _height_tween.is_valid(): + _height_tween.kill() + if not animate: + _panel.position = target_position + _panel.size = target_size + _collapse_button.position = collapse_position + _height_button.position = height_position + _unread_indicator.position = unread_position + _hint.position = hint_position + return + var scroll_state := _capture_history_scroll() + _height_tween = create_tween().set_parallel(true) + _height_tween.tween_property( + _panel, "position", target_position, UIMotion.CHAT_RESIZE_DURATION + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _height_tween.tween_property( + _panel, "size", target_size, UIMotion.CHAT_RESIZE_DURATION + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _height_tween.tween_property( + _collapse_button, + "position", + collapse_position, + UIMotion.CHAT_RESIZE_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _height_tween.tween_property( + _height_button, + "position", + height_position, + UIMotion.CHAT_RESIZE_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _height_tween.tween_property( + _unread_indicator, + "position", + unread_position, + UIMotion.CHAT_RESIZE_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _height_tween.finished.connect(func() -> void: + _restore_history_scroll(scroll_state) + ) + _hint.position = hint_position + + +func _capture_history_scroll() -> Dictionary: + if _history == null: + return {"pinned": true, "value": 0.0} + var scroll := _history.get_v_scroll_bar() + if scroll == null: + return {"pinned": true, "value": 0.0} + var bottom := maxf(scroll.min_value, scroll.max_value - scroll.page) + return { + "pinned": scroll.value >= bottom - 2.0, + "value": scroll.value, + } + + +func _restore_history_scroll(state: Dictionary) -> void: + if _history == null: + return + var scroll := _history.get_v_scroll_bar() + if scroll == null: + return + if bool(state.get("pinned", true)): + scroll.value = maxf(scroll.min_value, scroll.max_value - scroll.page) + else: + scroll.value = clampf( + float(state.get("value", 0.0)), + scroll.min_value, + maxf(scroll.min_value, scroll.max_value - scroll.page), + ) + + +func _on_viewport_resized() -> void: + _layout_presentation(false) + + +func _on_exterior_handle_entered() -> void: + _panel_hovered = true + _update_panel_opacity(true) + + +func _on_exterior_handle_exited() -> void: + _panel_hovered = false + _update_panel_opacity(true) + + func _on_peer_removed(peer_id: int) -> void: var state: Dictionary = _speech.get(peer_id, {}) var bubble := state.get("bubble") as PanelContainer diff --git a/ui/game_ui.gd b/ui/game_ui.gd index d720b70..af0c4b4 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -39,6 +39,7 @@ const PlayerSettingsManagerType = preload( signal pixelation_settings_visibility_changed(is_visible: bool) signal crisp_reset_focus_requested signal interactive_pointer_ui_changed(is_open: bool) +signal passive_pointer_ui_changed(is_enabled: bool) signal player_menu_backdrop_visibility_changed(is_visible: bool) @onready var _status_label: Label = %StatusLabel @@ -73,11 +74,15 @@ var _gameplay_ui_enabled: bool = false var _fishing_spot: FishingSpotType var _system_menu_open: bool = false var _shop_open: bool = false +var _chat_input_open: bool = false var _player_menu_hotbar_visible: bool = false var _item_effects: PlayerItemEffectsType func _ready() -> void: + _chat_ui.text_entry_ownership_changed.connect( + _on_chat_text_entry_ownership_changed + ) _hotbar_ui.presentation_transition_finished.connect( _on_hotbar_presentation_transition_finished ) @@ -615,16 +620,23 @@ func _refresh_hotbar_visibility() -> void: func _emit_interactive_pointer_ui_changed() -> void: interactive_pointer_ui_changed.emit( - _system_menu_open or _player_menu_open or _shop_open + _system_menu_open or _player_menu_open or _shop_open or _chat_input_open ) +func _on_chat_text_entry_ownership_changed(active: bool) -> void: + _chat_input_open = active + _emit_interactive_pointer_ui_changed() + + func _refresh_chat_availability() -> void: if _chat_ui == null: return - _chat_ui.set_available( + var chat_available := ( _gameplay_ui_enabled and not _system_menu_open and not _player_menu_open and not _shop_open ) + _chat_ui.set_available(chat_available) + passive_pointer_ui_changed.emit(chat_available) diff --git a/ui/ui_motion.gd b/ui/ui_motion.gd index 83ac66a..e538744 100644 --- a/ui/ui_motion.gd +++ b/ui/ui_motion.gd @@ -12,6 +12,7 @@ const PLAYER_MENU_PAGE_DURATION: float = 0.12 const PLAYER_MENU_INVENTORY_DURATION: float = 0.10 const PLAYER_MENU_ENTER_SCALE: float = 0.985 const PLAYER_MENU_EXIT_SCALE: float = 0.99 +const CHAT_RESIZE_DURATION: float = 0.15 static func bubble_duration(distance: float) -> float: diff --git a/ui/ui_pixelation_presenter.gd b/ui/ui_pixelation_presenter.gd index 1c79d28..ec41217 100644 --- a/ui/ui_pixelation_presenter.gd +++ b/ui/ui_pixelation_presenter.gd @@ -18,6 +18,7 @@ var _requested_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE var _effective_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE var _gameplay_active: bool = false var _interactive_ui_open: bool = false +var _passive_pointer_ui_enabled: bool = false func _ready() -> void: @@ -57,12 +58,20 @@ func set_interactive_ui_open(is_open: bool) -> void: _refresh_mouse_filter() +func set_passive_pointer_ui_enabled(is_enabled: bool) -> void: + _passive_pointer_ui_enabled = is_enabled + _refresh_mouse_filter() + + func _refresh_mouse_filter() -> void: - mouse_filter = ( - Control.MOUSE_FILTER_STOP - if not _gameplay_active or _interactive_ui_open - else Control.MOUSE_FILTER_IGNORE - ) + if not _gameplay_active or _interactive_ui_open: + mouse_filter = Control.MOUSE_FILTER_STOP + elif _passive_pointer_ui_enabled: + # Forward pointer events to lightweight gameplay overlays such as Chat, + # while allowing events they do not consume to continue to gameplay. + mouse_filter = Control.MOUSE_FILTER_PASS + else: + mouse_filter = Control.MOUSE_FILTER_IGNORE func _resize_presentation() -> void: From cb97ae9cc433b61fecf9a1e0a8ecad155a0ce87c Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 31 Jul 2026 14:30:52 -0400 Subject: [PATCH 2/4] Polish chat panel geometry --- ui/chat_ui.gd | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 90dff30..ff6c163 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -7,6 +7,7 @@ const SPEECH_SECONDS: float = 6.0 const DRAFT_SAVE_DELAY: float = 0.4 const IDLE_ALPHA: float = 0.58 const PANEL_WIDTH: float = 390.0 +const EXPANDED_WIDTH: float = PANEL_WIDTH * 0.85 const COMPACT_HEIGHT: float = 250.0 const BOTTOM_MARGIN: float = 94.0 const MIN_TOP_MARGIN: float = 24.0 @@ -198,7 +199,7 @@ func _build_ui() -> void: _panel.add_child(box) _history = RichTextLabel.new() _history.name = "ChatHistory" - _history.custom_minimum_size = Vector2(360, MIN_HISTORY_HEIGHT) + _history.custom_minimum_size = Vector2(0, MIN_HISTORY_HEIGHT) _history.size_flags_vertical = Control.SIZE_EXPAND_FILL _history.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _history.fit_content = false @@ -212,7 +213,7 @@ func _build_ui() -> void: _history.add_theme_stylebox_override("normal", _borderless_style(Color.TRANSPARENT)) box.add_child(_history) _status = Label.new() - _status.custom_minimum_size = Vector2(360, 20) + _status.custom_minimum_size = Vector2(0, 20) _status.hide() _status.mouse_filter = Control.MOUSE_FILTER_IGNORE _status.add_theme_font_override("font", UtilityPageStyle.TuffyFont) @@ -322,7 +323,10 @@ func _build_ui() -> void: func _chat_panel_style() -> StyleBoxFlat: var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94)) - style.set_corner_radius_all(10) + style.corner_radius_top_left = 0 + style.corner_radius_bottom_left = 0 + style.corner_radius_top_right = 10 + style.corner_radius_bottom_right = 10 style.content_margin_left = 12 style.content_margin_right = 12 style.content_margin_top = 10 @@ -681,14 +685,19 @@ func _layout_presentation(animate: bool) -> void: ) if layout_state == PresentationState.EXPANDED: height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin) + var panel_width := ( + EXPANDED_WIDTH + if layout_state == PresentationState.EXPANDED + else PANEL_WIDTH + ) height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN)) var target_position := Vector2( - -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) + -(panel_width - COLLAPSED_REVEAL_WIDTH) if _presentation_state == PresentationState.COLLAPSED else 0.0, bottom - height, ) - var target_size := Vector2(PANEL_WIDTH, height) + var target_size := Vector2(panel_width, height) var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP var handle_stack_top := ( bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5 @@ -696,7 +705,7 @@ func _layout_presentation(animate: bool) -> void: var collapse_position := Vector2( COLLAPSED_REVEAL_WIDTH if _presentation_state == PresentationState.COLLAPSED - else PANEL_WIDTH, + else panel_width, bottom - COMPACT_HEIGHT * 0.5 - HANDLE_SIZE.y * 0.5 if _presentation_state == PresentationState.COLLAPSED else handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP, @@ -704,7 +713,7 @@ func _layout_presentation(animate: bool) -> void: var height_position := Vector2( COLLAPSED_REVEAL_WIDTH if _presentation_state == PresentationState.COLLAPSED - else PANEL_WIDTH, + else panel_width, handle_stack_top, ) var unread_position := collapse_position + Vector2(6.0, -18.0) From a0d2fff99dbb32c3d68c5eadea61192a141a9d49 Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 31 Jul 2026 14:34:37 -0400 Subject: [PATCH 3/4] Keep chat controls consistent when hidden --- ui/chat_ui.gd | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index ff6c163..8cfac40 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -6,8 +6,8 @@ const RECENT_SECONDS: float = 8.0 const SPEECH_SECONDS: float = 6.0 const DRAFT_SAVE_DELAY: float = 0.4 const IDLE_ALPHA: float = 0.58 -const PANEL_WIDTH: float = 390.0 -const EXPANDED_WIDTH: float = PANEL_WIDTH * 0.85 +const ORIGINAL_PANEL_WIDTH: float = 390.0 +const PANEL_WIDTH: float = ORIGINAL_PANEL_WIDTH * 0.85 const COMPACT_HEIGHT: float = 250.0 const BOTTOM_MARGIN: float = 94.0 const MIN_TOP_MARGIN: float = 24.0 @@ -262,6 +262,15 @@ func _build_ui() -> void: _height_button.focus_mode = Control.FOCUS_ALL _height_button.z_index = 3 _height_button.pressed.connect(func() -> void: + if _presentation_state == PresentationState.COLLAPSED: + _visible_state_before_collapse = ( + PresentationState.COMPACT + if _visible_state_before_collapse == PresentationState.EXPANDED + else PresentationState.EXPANDED + ) + _refresh_handle_labels(PresentationState.COLLAPSED) + _layout_presentation(true) + return _set_presentation_state( PresentationState.COMPACT if _presentation_state == PresentationState.EXPANDED @@ -521,7 +530,7 @@ func _refresh_visibility() -> void: _collapse_button.show() var collapsed := _presentation_state == PresentationState.COLLAPSED _panel.show() - _height_button.visible = not collapsed + _height_button.show() _unread_indicator.visible = collapsed and _collapsed_has_unread _hint.visible = not _opened @@ -531,17 +540,17 @@ func _set_presentation_state( persist: bool = true, animate: bool = false, ) -> void: - _refresh_handle_labels(state) - if state == _presentation_state and not animate: - _layout_presentation(false) - _refresh_visibility() - return var previous_state := _presentation_state if ( state == PresentationState.COLLAPSED and previous_state != PresentationState.COLLAPSED ): _visible_state_before_collapse = previous_state + _refresh_handle_labels(state) + if state == _presentation_state and not animate: + _layout_presentation(false) + _refresh_visibility() + return _presentation_state = state var collapsed := state == PresentationState.COLLAPSED if not collapsed: @@ -556,14 +565,15 @@ func _set_presentation_state( func _refresh_handle_labels(state: PresentationState) -> void: var collapsed := state == PresentationState.COLLAPSED + var height_state := _visible_state_before_collapse if collapsed else state _collapse_button.text = ">" if collapsed else "<" _collapse_button.tooltip_text = ( "Show chat" if collapsed else "Hide chat" ) - _height_button.text = "v" if state == PresentationState.EXPANDED else "^" + _height_button.text = "v" if height_state == PresentationState.EXPANDED else "^" _height_button.tooltip_text = ( "Compact chat" - if state == PresentationState.EXPANDED + if height_state == PresentationState.EXPANDED else "Expand chat history" ) @@ -577,6 +587,7 @@ func _update_panel_opacity(immediate_recheck: bool = false) -> void: _collapse_button.modulate.a = ( 1.0 if _collapsed_has_unread else 0.82 ) + _height_button.modulate.a = 0.82 return var recent := ( Time.get_ticks_msec() / 1000.0 - _last_message_time < RECENT_SECONDS @@ -685,19 +696,14 @@ func _layout_presentation(animate: bool) -> void: ) if layout_state == PresentationState.EXPANDED: height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin) - var panel_width := ( - EXPANDED_WIDTH - if layout_state == PresentationState.EXPANDED - else PANEL_WIDTH - ) height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN)) var target_position := Vector2( - -(panel_width - COLLAPSED_REVEAL_WIDTH) + -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) if _presentation_state == PresentationState.COLLAPSED else 0.0, bottom - height, ) - var target_size := Vector2(panel_width, height) + var target_size := Vector2(PANEL_WIDTH, height) var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP var handle_stack_top := ( bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5 @@ -705,7 +711,7 @@ func _layout_presentation(animate: bool) -> void: var collapse_position := Vector2( COLLAPSED_REVEAL_WIDTH if _presentation_state == PresentationState.COLLAPSED - else panel_width, + else PANEL_WIDTH, bottom - COMPACT_HEIGHT * 0.5 - HANDLE_SIZE.y * 0.5 if _presentation_state == PresentationState.COLLAPSED else handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP, @@ -713,7 +719,7 @@ func _layout_presentation(animate: bool) -> void: var height_position := Vector2( COLLAPSED_REVEAL_WIDTH if _presentation_state == PresentationState.COLLAPSED - else panel_width, + else PANEL_WIDTH, handle_stack_top, ) var unread_position := collapse_position + Vector2(6.0, -18.0) From 53fdc9ef211f1202fd5e301ff19bffa7ba282704 Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 31 Jul 2026 14:36:07 -0400 Subject: [PATCH 4/4] Finalize chat messaging controls --- ui/chat_ui.gd | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 8cfac40..f67727a 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -712,9 +712,7 @@ func _layout_presentation(animate: bool) -> void: COLLAPSED_REVEAL_WIDTH if _presentation_state == PresentationState.COLLAPSED else PANEL_WIDTH, - bottom - COMPACT_HEIGHT * 0.5 - HANDLE_SIZE.y * 0.5 - if _presentation_state == PresentationState.COLLAPSED - else handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP, + handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP, ) var height_position := Vector2( COLLAPSED_REVEAL_WIDTH