diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 1c6c7bc..6384d19 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -399,6 +399,35 @@ func _run() -> void: assert(hide_button.button_group.allow_unpress) assert(brush_option.item_count == 4) assert(grid_option.item_count == 4) + for pointer_only_control: Control in [ + brush_option, + grid_option, + mode_button, + eraser_button, + undo_button, + hide_button, + restore_button, + finalize_button, + ]: + assert(pointer_only_control.focus_mode == Control.FOCUS_NONE) + assert(brush_option.get_popup().unfocusable) + assert(grid_option.get_popup().unfocusable) + brush_option.get_popup().popup(Rect2i(100, 100, 220, 220)) + await process_frame + game_ui.call("_begin_virtual_mouse", 0) + game_ui.call("_update_virtual_cursor_position", Vector2(160.0, 160.0)) + var brush_popup_cursor := toolbar.get( + "_brush_popup_cursor" + ) as ControllerVirtualCursor + assert(brush_popup_cursor != null and brush_popup_cursor.visible) + assert(brush_popup_cursor.get_parent() == brush_option.get_popup()) + assert(brush_popup_cursor.z_index == RenderingServer.CANVAS_ITEM_Z_MAX) + assert(not virtual_cursor.visible) + brush_option.get_popup().hide() + game_ui.call("_update_virtual_cursor_position", Vector2(160.0, 160.0)) + assert(not brush_popup_cursor.visible) + assert(virtual_cursor.visible) + game_ui.call("_end_virtual_mouse") var expected_brush_icons: Array[String] = [ "art_kit_marker_tip_fine.png", "art_kit_marker_tip_thin.png", @@ -417,7 +446,9 @@ func _run() -> void: assert(brush_option.get_item_icon(index) != null) assert(grid_option.get_item_icon(index) != null) assert( - brush_option.get_item_icon(index).resource_path.ends_with( + str(brush_option.get_item_icon(index).get_meta( + &"channel_mask_source", "" + )).ends_with( expected_brush_icons[index] ) ) @@ -432,6 +463,8 @@ func _run() -> void: assert(grid_option.get_popup().is_item_disabled(1)) var color_buttons: Dictionary = toolbar.get("_color_buttons") assert(color_buttons.size() == SurfaceDrawingPalette.COLORS.size()) + for color_button: Button in color_buttons.values(): + assert(color_button.focus_mode == Control.FOCUS_NONE) assert(not (color_buttons[&"chalk_white"] as Button).disabled) assert((color_buttons[&"ocean_teal"] as Button).disabled) @@ -451,6 +484,8 @@ func _run() -> void: assert(service.get_grid_size() == 128) var marker_mode_material := mode_button.material as ShaderMaterial assert(marker_mode_material != null) + assert(brush_option.material == null) + assert(grid_option.material == null) assert( marker_mode_material.shader.resource_path.ends_with( "/ui/art_kit_marker_icon.gdshader" diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index f966370..8e09e6d 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -296,6 +296,9 @@ func _validate_page() -> void: (page.get("_portrait_overlay_backdrop") as Button).pressed.emit() await process_frame assert(not portrait_overlay.visible) + detail_buttons = page.get("_detail_buttons") as Array + assert(detail_buttons.size() == 4) + facts_detail = detail_buttons[1] as Button facts_detail.pressed.emit() await process_frame var overlay_text := page.get("_portrait_overlay_text") as Label diff --git a/ui/game_ui.gd b/ui/game_ui.gd index a7f0f76..c341d48 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -264,8 +264,11 @@ func set_controller_text_entry_request( func request_controller_text_entry_for(control: Control = null) -> bool: + var target: Control = control + if target == null: + target = get_viewport().gui_get_focus_owner() return ( - bool(_controller_text_entry_request.call(control)) + bool(_controller_text_entry_request.call(target)) if _controller_text_entry_request.is_valid() else false ) @@ -844,9 +847,8 @@ func _begin_virtual_mouse(device_id: int) -> void: _virtual_mouse_window_position = _clamp_virtual_mouse_window_position( Vector2(get_window().size) * 0.5 ) - _controller_virtual_cursor.visible = true - _update_virtual_cursor_position(_virtual_mouse_window_position) _emit_virtual_mouse_motion(Vector2.ZERO) + _update_virtual_cursor_position(_virtual_mouse_window_position) func _end_virtual_mouse() -> void: @@ -860,6 +862,7 @@ func _end_virtual_mouse() -> void: _virtual_mouse_trigger_strength = 0.0 _virtual_mouse_stick = Vector2.ZERO _controller_virtual_cursor.visible = false + _surface_drawing_toolbar.hide_virtual_pointer_overlay() if _fishing_spot != null: _fishing_spot.set_local_menu_input_suppressed( VIRTUAL_MOUSE_INPUT_OWNER, @@ -884,6 +887,7 @@ func _update_virtual_mouse(delta: float) -> void: var stick: Vector2 = _virtual_mouse_stick var stick_length: float = stick.length() if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE: + _update_virtual_cursor_position(_virtual_mouse_window_position) return var adjusted_strength: float = ( (stick_length - VIRTUAL_MOUSE_STICK_DEADZONE) @@ -902,8 +906,8 @@ func _update_virtual_mouse(delta: float) -> void: _virtual_mouse_window_position = _clamp_virtual_mouse_window_position( _virtual_mouse_window_position + relative_motion ) - _update_virtual_cursor_position(_virtual_mouse_window_position) _emit_virtual_mouse_motion(relative_motion) + _update_virtual_cursor_position(_virtual_mouse_window_position) func _poll_virtual_mouse_controller_state() -> void: @@ -1082,6 +1086,7 @@ func _set_virtual_mouse_button(button: MouseButton, pressed: bool) -> void: mouse_event.pressed = pressed mouse_event.factor = 1.0 _parse_virtual_mouse_event(mouse_event) + _update_virtual_cursor_position(_virtual_mouse_window_position) func _emit_virtual_mouse_motion(relative_motion: Vector2) -> void: @@ -1098,6 +1103,12 @@ func _parse_virtual_mouse_event(event: InputEventMouse) -> void: func _update_virtual_cursor_position(viewport_position: Vector2) -> void: + if _surface_drawing_toolbar.update_virtual_pointer_overlay( + _virtual_mouse_active + ): + _controller_virtual_cursor.visible = false + return + _controller_virtual_cursor.visible = _virtual_mouse_active var output_scale: float = UIReferencePresentationType.get_scale( Vector2(get_window().size) ) diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 2354311..53e4189 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -484,7 +484,7 @@ script = ExtResource("9_chat") [node name="ControllerVirtualCursor" type="Control" parent="UIRoot"] unique_name_in_owner = true -z_index = 500 +z_index = 4096 mouse_filter = 2 script = ExtResource("13_cursor") diff --git a/ui/network/join_game_page.gd b/ui/network/join_game_page.gd index 7cb9e4c..b05bdc8 100644 --- a/ui/network/join_game_page.gd +++ b/ui/network/join_game_page.gd @@ -100,8 +100,7 @@ func _ready() -> void: _back_button.pressed.connect(_on_back_pressed) _address.text_submitted.connect(func(_value: String) -> void: if _name_entry_active: - _name_edit.grab_focus() - _name_edit.select_all() + _focus_control(_name_edit, true) else: _request_join() ) @@ -190,10 +189,7 @@ func open_page(preserved_endpoint: String = "") -> void: UtilityPageStyle.animate_in(self) _set_mode(_mode, false) if _mode == Mode.DIRECT: - _address.grab_focus() _address.select_all() - else: - _server_list.grab_focus() func close_page() -> void: @@ -261,10 +257,7 @@ func _set_mode(mode: Mode, clear_connection_error: bool = true) -> void: _discovery_refresh_timer.stop() if not is_visible_in_tree(): return - if mode == Mode.DIRECT: - _address.grab_focus() - else: - _server_list.grab_focus() + _defer_focus_control(_address if mode == Mode.DIRECT else _server_list) func _request_join() -> void: @@ -365,9 +358,8 @@ func _on_save_pressed() -> void: _name_entry_active = true _name_edit.show() _save_button.text = "save" - _name_edit.grab_focus() - _name_edit.select_all() _refresh() + _defer_focus_control(_name_edit, true) func _commit_name_entry() -> void: @@ -408,9 +400,8 @@ func _on_edit_pressed() -> void: _name_entry_active = true _name_edit.show() _save_button.text = "save" - _name_edit.grab_focus() - _name_edit.select_all() _refresh() + _defer_focus_control(_name_edit, true) func _on_favorite_pressed() -> void: @@ -469,7 +460,7 @@ func _confirm_delete() -> void: func _cancel_delete() -> void: _delete_armed = false _delete_confirmation.hide_page() - _delete_button.grab_focus() + _defer_focus_control(_delete_button) func _on_list_item_selected(index: int) -> void: @@ -762,6 +753,30 @@ func _controller_focus_eligible(control: Control) -> bool: return line_edit == null or line_edit.editable +func _focus_control(control: Control, select_all: bool = false) -> void: + if ( + control == null + or not is_instance_valid(control) + or not control.is_inside_tree() + or not control.is_visible_in_tree() + or control.focus_mode not in [Control.FOCUS_CLICK, Control.FOCUS_ALL] + ): + return + var button := control as BaseButton + if button != null and button.disabled: + return + var line_edit := control as LineEdit + if line_edit != null and not line_edit.editable: + return + control.grab_focus() + if select_all and line_edit != null: + line_edit.select_all() + + +func _defer_focus_control(control: Control, select_all: bool = false) -> void: + _focus_control.call_deferred(control, select_all) + + func _set_controller_neighbors( control: Control, left: Control, @@ -787,7 +802,7 @@ func _recover_controller_focus( if owner != null and _delete_confirmation.is_ancestor_of(owner): return if fallback != null: - fallback.call_deferred("grab_focus") + _defer_focus_control(fallback) func _format_entry_details(entry: SavedServerEntry) -> String: @@ -978,7 +993,7 @@ func _on_back_pressed() -> void: _clear_edit_state() _refresh() var content: Control = _address if _mode == Mode.DIRECT else _server_list - content.call_deferred("grab_focus") + _defer_focus_control(content) return if ( _network_session != null diff --git a/ui/surface_drawing_toolbar.gd b/ui/surface_drawing_toolbar.gd index 5ede917..416274d 100644 --- a/ui/surface_drawing_toolbar.gd +++ b/ui/surface_drawing_toolbar.gd @@ -2,6 +2,9 @@ class_name SurfaceDrawingToolbar extends Control const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") +const ControllerVirtualCursorType = preload( + "res://ui/controller_virtual_cursor.gd" +) const MARKER_MODE_ICON: Texture2D = preload( "res://items/icons/art/art_kit_marker.png" ) @@ -44,6 +47,9 @@ var _unlocks: PlayerArtUnlocks var _color_buttons: Dictionary[StringName, Button] = {} var _dock_right: bool = true var _marker_mode_material: ShaderMaterial +var _brush_popup_cursor: ControllerVirtualCursorType +var _grid_popup_cursor: ControllerVirtualCursorType +var _applied_marker_icon_color: Color = Color(-1.0, -1.0, -1.0, -1.0) func _ready() -> void: @@ -89,7 +95,13 @@ func _ready() -> void: ) _brush_option.item_selected.connect(_select_brush) _grid_option.item_selected.connect(_select_grid) + _configure_pointer_only_controls() + _brush_popup_cursor = _add_popup_cursor(_brush_option.get_popup()) + _grid_popup_cursor = _add_popup_cursor(_grid_option.get_popup()) _build_options() + _apply_marker_color_to_brush_icons( + SurfaceDrawingPalette.get_color(SurfaceDrawingPalette.DEFAULT_COLOR_ID) + ) _apply_dock_side() hide() @@ -203,6 +215,64 @@ func owns_pointer_event(event: InputEvent) -> bool: return false +func update_virtual_pointer_overlay(is_pointer_visible: bool) -> bool: + var active_popup: PopupMenu + var active_cursor: ControllerVirtualCursorType + if _brush_option.get_popup().visible: + active_popup = _brush_option.get_popup() + active_cursor = _brush_popup_cursor + elif _grid_option.get_popup().visible: + active_popup = _grid_option.get_popup() + active_cursor = _grid_popup_cursor + for popup_cursor: ControllerVirtualCursorType in [ + _brush_popup_cursor, _grid_popup_cursor, + ]: + if popup_cursor != null: + popup_cursor.visible = ( + is_pointer_visible and popup_cursor == active_cursor + ) + if not is_pointer_visible or active_popup == null or active_cursor == null: + return false + active_cursor.set_pointer_position(active_popup.get_mouse_position()) + return true + + +func hide_virtual_pointer_overlay() -> void: + update_virtual_pointer_overlay(false) + + +func _configure_pointer_only_controls() -> void: + for control: Control in [ + _mode_button, + _brush_option, + _grid_option, + _eraser_button, + _undo_button, + _hide_guide_button, + _restore_guide_button, + _finalize_guide_button, + ]: + control.focus_mode = Control.FOCUS_NONE + control.focus_neighbor_left = NodePath() + control.focus_neighbor_top = NodePath() + control.focus_neighbor_right = NodePath() + control.focus_neighbor_bottom = NodePath() + control.focus_next = NodePath() + control.focus_previous = NodePath() + for popup: PopupMenu in [ + _brush_option.get_popup(), _grid_option.get_popup(), + ]: + popup.unfocusable = true + + +func _add_popup_cursor(popup: PopupMenu) -> ControllerVirtualCursorType: + var cursor := ControllerVirtualCursorType.new() + cursor.z_index = RenderingServer.CANVAS_ITEM_Z_MAX + cursor.z_as_relative = false + popup.add_child(cursor) + return cursor + + func _build_options() -> void: _brush_option.clear() for brush_size: int in PlayerArtUnlocks.BRUSH_SIZES: @@ -232,7 +302,7 @@ func _build_options() -> void: for color_id: StringName in SurfaceDrawingPalette.get_color_ids(): var button := Button.new() button.custom_minimum_size = Vector2(34, 34) - button.focus_mode = Control.FOCUS_ALL + button.focus_mode = Control.FOCUS_NONE button.tooltip_text = SurfaceDrawingPalette.get_display_name(color_id) button.pressed.connect(_select_color.bind(color_id)) _color_list.add_child(button) @@ -240,6 +310,61 @@ func _build_options() -> void: _apply_color_button_style(button, color_id, false) +func _apply_marker_color_to_brush_icons(marker_color: Color) -> void: + if _applied_marker_icon_color.is_equal_approx(marker_color): + return + _applied_marker_icon_color = marker_color + for index: int in range(_brush_option.item_count): + var brush_size: int = _brush_option.get_item_id(index) + _brush_option.set_item_icon( + index, + _channel_masked_icon(BRUSH_SIZE_ICONS[brush_size], marker_color), + ) + + +func _channel_masked_icon(source: Texture2D, marker_color: Color) -> Texture2D: + var source_image: Image = source.get_image() + if source_image == null or source_image.is_empty(): + return source + var image := source_image.duplicate() as Image + if image.is_compressed(): + if image.decompress() != OK: + return source + image.convert(Image.FORMAT_RGBA8) + for y: int in image.get_height(): + for x: int in image.get_width(): + var source_color: Color = image.get_pixel(x, y) + var red_dominance: float = ( + source_color.r - maxf(source_color.g, source_color.b) + ) + var marker_mask: float = smoothstep(0.04, 0.18, red_dominance) + image.set_pixel( + x, + y, + Color( + lerpf( + source_color.r, + marker_color.r * source_color.r, + marker_mask, + ), + lerpf( + source_color.g, + marker_color.g * source_color.r, + marker_mask, + ), + lerpf( + source_color.b, + marker_color.b * source_color.r, + marker_mask, + ), + source_color.a * marker_color.a, + ), + ) + var texture := ImageTexture.create_from_image(image) + texture.set_meta(&"channel_mask_source", source.resource_path) + return texture + + func _refresh_unlocks() -> void: if not is_node_ready() or _unlocks == null: return @@ -342,11 +467,13 @@ func _on_service_state_changed( ) -> void: visible = is_active if not is_active: + hide_virtual_pointer_overlay() return _mode_button.icon = ( GRID_MODE_ICON if mode_name == "place grid" else MARKER_MODE_ICON ) _marker_mode_material.set_shader_parameter("marker_color", color_value) + _apply_marker_color_to_brush_icons(color_value) _mode_button.accessibility_name = ( "switch to marker mode" if mode_name == "place grid"