diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 95ebc2e..3c5dc77 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -3253,8 +3253,13 @@ func _finish_menu_entry(generation: int) -> void: _presentation_tween = null _transitioning = false _set_shell_interactive(true) + # A queued TAB press closes the menu immediately after its entry animation. + # Do that before scheduling deferred focus so the closing menu cannot leave a + # stale grab_focus call targeting controls that have just been disabled. + if _backpack_toggle_queued: + _consume_queued_backpack_toggle() + return _reset_controller_zone_for_section() - _consume_queued_backpack_toggle() func _begin_menu_exit(reason: CloseReason, restore_controls: bool) -> void: diff --git a/ui/players_page.gd b/ui/players_page.gd index 04e078b..7b77ef1 100644 --- a/ui/players_page.gd +++ b/ui/players_page.gd @@ -470,77 +470,94 @@ func _build_active_rows() -> void: _add_empty("No authenticated players.") return for entry: PlayerListEntry in entries: - var row := _make_row() - var identity := Label.new() - identity.custom_minimum_size.x = 250 - identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL - var markers: Array[String] = [] - if entry.is_host: - markers.append("host") - if entry.is_operator: - markers.append("operator") - if entry.is_local_player: - markers.append("You") - identity.text = "%s%s · %s\n%s" % [ - entry.display_name, - " [%s]" % ", ".join(markers) if not markers.is_empty() else "", - entry.compact_fingerprint, - entry.continuity_state, - ] - identity.tooltip_text = "Full identity fingerprint:\n%s" % ( - entry.full_fingerprint - ) - identity.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - row.add_child(identity) - var ping := Label.new() - ping.custom_minimum_size.x = 60 - ping.text = ( - "Local" if entry.is_host - else "%d ms" % entry.ping_to_host_ms - if entry.ping_to_host_ms >= 0 else "—" - ) - ping.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - row.add_child(ping) - var mute := Button.new() - var mute_action: String = "unmute" if entry.muted else "mute" - mute.disabled = entry.is_local_player - mute.pressed.connect(_toggle_mute.bind(entry)) - _configure_moderation_button(mute, MODERATION_MUTE_ICON, mute_action) - row.add_child(mute) - var block := Button.new() - block.disabled = entry.is_local_player - block.pressed.connect(_confirm_block.bind(entry)) - _configure_moderation_button(block, MODERATION_BLOCK_ICON, "block") - row.add_child(block) - if entry.can_manage_operator: - var operator := Button.new() - operator.text = "deop" if entry.is_operator else "op" - operator.pressed.connect(_confirm_operator.bind(entry)) - UtilityPageStyle.apply_compact_ocean_button(operator) - row.add_child(operator) - var clear_art := Button.new() - clear_art.text = "clear art" - clear_art.disabled = not entry.can_clear_art - clear_art.tooltip_text = ( - "Remove every shared artwork this player participated in." - ) - clear_art.pressed.connect(_confirm_clear_art.bind(entry)) - UtilityPageStyle.apply_compact_ocean_button(clear_art) - row.add_child(clear_art) - var kick := Button.new() - kick.disabled = not entry.can_kick - kick.pressed.connect(_confirm_kick.bind(entry)) - _configure_moderation_button(kick, MODERATION_KICK_ICON, "kick") - row.add_child(kick) - var ban := Button.new() - ban.disabled = not entry.can_ban - ban.pressed.connect(_confirm_ban.bind(entry)) - _configure_moderation_button(ban, MODERATION_BAN_ICON, "ban") - row.add_child(ban) + _build_active_player_row(entry) + + +func _build_active_player_row(entry: PlayerListEntry) -> void: + var row := _make_active_player_row() + var identity_row := HBoxContainer.new() + identity_row.add_theme_constant_override("separation", 8) + row.add_child(identity_row) + + var identity := Label.new() + identity.clip_text = true + identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var markers: Array[String] = [] + if entry.is_host: + markers.append("host") + if entry.is_operator: + markers.append("operator") + if entry.is_local_player: + markers.append("You") + identity.text = "%s%s · %s\n%s" % [ + entry.display_name, + " [%s]" % ", ".join(markers) if not markers.is_empty() else "", + entry.compact_fingerprint, + entry.continuity_state, + ] + identity.tooltip_text = "Full identity fingerprint:\n%s" % ( + entry.full_fingerprint + ) + identity.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY + ) + identity_row.add_child(identity) + var ping := Label.new() + ping.custom_minimum_size.x = 72 + ping.text = ( + "Local" if entry.is_host + else "%d ms" % entry.ping_to_host_ms + if entry.ping_to_host_ms >= 0 else "—" + ) + ping.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY + ) + ping.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + identity_row.add_child(ping) + + var actions := HBoxContainer.new() + actions.alignment = BoxContainer.ALIGNMENT_END + actions.add_theme_constant_override("separation", 6) + row.add_child(actions) + var mute := Button.new() + var mute_action: String = "unmute" if entry.muted else "mute" + mute.disabled = entry.is_local_player + mute.pressed.connect(_toggle_mute.bind(entry)) + _configure_moderation_button(mute, MODERATION_MUTE_ICON, mute_action) + actions.add_child(mute) + var block := Button.new() + block.disabled = entry.is_local_player + block.pressed.connect(_confirm_block.bind(entry)) + _configure_moderation_button(block, MODERATION_BLOCK_ICON, "block") + actions.add_child(block) + if entry.can_manage_operator: + var operator := Button.new() + operator.text = "deop" if entry.is_operator else "op" + operator.pressed.connect(_confirm_operator.bind(entry)) + UtilityPageStyle.apply_compact_ocean_button(operator) + operator.custom_minimum_size = MODERATION_BUTTON_SIZE + actions.add_child(operator) + var clear_art := Button.new() + clear_art.text = "clear art" + clear_art.disabled = not entry.can_clear_art + clear_art.tooltip_text = ( + "Remove every shared artwork this player participated in." + ) + clear_art.pressed.connect(_confirm_clear_art.bind(entry)) + UtilityPageStyle.apply_compact_ocean_button(clear_art) + clear_art.custom_minimum_size = Vector2(84.0, 40.0) + actions.add_child(clear_art) + var kick := Button.new() + kick.disabled = not entry.can_kick + kick.pressed.connect(_confirm_kick.bind(entry)) + _configure_moderation_button(kick, MODERATION_KICK_ICON, "kick") + actions.add_child(kick) + var ban := Button.new() + ban.disabled = not entry.can_ban + ban.pressed.connect(_confirm_ban.bind(entry)) + _configure_moderation_button(ban, MODERATION_BAN_ICON, "ban") + actions.add_child(ban) func _configure_moderation_button( @@ -580,8 +597,9 @@ func _build_session_artwork_controls() -> void: var counts: Vector2i = _service.get_session_artwork_counts() var row := _make_row() var label := Label.new() - label.custom_minimum_size.x = 600 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.clip_text = true + label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.text = "session artwork · %d layers · %d painted pixels" % [ counts.x, counts.y, ] @@ -611,8 +629,9 @@ func _build_relationship_rows() -> void: for record: Dictionary in records: var row := _make_row() var label := Label.new() - label.custom_minimum_size.x = 560 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.clip_text = true + label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var fingerprint := str(record["fingerprint"]) label.text = "%s · %s %s" % [ str(record.get("last_known_display_name", "Player")), @@ -651,8 +670,9 @@ func _build_ban_rows() -> void: var row := _make_row() var fingerprint := str(record["target_fingerprint"]) var label := Label.new() - label.custom_minimum_size.x = 600 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.clip_text = true + label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.text = "%s · %s banned %s" % [ str(record.get("last_known_display_name", "Player")), NetworkIdentityCrypto.compact_suffix(fingerprint), @@ -683,6 +703,20 @@ func _make_row() -> HBoxContainer: return row +func _make_active_player_row() -> VBoxContainer: + var panel := PanelContainer.new() + panel.clip_contents = true + panel.add_theme_stylebox_override( + "panel", UtilityPageStyle.row_style(false) + ) + _list.add_child(panel) + var row := VBoxContainer.new() + row.custom_minimum_size.y = 88 + row.add_theme_constant_override("separation", 4) + panel.add_child(row) + return row + + func _add_empty(text: String) -> void: var label := Label.new() label.text = text diff --git a/ui/the_net_page.gd b/ui/the_net_page.gd index 5f699ad..504128d 100644 --- a/ui/the_net_page.gd +++ b/ui/the_net_page.gd @@ -311,15 +311,17 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void: return for job: Dictionary in jobs: var row := PanelContainer.new() + row.clip_contents = true row.add_theme_stylebox_override( "panel", UtilityPageStyle.row_style(false) ) var content := HBoxContainer.new() - content.add_theme_constant_override("separation", 14) + content.clip_contents = true + content.add_theme_constant_override("separation", 10) row.add_child(content) var text_column := VBoxContainer.new() - text_column.custom_minimum_size.x = 245.0 + text_column.custom_minimum_size.x = 220.0 text_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL content.add_child(text_column) var title := Label.new() @@ -334,6 +336,8 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void: var description := Label.new() description.text = str(job.get("description", "")) description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + description.max_lines_visible = 2 + description.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS description.add_theme_font_size_override("font_size", 16) description.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY @@ -343,7 +347,8 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void: var target: int = int(job.get("target", 1)) var progress: int = int(job.get("progress", 0)) var progress_bar := ProgressBar.new() - progress_bar.custom_minimum_size = Vector2(100.0, 24.0) + progress_bar.custom_minimum_size = Vector2(104.0, 24.0) + progress_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER progress_bar.max_value = float(target) progress_bar.value = float(progress) progress_bar.show_percentage = false @@ -360,24 +365,30 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void: content.add_child(progress_bar) var count := Label.new() - count.custom_minimum_size.x = 54.0 - count.text = "%d / %d" % [progress, target] + count.custom_minimum_size.x = 82.0 + count.clip_text = true + count.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + count.text = "%s / %s" % [ + _compact_integer(progress), + _compact_integer(target), + ] + count.tooltip_text = "%d / %d" % [progress, target] count.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + count.add_theme_font_size_override("font_size", 14) count.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) content.add_child(count) - var reward := _make_reward_display( + var reward := _make_job_reward_display( int(job.get("fish_coin", 0)), int(job.get("experience", 0)), ) - reward.custom_minimum_size.x = 128.0 content.add_child(reward) var claim := Button.new() - claim.custom_minimum_size = Vector2(76.0, 36.0) + claim.custom_minimum_size = Vector2(72.0, 36.0) var claimable: bool = bool(job.get("claimable", false)) claim.text = "claim" if claimable else ( "done" if int(job.get("completed_count", 0)) > 0 else "working" @@ -457,17 +468,21 @@ func _claim(claim_id: String) -> void: func _make_reward_display(fish_coin: int, experience: int) -> HBoxContainer: var reward := HBoxContainer.new() - reward.mouse_filter = Control.MOUSE_FILTER_IGNORE + reward.tooltip_text = "%d fish coins · %d xp" % [fish_coin, experience] reward.add_theme_constant_override("separation", 7) var currency: CurrencyAmount = ( CurrencyPresentationType.instantiate_amount(fish_coin, 18.0) ) + currency.set_amount_text(_compact_integer(fish_coin)) + currency.tooltip_text = "%d fish coins" % fish_coin currency.get_amount_label().add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) reward.add_child(currency) var experience_label := Label.new() - experience_label.text = "• %d xp" % experience + experience_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + experience_label.text = "• %s xp" % _compact_integer(experience) + experience_label.tooltip_text = "%d xp" % experience experience_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER experience_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY @@ -476,6 +491,67 @@ func _make_reward_display(fish_coin: int, experience: int) -> HBoxContainer: return reward +func _make_job_reward_display( + fish_coin: int, + experience: int, +) -> VBoxContainer: + var reward := VBoxContainer.new() + reward.custom_minimum_size.x = 104.0 + reward.tooltip_text = "%d fish coins · %d xp" % [fish_coin, experience] + reward.add_theme_constant_override("separation", 1) + var currency: CurrencyAmount = ( + CurrencyPresentationType.instantiate_amount(fish_coin, 16.0) + ) + currency.set_amount_text(_compact_integer(fish_coin)) + currency.tooltip_text = "%d fish coins" % fish_coin + currency.size_flags_horizontal = Control.SIZE_EXPAND_FILL + currency.alignment = BoxContainer.ALIGNMENT_CENTER + currency.get_amount_label().add_theme_font_size_override("font_size", 14) + currency.get_amount_label().add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY + ) + reward.add_child(currency) + var experience_label := Label.new() + experience_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + experience_label.text = "%s xp" % _compact_integer(experience) + experience_label.tooltip_text = "%d xp" % experience + experience_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + experience_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + experience_label.add_theme_font_size_override("font_size", 14) + experience_label.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY + ) + reward.add_child(experience_label) + return reward + + +func _compact_integer(value: int) -> String: + var magnitude: int = absi(value) + var divisor: float = 1.0 + var suffix: String = "" + if magnitude >= 1_000_000_000_000: + divisor = 1_000_000_000_000.0 + suffix = "t" + elif magnitude >= 1_000_000_000: + divisor = 1_000_000_000.0 + suffix = "b" + elif magnitude >= 1_000_000: + divisor = 1_000_000.0 + suffix = "m" + elif magnitude >= 1_000: + divisor = 1_000.0 + suffix = "k" + else: + return str(value) + var scaled: float = float(value) / divisor + var number: String = ( + str(roundi(scaled)) + if absf(scaled) >= 100.0 + else ("%.1f" % scaled).trim_suffix(".0") + ) + return number + suffix + + func _add_empty(message: String) -> void: var empty := Label.new() empty.text = message