From c6b897718c307a9e38823343c62b293f198358f0 Mon Sep 17 00:00:00 2001 From: Voyager Date: Wed, 19 Aug 2026 06:53:55 -0400 Subject: [PATCH] Improve fullscreen menus on 4:3 displays --- tests/controller_ui_navigation_validation.gd | 18 ++++- tests/logbook_validation.gd | 35 ++++++++++ tests/ui_scaling_runtime_validation.gd | 67 +++++++++++++++++++ ui/fullscreen_menu_presentation.gd | 57 ++++++++++++++++ ui/fullscreen_menu_presentation.gd.uid | 1 + ui/logbook_page.gd | 70 +++++++++++++++----- ui/pause_menu.gd | 14 ++++ ui/player_menu.gd | 25 +++++++ ui/title_screen.gd | 13 +++- 9 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 ui/fullscreen_menu_presentation.gd create mode 100644 ui/fullscreen_menu_presentation.gd.uid diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd index 488c790..ec6c0d4 100644 --- a/tests/controller_ui_navigation_validation.gd +++ b/tests/controller_ui_navigation_validation.gd @@ -6,6 +6,9 @@ const ControllerFocusNavigationType = preload( const UIReferencePresentationType = preload( "res://ui/ui_reference_presentation.gd" ) +const FullscreenMenuPresentationType = preload( + "res://ui/fullscreen_menu_presentation.gd" +) func _initialize() -> void: @@ -234,10 +237,21 @@ func _validate_settings_navigation_contract() -> void: func _validate_four_by_three_centering() -> void: - var stage_position: Vector2 = ( - UIReferencePresentationType.get_stage_position(Vector2(640.0, 480.0)) + var display_size := Vector2(640.0, 480.0) + var stage_position: Vector2 = UIReferencePresentationType.get_stage_position( + display_size ) assert(stage_position.is_equal_approx(Vector2(0.0, 120.0))) + assert(FullscreenMenuPresentationType.uses_compact_profile(display_size)) + assert(is_equal_approx( + FullscreenMenuPresentationType.get_profile_scale(display_size), + 4.0 / 3.0, + )) + assert(FullscreenMenuPresentationType.get_profile_position( + display_size + ).is_equal_approx(Vector2(-640.0 / 3.0, -120.0))) + assert(FullscreenMenuPresentationType.get_source_rect(display_size) + == Rect2(160.0, 0.0, 960.0, 720.0)) func _validate_low_end_profile_contract() -> void: diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index ff5b771..1645096 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -81,6 +81,22 @@ func _validate_page() -> void: page.setup(collection, inventory, CatalogResource) page.activate() await process_frame + var outer_margin := page.get("_outer_margin") as MarginContainer + var book := page.get("_book") as Control + assert(outer_margin != null) + assert(book != null) + page.set_compact_presentation(true) + await process_frame + assert(outer_margin.get_theme_constant("margin_left") == 160) + assert(outer_margin.get_theme_constant("margin_right") == 160) + assert(outer_margin.get_theme_constant("margin_bottom") == 106) + assert(is_equal_approx(book.offset_top, 65.0)) + page.set_compact_presentation(false) + await process_frame + assert(outer_margin.get_theme_constant("margin_left") == 52) + assert(outer_margin.get_theme_constant("margin_right") == 52) + assert(outer_margin.get_theme_constant("margin_bottom") == 18) + assert(is_equal_approx(book.offset_top, 50.0)) assert( page.get("_category") == LogbookCatalog.Category.FRESH_WATER ) @@ -289,6 +305,25 @@ func _validate_page() -> void: var facts_detail := detail_buttons[1] as Button var quality_detail := detail_buttons[2] as Button var stats_detail := detail_buttons[3] as Button + assert( + quality_detail.custom_minimum_size + == LogbookPage.DETAIL_QUALITY_SECTION_SIZE + ) + assert( + stats_detail.custom_minimum_size + == LogbookPage.DETAIL_STATS_SECTION_SIZE + ) + assert( + quality_detail.size_flags_horizontal + == Control.SIZE_SHRINK_CENTER + ) + assert( + stats_detail.size_flags_horizontal + == Control.SIZE_SHRINK_CENTER + ) + assert( + stats_detail.size_flags_vertical & Control.SIZE_EXPAND == 0 + ) assert( portrait_detail.get_node(portrait_detail.focus_neighbor_right) == facts_detail diff --git a/tests/ui_scaling_runtime_validation.gd b/tests/ui_scaling_runtime_validation.gd index 399faee..b9e8bce 100644 --- a/tests/ui_scaling_runtime_validation.gd +++ b/tests/ui_scaling_runtime_validation.gd @@ -6,6 +6,9 @@ const UIPixelationPresenterType = preload( const UIReferencePresentationType = preload( "res://ui/ui_reference_presentation.gd" ) +const FullscreenMenuPresentationType = preload( + "res://ui/fullscreen_menu_presentation.gd" +) const GameUIScene := preload("res://ui/game_ui.tscn") const TARGET_SIZES: Array[Vector2i] = [ Vector2i(1280, 720), @@ -40,6 +43,9 @@ func _run() -> void: var player_menu := game_ui.get_node( "UIRoot/CanonicalStage/PlayerMenu" ) as PlayerMenu + var player_menu_profile_stage := player_menu.get_node( + "ResponsivePlayerMenuStage" + ) as Control var hotbar := game_ui.get_node( "UIRoot/CanonicalStage/Hotbar" ) as HotbarUI @@ -51,6 +57,15 @@ func _run() -> void: var title_content_stage := title_screen.get_node( "ResponsiveTitleStage" ) as Control + var title_profile_root := title_screen.get_node( + "ResponsiveTitleStage/TitlePresentationScaleRoot" + ) as Control + var pause_menu := game_ui.get_node( + "UIRoot/CanonicalStage/PauseMenu" + ) as PauseMenu + var pause_profile_stage := pause_menu.get_node( + "ResponsivePauseStage" + ) as Control var decorative_fish_layer := title_screen.get_node( "DecorativeFishLayer" ) as Control @@ -94,6 +109,12 @@ func _run() -> void: var expected_stage_offset: Vector2 = UIReferencePresentationType.get_offset( display_size ) + var expected_profile_scale: float = ( + FullscreenMenuPresentationType.get_profile_scale(display_size) + ) + var expected_profile_position: Vector2 = ( + FullscreenMenuPresentationType.get_profile_position(display_size) + ) assert(presenter.position.is_equal_approx(Vector2.ZERO)) assert(ui_root.position.is_equal_approx(Vector2.ZERO)) var expected_visible_size: Vector2 = ( @@ -112,6 +133,12 @@ func _run() -> void: assert(player_menu.size.is_equal_approx( UIReferencePresentationType.REFERENCE_SIZE )) + assert(player_menu_profile_stage.scale.is_equal_approx( + Vector2.ONE * expected_profile_scale + )) + assert(player_menu_profile_stage.position.is_equal_approx( + expected_profile_position + )) assert(hotbar.position.is_equal_approx(Vector2( 0.0, canonical_stage.position.y, @@ -126,6 +153,18 @@ func _run() -> void: assert(title_content_stage.position.is_equal_approx( canonical_stage.position )) + assert(title_profile_root.scale.is_equal_approx( + Vector2.ONE * expected_profile_scale + )) + assert(title_profile_root.position.is_equal_approx( + expected_profile_position + )) + assert(pause_profile_stage.scale.is_equal_approx( + Vector2.ONE * expected_profile_scale + )) + assert(pause_profile_stage.position.is_equal_approx( + expected_profile_position + )) assert(decorative_fish_layer.size.is_equal_approx( expected_visible_size )) @@ -153,6 +192,34 @@ func _run() -> void: assert(( canonical_stage.position * effective_scale ).distance_to(expected_stage_offset) <= 0.01) + var profile_source_rect: Rect2 = ( + FullscreenMenuPresentationType.get_source_rect(display_size) + ) + var profile_screen_start: Vector2 = expected_stage_offset + ( + expected_profile_position + + profile_source_rect.position * expected_profile_scale + ) * expected_scale + var profile_screen_size: Vector2 = ( + profile_source_rect.size + * expected_profile_scale + * expected_scale + ) + var compact_target_scale: float = minf( + display_size.x / profile_source_rect.size.x, + display_size.y / profile_source_rect.size.y, + ) + var expected_profile_screen_size: Vector2 = ( + profile_source_rect.size * compact_target_scale + ) + var expected_profile_screen_start: Vector2 = ( + display_size - expected_profile_screen_size + ) * 0.5 + assert(profile_screen_start.distance_to( + expected_profile_screen_start + ) <= 0.01) + assert(profile_screen_size.distance_to( + expected_profile_screen_size + ) <= 0.01) var expected_render_height: int = mini( roundi(UIReferencePresentationType.REFERENCE_SIZE.y * expected_scale), PlayerSettings.get_ui_render_height( diff --git a/ui/fullscreen_menu_presentation.gd b/ui/fullscreen_menu_presentation.gd new file mode 100644 index 0000000..75b54d8 --- /dev/null +++ b/ui/fullscreen_menu_presentation.gd @@ -0,0 +1,57 @@ +class_name FullscreenMenuPresentation +extends RefCounted + +const UIReferencePresentationType = preload( + "res://ui/ui_reference_presentation.gd" +) + +const WIDE_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) +const COMPACT_REFERENCE_SIZE: Vector2 = Vector2(960.0, 720.0) +const COMPACT_MAX_ASPECT: float = 1.5 +const COMPACT_SOURCE_RECT := Rect2(160.0, 0.0, 960.0, 720.0) + + +static func uses_compact_profile(display_size: Vector2) -> bool: + if display_size.x <= 0.0 or display_size.y <= 0.0: + return false + return display_size.aspect() <= COMPACT_MAX_ASPECT + + +static func get_profile_scale(display_size: Vector2) -> float: + if not uses_compact_profile(display_size): + return 1.0 + var wide_scale: float = UIReferencePresentationType.get_scale(display_size) + var compact_scale: float = minf( + display_size.x / COMPACT_REFERENCE_SIZE.x, + display_size.y / COMPACT_REFERENCE_SIZE.y, + ) + return compact_scale / wide_scale + + +static func get_profile_position(display_size: Vector2) -> Vector2: + if not uses_compact_profile(display_size): + return Vector2.ZERO + var wide_scale: float = UIReferencePresentationType.get_scale(display_size) + var compact_scale: float = minf( + display_size.x / COMPACT_REFERENCE_SIZE.x, + display_size.y / COMPACT_REFERENCE_SIZE.y, + ) + var profile_scale: float = compact_scale / wide_scale + var wide_offset: Vector2 = UIReferencePresentationType.get_offset( + display_size + ) + var compact_offset: Vector2 = ( + display_size - COMPACT_REFERENCE_SIZE * compact_scale + ) * 0.5 + return ( + (compact_offset - wide_offset) / wide_scale + - COMPACT_SOURCE_RECT.position * profile_scale + ) + + +static func get_source_rect(display_size: Vector2) -> Rect2: + return ( + COMPACT_SOURCE_RECT + if uses_compact_profile(display_size) + else Rect2(Vector2.ZERO, WIDE_REFERENCE_SIZE) + ) diff --git a/ui/fullscreen_menu_presentation.gd.uid b/ui/fullscreen_menu_presentation.gd.uid new file mode 100644 index 0000000..af716b7 --- /dev/null +++ b/ui/fullscreen_menu_presentation.gd.uid @@ -0,0 +1 @@ +uid://bx4pj52npcum3 diff --git a/ui/logbook_page.gd b/ui/logbook_page.gd index a703b95..5921b8f 100644 --- a/ui/logbook_page.gd +++ b/ui/logbook_page.gd @@ -49,13 +49,18 @@ const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0) const LOGBOOK_TAB_SEPARATION: float = 6.0 const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0 const PAGE_CONTENT_SCALE: float = 0.97 +const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0) +const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0) +const WIDE_BOOK_TOP: float = 50.0 +const COMPACT_BOOK_TOP: float = 65.0 const CATALOG_PORTRAIT_SIZE := Vector2(78.0, 36.0) const CATALOG_ENTRY_SIZE := Vector2(92.0, 92.0) const CATALOG_ROW_STEP: float = 98.0 const CATALOG_SNAP_DELAY: float = 0.12 const DETAIL_PORTRAIT_SIZE := Vector2(160.0, 88.0) const DETAIL_SECTION_SEPARATION: int = 10 -const DETAIL_QUALITY_SECTION_HEIGHT: float = 104.0 +const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0) +const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0) const DETAIL_STATS_ROW_SEPARATION: int = 8 enum ControllerZone { @@ -83,6 +88,9 @@ var _category_tween: Tween var _silhouette_material: ShaderMaterial var _snapping_catalog_scroll: bool = false var _catalog_scroll_snap_timer: Timer +var _outer_margin: MarginContainer +var _book: Control +var _compact_presentation: bool = false var _category_tabs: Array[Button] = [] var _category_tab_categories: Array[LogbookCatalog.Category] = [] @@ -136,6 +144,11 @@ func setup( _refresh_catalog() +func set_compact_presentation(compact: bool) -> void: + _compact_presentation = compact + _apply_presentation_margins() + + func activate() -> void: _active = true set_interactive(true) @@ -279,17 +292,14 @@ func _select_adjacent_controller_category(direction: int) -> void: func _build_interface() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - var outer := MarginContainer.new() - outer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - outer.add_theme_constant_override("margin_left", 52) - outer.add_theme_constant_override("margin_top", 85) - outer.add_theme_constant_override("margin_right", 52) - outer.add_theme_constant_override("margin_bottom", 18) - add_child(outer) + _outer_margin = MarginContainer.new() + _outer_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(_outer_margin) + _apply_presentation_margins() var stack := Control.new() stack.mouse_filter = Control.MOUSE_FILTER_IGNORE - outer.add_child(stack) + _outer_margin.add_child(stack) var tabs := HBoxContainer.new() # Align the first flange with the authored left paper edge. @@ -325,8 +335,11 @@ func _build_interface() -> void: _configure_category_focus() var book := Control.new() + _book = book book.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - book.offset_top = 50.0 + book.offset_top = ( + COMPACT_BOOK_TOP if _compact_presentation else WIDE_BOOK_TOP + ) book.z_index = 20 book.mouse_filter = Control.MOUSE_FILTER_IGNORE stack.add_child(book) @@ -411,6 +424,34 @@ func _build_interface() -> void: _build_portrait_overlay() +func _apply_presentation_margins() -> void: + if _outer_margin == null: + return + var margins: Rect2 = ( + COMPACT_OUTER_MARGINS + if _compact_presentation + else WIDE_OUTER_MARGINS + ) + _outer_margin.add_theme_constant_override( + "margin_left", roundi(margins.position.x) + ) + _outer_margin.add_theme_constant_override( + "margin_top", roundi(margins.position.y) + ) + _outer_margin.add_theme_constant_override( + "margin_right", roundi(margins.size.x) + ) + _outer_margin.add_theme_constant_override( + "margin_bottom", roundi(margins.size.y) + ) + if _book != null: + _book.offset_top = ( + COMPACT_BOOK_TOP + if _compact_presentation + else WIDE_BOOK_TOP + ) + + func _apply_artwork_rect(control: Control, source_rect: Rect2) -> void: control.anchor_left = source_rect.position.x / LOGBOOK_ARTWORK_SOURCE_SIZE.x control.anchor_top = source_rect.position.y / LOGBOOK_ARTWORK_SOURCE_SIZE.y @@ -869,8 +910,8 @@ func _build_known_details(fish: FishDataType) -> void: "quality collection", _quality_overlay_text(fish.id), ) - quality_button.custom_minimum_size.y = DETAIL_QUALITY_SECTION_HEIGHT - quality_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + quality_button.custom_minimum_size = DETAIL_QUALITY_SECTION_SIZE + quality_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _detail_body.add_child(quality_button) var quality_progress := _build_quality_progress(fish.id) quality_progress.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) @@ -881,9 +922,8 @@ func _build_known_details(fish: FishDataType) -> void: "fish stats", _stats_overlay_text(fish, catalog_number), ) - stats_button.custom_minimum_size.y = 190.0 - stats_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL - stats_button.size_flags_vertical = Control.SIZE_EXPAND_FILL + stats_button.custom_minimum_size = DETAIL_STATS_SECTION_SIZE + stats_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _detail_body.add_child(stats_button) var stats_anchor := VBoxContainer.new() stats_anchor.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) diff --git a/ui/pause_menu.gd b/ui/pause_menu.gd index edd3065..842382f 100644 --- a/ui/pause_menu.gd +++ b/ui/pause_menu.gd @@ -10,6 +10,9 @@ const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const SettingsPanelType = preload("res://ui/settings_panel.gd") +const FullscreenMenuPresentationType = preload( + "res://ui/fullscreen_menu_presentation.gd" +) const BubbleConfirmationPageType = preload( "res://ui/components/bubble_menu/bubble_confirmation_page.gd" ) @@ -43,6 +46,7 @@ enum CloseReason { } @onready var _presentation_scale_root: Control = %PausePresentationScaleRoot +@onready var _responsive_pause_stage: Control = %ResponsivePauseStage @onready var _root_page: SettingsBubblePage = %RootPage @onready var _settings_panel: SettingsPanelType = %SettingsPanel @onready var _transition_flurry: BubbleTransitionFlurry = ( @@ -94,6 +98,7 @@ func _ready() -> void: _join_game_page.back_requested.connect(_close_join_game) _confirmation_page.hide_page() resized.connect(_update_responsive_pause_stage) + get_window().size_changed.connect(_update_responsive_pause_stage) call_deferred("_update_responsive_pause_stage") @@ -549,6 +554,15 @@ func _emit_transition_flurry() -> void: func _update_responsive_pause_stage() -> void: if not is_node_ready(): return + var display_size := Vector2(get_window().size) + _responsive_pause_stage.set_anchors_preset(Control.PRESET_TOP_LEFT) + _responsive_pause_stage.size = PAUSE_DESKTOP_REFERENCE_SIZE + _responsive_pause_stage.scale = Vector2.ONE * ( + FullscreenMenuPresentationType.get_profile_scale(display_size) + ) + _responsive_pause_stage.position = ( + FullscreenMenuPresentationType.get_profile_position(display_size) + ) _presentation_scale_root.size = PAUSE_DESKTOP_REFERENCE_SIZE _presentation_scale_root.scale = Vector2.ONE _presentation_scale_root.position = Vector2.ZERO diff --git a/ui/player_menu.gd b/ui/player_menu.gd index cb69b6a..4fe7074 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -42,6 +42,9 @@ const ControllerMappingManagerType = preload( const ControllerFocusNavigationType = preload( "res://ui/controller_focus_navigation.gd" ) +const FullscreenMenuPresentationType = preload( + "res://ui/fullscreen_menu_presentation.gd" +) const FishBatchSelectionType = preload( "res://ui/fish_batch_selection.gd" ) @@ -170,6 +173,7 @@ const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42 const LIGHT_COOLER_WATER_COLOR := Color(0.037, 0.27, 0.375, 1.0) @onready var _navigation_cluster: BubbleClusterType = %NavigationCluster +@onready var _presentation_profile_stage: Control = $ResponsivePlayerMenuStage @onready var _presentation_scale_root: Control = %PlayerMenuPresentationScaleRoot @onready var _cooler_page: Control = %CoolerPage @onready var _cooler_outer_wall: PanelContainer = %CoolerOuterWall @@ -445,8 +449,10 @@ func _ready() -> void: _bag_item_field.gui_input.connect(_on_bag_field_gui_input) _apply_mail_notification_style() resized.connect(_update_shell_layout) + get_window().size_changed.connect(_update_presentation_profile) _show_section_immediate(_current_section) call_deferred("_update_shell_layout") + call_deferred("_update_presentation_profile") call_deferred("_update_cooler_water_mask") set_process(false) @@ -3152,6 +3158,25 @@ func _update_shell_layout() -> void: _profile_page.modulate.a = 1.0 _players_page.modulate.a = 1.0 _layout_cooler_fish(false) + _update_presentation_profile() + + +func _update_presentation_profile() -> void: + if not is_node_ready(): + return + var display_size := Vector2(get_window().size) + var compact: bool = ( + FullscreenMenuPresentationType.uses_compact_profile(display_size) + ) + _presentation_profile_stage.set_anchors_preset(Control.PRESET_TOP_LEFT) + _presentation_profile_stage.size = DESKTOP_REFERENCE_SIZE + _presentation_profile_stage.scale = Vector2.ONE * ( + FullscreenMenuPresentationType.get_profile_scale(display_size) + ) + _presentation_profile_stage.position = ( + FullscreenMenuPresentationType.get_profile_position(display_size) + ) + _catalog_logbook.set_compact_presentation(compact) func _layout_cooler_detail_text(compact: bool) -> void: diff --git a/ui/title_screen.gd b/ui/title_screen.gd index 0da2b19..049c90e 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -7,6 +7,9 @@ const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const SettingsPanelType = preload("res://ui/settings_panel.gd") +const FullscreenMenuPresentationType = preload( + "res://ui/fullscreen_menu_presentation.gd" +) const BubbleButtonType = preload( "res://ui/components/bubble_menu/bubble_button.gd" ) @@ -229,6 +232,7 @@ func _ready() -> void: ) visibility_changed.connect(_on_title_visibility_changed) resized.connect(_update_responsive_title_stage) + get_window().size_changed.connect(_update_responsive_title_stage) _start_prompt_label.resized.connect(_update_start_prompt_pivot) _decorative_rng.randomize() set_process(false) @@ -453,9 +457,14 @@ func _update_title_layout() -> void: func _update_responsive_title_stage() -> void: if not is_node_ready(): return + var display_size := Vector2(get_window().size) _title_presentation_scale_root.size = TITLE_DESKTOP_REFERENCE_SIZE - _title_presentation_scale_root.scale = Vector2.ONE - _title_presentation_scale_root.position = Vector2.ZERO + _title_presentation_scale_root.scale = Vector2.ONE * ( + FullscreenMenuPresentationType.get_profile_scale(display_size) + ) + _title_presentation_scale_root.position = ( + FullscreenMenuPresentationType.get_profile_position(display_size) + ) _update_title_layout() if ( _is_confirmation_active()