Improve fullscreen menus on 4:3 displays

This commit is contained in:
Alexander Sellite 2026-08-19 06:53:55 -04:00
parent 805885a9f1
commit c6b897718c
9 changed files with 281 additions and 19 deletions

View file

@ -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:

View file

@ -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

View file

@ -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(

View file

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

View file

@ -0,0 +1 @@
uid://bx4pj52npcum3

View file

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

View file

@ -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

View file

@ -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:

View file

@ -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()