diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 953a84a4e..e0052c5d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,8 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.10 + # Ruff version. + rev: v0.11.4 hooks: # Run the linter. - id: ruff @@ -21,7 +22,9 @@ repos: hooks: - id: mypy args: [ --explicit-package-bases ] + language: system - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.396 hooks: - id: pyright + language: system diff --git a/arcade/examples/gui/exp_controller_support.py b/arcade/examples/gui/exp_controller_support.py new file mode 100644 index 000000000..f85b207fb --- /dev/null +++ b/arcade/examples/gui/exp_controller_support.py @@ -0,0 +1,198 @@ +""" +Example demonstrating controller support in an Arcade GUI. + +This example shows how to integrate controller input with the Arcade GUI framework. +It includes a controller indicator widget that displays the last controller input, +and a modal dialog that can be navigated using a controller. + +If Arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.exp_controller_support +""" + +from typing import Optional + +import arcade +from arcade import Texture +from arcade.experimental.controller_window import ControllerWindow, ControllerView +from arcade.gui import ( + UIAnchorLayout, + UIBoxLayout, + UIDropdown, + UIEvent, + UIFlatButton, + UIImage, + UIMouseFilterMixin, + UIOnChangeEvent, + UIOnClickEvent, + UISlider, + UIView, +) +from arcade.gui.events import ( + UIControllerButtonEvent, + UIControllerButtonPressEvent, + UIControllerDpadEvent, + UIControllerEvent, + UIControllerStickEvent, + UIControllerTriggerEvent, +) +from arcade.gui.experimental.focus import UIFocusGroup +from arcade.types import Color + + +class ControllerIndicator(UIAnchorLayout): + """ + A widget that displays the last controller input. + """ + + BLANK_TEX = Texture.create_empty("empty", (40, 40), arcade.color.TRANSPARENT_BLACK) + TEXTURE_CACHE: dict[str, Texture] = {} + + def __init__(self): + super().__init__() + + self._indicator = self.add(UIImage(texture=self.BLANK_TEX), anchor_y="bottom", align_y=10) + self._indicator.with_background(color=Color(0, 0, 0, 0)) + self._indicator._strong_background = True + + @classmethod + def get_texture(cls, path: str) -> Texture: + if path not in cls.TEXTURE_CACHE: + cls.TEXTURE_CACHE[path] = arcade.load_texture(path) + return cls.TEXTURE_CACHE[path] + + @classmethod + def input_prompts(cls, event: UIControllerEvent) -> Texture | None: + if isinstance(event, UIControllerButtonEvent): + match event.button: + case "a": + return cls.get_texture(":resources:input_prompt/xbox/button_a.png") + case "b": + return cls.get_texture(":resources:input_prompt/xbox/button_b.png") + case "x": + return cls.get_texture(":resources:input_prompt/xbox/button_x.png") + case "y": + return cls.get_texture(":resources:input_prompt/xbox/button_y.png") + case "rightshoulder": + return cls.get_texture(":resources:input_prompt/xbox/rb.png") + case "leftshoulder": + return cls.get_texture(":resources:input_prompt/xbox/lb.png") + case "start": + return cls.get_texture(":resources:input_prompt/xbox/button_start.png") + case "back": + return cls.get_texture(":resources:input_prompt/xbox/button_back.png") + + if isinstance(event, UIControllerTriggerEvent): + match event.name: + case "lefttrigger": + return cls.get_texture(":resources:input_prompt/xbox/lt.png") + case "righttrigger": + return cls.get_texture(":resources:input_prompt/xbox/rt.png") + + if isinstance(event, UIControllerDpadEvent): + match event.vector: + case (1, 0): + return cls.get_texture(":resources:input_prompt/xbox/dpad_right.png") + case (-1, 0): + return cls.get_texture(":resources:input_prompt/xbox/dpad_left.png") + case (0, 1): + return cls.get_texture(":resources:input_prompt/xbox/dpad_up.png") + case (0, -1): + return cls.get_texture(":resources:input_prompt/xbox/dpad_down.png") + + if isinstance(event, UIControllerStickEvent) and event.vector.length() > 0.2: + stick = "l" if event.name == "leftstick" else "r" + + # map atan2(y, x) to direction string (up, down, left, right) + heading = event.vector.heading() + if 0.785 > heading > -0.785: + return cls.get_texture(f":resources:input_prompt/xbox/stick_{stick}_right.png") + elif 0.785 < heading < 2.356: + return cls.get_texture(f":resources:input_prompt/xbox/stick_{stick}_up.png") + elif heading > 2.356 or heading < -2.356: + return cls.get_texture(f":resources:input_prompt/xbox/stick_{stick}_left.png") + elif -2.356 < heading < -0.785: + return cls.get_texture(f":resources:input_prompt/xbox/stick_{stick}_down.png") + + return None + + def on_event(self, event: UIEvent) -> Optional[bool]: + if isinstance(event, UIControllerEvent): + input_texture = self.input_prompts(event) + + if input_texture: + self._indicator.texture = input_texture + + arcade.unschedule(self.reset) + arcade.schedule_once(self.reset, 0.5) + + return super().on_event(event) + + def reset(self, *_): + self._indicator.texture = self.BLANK_TEX + + +class ControllerModal(UIMouseFilterMixin, UIFocusGroup): + def __init__(self): + super().__init__(size_hint=(0.8, 0.8)) + self.with_background(color=arcade.uicolor.DARK_BLUE_MIDNIGHT_BLUE) + + root = self.add(UIBoxLayout(space_between=10)) + + root.add(UIFlatButton(text="Modal Button 1", width=200)) + root.add(UIFlatButton(text="Modal Button 2", width=200)) + root.add(UIFlatButton(text="Modal Button 3", width=200)) + root.add(UIFlatButton(text="Close")).on_click = self.close + + self.detect_focusable_widgets() + + def on_event(self, event): + if super().on_event(event): + return True + + if isinstance(event, UIControllerButtonPressEvent): + if event.button == "b": + self.close(None) + return True + + return False + + def close(self, event): + print("Close") + # self.trigger_full_render() + self.trigger_full_render() + self.parent.remove(self) + + +class MyView(ControllerView, UIView): + def __init__(self): + super().__init__() + arcade.set_background_color(arcade.color.AMAZON) + + base = self.add_widget(ControllerIndicator()) + self.root = base.add(UIFocusGroup()) + self.root.with_padding(left=10) + box = self.root.add(UIBoxLayout(space_between=10), anchor_x="left") + + box.add(UIFlatButton(text="Button 1")).on_click = self.on_button_click + box.add(UIFlatButton(text="Button 2")).on_click = self.on_button_click + box.add(UIFlatButton(text="Button 3")).on_click = self.on_button_click + + box.add(UIDropdown(default="Option 1", options=["Option 1", "Option 2", "Option 3"])) + + slider = box.add(UISlider(value=0.5, min_value=0, max_value=1, width=200)) + + @slider.event + def on_change(event: UIOnChangeEvent): + print(f"Slider value changed: {event}") + + self.root.detect_focusable_widgets() + + def on_button_click(self, event: UIOnClickEvent): + print("Button clicked") + self.root.add(ControllerModal()) + + +if __name__ == "__main__": + window = ControllerWindow(title="Controller UI Example") + window.show_view(MyView()) + arcade.run() diff --git a/arcade/examples/gui/exp_controller_support_grid.py b/arcade/examples/gui/exp_controller_support_grid.py new file mode 100644 index 000000000..dfef1ca0d --- /dev/null +++ b/arcade/examples/gui/exp_controller_support_grid.py @@ -0,0 +1,96 @@ +""" +Example demonstrating a grid layout with focusable buttons in an Arcade GUI. + +This example shows how to create a grid layout with buttons +that can be navigated using a controller. +It includes a focus transition setup to allow smooth navigation between buttons in the grid. + +If Arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.exp_controller_support_grid +""" + +from typing import Dict, Tuple + +import arcade +from arcade.examples.gui.exp_controller_support import ControllerIndicator +from arcade.experimental.controller_window import ControllerView, ControllerWindow +from arcade.gui import ( + UIFlatButton, + UIGridLayout, + UIView, + UIWidget, +) +from arcade.gui.experimental.focus import Focusable, UIFocusGroup + + +class FocusableButton(Focusable, UIFlatButton): + pass + + +def setup_grid_focus_transition(grid: Dict[Tuple[int, int], UIWidget]): + """Setup focus transition in grid. + + Connect focus transition between `Focusable` in grid. + + Args: + grid: Dict[Tuple[int, int], Focusable]: grid of Focusable widgets. + key represents position in grid (x,y) + + """ + + cols = max(x for x, y in grid.keys()) + 1 + rows = max(y for x, y in grid.keys()) + 1 + for c in range(cols): + for r in range(rows): + btn = grid.get((c, r)) + if btn is None or not isinstance(btn, Focusable): + continue + + if c > 0: + btn.neighbor_left = grid.get((c - 1, r)) + else: + btn.neighbor_left = grid.get((cols - 1, r)) + + if c < cols - 1: + btn.neighbor_right = grid.get((c + 1, r)) + else: + btn.neighbor_right = grid.get((0, r)) + + if r > 0: + btn.neighbor_up = grid.get((c, r - 1)) + else: + btn.neighbor_up = grid.get((c, rows - 1)) + + if r < rows - 1: + btn.neighbor_down = grid.get((c, r + 1)) + else: + btn.neighbor_down = grid.get((c, 0)) + + +class MyView(ControllerView, UIView): + def __init__(self): + super().__init__() + arcade.set_background_color(arcade.color.AMAZON) + + self.root = self.add_widget(ControllerIndicator()) + self.root = self.root.add(UIFocusGroup()) + grid = self.root.add( + UIGridLayout(column_count=3, row_count=3, vertical_spacing=10, horizontal_spacing=10) + ) + + _grid = {} + for i in range(9): + btn = FocusableButton(text=f"Button {i}") + _grid[(i % 3, i // 3)] = btn + grid.add(btn, column=i % 3, row=i // 3) + + # connect focus transition in grid + setup_grid_focus_transition(_grid) + + self.root.detect_focusable_widgets() + + +if __name__ == "__main__": + window = ControllerWindow(title="Controller UI Example") + window.show_view(MyView()) + arcade.run() diff --git a/arcade/examples/gui/exp_inventory_demo.py b/arcade/examples/gui/exp_inventory_demo.py new file mode 100644 index 000000000..86faf0ff9 --- /dev/null +++ b/arcade/examples/gui/exp_inventory_demo.py @@ -0,0 +1,403 @@ +""" + +Example of a full functional inventory system. + +This example demonstrates how to create a simple inventory system. + +Main features are: +- Inventory slots +- Equipment slots +- Move items between slots +- Controller support + +If Arcade and Python are properly installed, you can run this example with: +python -m arcade.examples.gui.exp_inventory_demo +""" + +from functools import partial +# TODO: Drag and Drop + +from typing import List + +import pyglet.font +from pyglet.gl import GL_NEAREST + +import arcade +from arcade import Rect, open_window +from arcade.examples.gui.exp_controller_support_grid import ( + ControllerIndicator, + setup_grid_focus_transition, +) +from arcade.gui import ( + Property, + Surface, + UIAnchorLayout, + UIBoxLayout, + UIFlatButton, + UIGridLayout, + UILabel, + UIOnClickEvent, + UIView, + UIWidget, + bind, +) +from arcade.gui.experimental.controller import UIControllerBridge +from arcade.gui.experimental.focus import Focusable, UIFocusGroup +from arcade.resources import load_kenney_fonts + + +class Item: + """Base class for all items.""" + + def __init__(self, symbol: str): + self.symbol = symbol + + +class Inventory: + """ + Basic inventory class. + + Contains items and manages items. + + + inventory = Inventory(10) + inventory.add(Item("🍎")) + inventory.add(Item("🍌")) + inventory.add(Item("🍇")) + + + for item in inventory: + print(item.symbol) + + inventory.remove(inventory[0]) + """ + + def __init__(self, capacity: int): + self._items: List[Item | None] = [None for _ in range(capacity)] + self.capacity = capacity + + def add(self, item: Item): + empty_slot = None + for i, slot in enumerate(self._items): + if slot is None: + empty_slot = i + break + + if empty_slot is not None: + self._items[empty_slot] = item + else: + raise ValueError("Inventory is full.") + + def is_full(self): + return len(self._items) == self.capacity + + def remove(self, item: Item): + for i, slot in enumerate(self._items): + if slot == item: + self._items[i] = None + return + + def __getitem__(self, index: int): + return self._items[index] + + def __setitem__(self, index: int, value: Item): + self._items[index] = value + + def __iter__(self): + yield from self._items + + +class Equipment(Inventory): + """Equipment inventory. + + Contains three slots for head, chest and legs. + """ + + def __init__(self): + super().__init__(3) + + @property + def head(self) -> Item: + return self[0] + + @head.setter + def head(self, value): + self[0] = value + + @property + def chest(self) -> Item: + return self[1] + + @chest.setter + def chest(self, value): + self[1] = value + + @property + def legs(self) -> Item: + return self[2] + + @legs.setter + def legs(self, value): + self[2] = value + + +class InventorySlotUI(Focusable, UIFlatButton): + """Represents a single inventory slot. + The slot accesses a specific index in the inventory. + + Emits an on_click event. + """ + + def __init__(self, inventory: Inventory, index: int, **kwargs): + super().__init__(size_hint=(1, 1), **kwargs) + self.ui_label.update_font(font_size=24) + self._inventory = inventory + self._index = index + + item = inventory[index] + if item: + self.text = item.symbol + + @property + def item(self) -> Item | None: + return self._inventory[self._index] + + @item.setter + def item(self, value): + self._inventory[self._index] = value + self._on_item_change() + + def _on_item_change(self, *args): + if self.item: + self.text = self.item.symbol + else: + self.text = "" + + +class EquipmentSlotUI(InventorySlotUI): + pass + + +class InventoryUI(UIGridLayout): + """Manages inventory slots. + + Emits an `on_slot_clicked(slot)` event when a slot is clicked. + + """ + + def __init__(self, inventory: Inventory, **kwargs): + super().__init__( + size_hint=(0.7, 1), + column_count=6, + row_count=5, + align_vertical="center", + align_horizontal="center", + vertical_spacing=10, + horizontal_spacing=10, + **kwargs, + ) + self.with_padding(all=10) + self.with_border(color=arcade.color.WHITE, width=2) + + self.inventory = inventory + self.grid = {} + + for i, item in enumerate(inventory): + slot = InventorySlotUI(inventory, i) + # fill left to right, bottom to top (6x5 grid) + self.add(slot, column=i % 6, row=i // 6) + self.grid[(i % 6, i // 6)] = slot + slot.on_click = self._on_slot_click # type: ignore + + InventoryUI.register_event_type("on_slot_clicked") + + def _on_slot_click(self, event: UIOnClickEvent): + # propagate slot click event to parent + self.dispatch_event("on_slot_clicked", event.source) + + def on_slot_clicked(self, event: UIOnClickEvent): + pass + + +class EquipmentUI(UIBoxLayout): + """Contains three slots for equipment items. + + - Head + - Chest + - Legs + + Emits an `on_slot_clicked(slot)` event when a slot is clicked. + + """ + + def __init__(self, **kwargs): + super().__init__(size_hint=(0.3, 1), space_between=10, **kwargs) + self.with_padding(all=20) + self.with_border(color=arcade.color.WHITE, width=2) + + equipment = Equipment() + + self.head_slot = self.add(EquipmentSlotUI(equipment, 0)) + self.head_slot.on_click = partial(self.dispatch_event, "on_slot_clicked", self.head_slot) + + self.chest_slot = self.add(EquipmentSlotUI(equipment, 1)) + self.chest_slot.on_click = partial(self.dispatch_event, "on_slot_clicked", self.chest_slot) + + self.legs_slot = self.add(EquipmentSlotUI(equipment, 2)) + self.legs_slot.on_click = partial(self.dispatch_event, "on_slot_clicked", self.legs_slot) + + EquipmentUI.register_event_type("on_slot_clicked") + + +class ActiveSlotTrackerMixin(UIWidget): + """ + Mixin class to track the active slot. + """ + + active_slot = Property[InventorySlotUI | None](None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + bind(self, "active_slot", self.trigger_render) + + def do_render(self, surface: Surface): + surface.limit(None) + if self.active_slot: + rect: Rect = self.active_slot.rect + + rect = rect.resize(*(rect.size + (2, 2))) + arcade.draw_rect_outline(rect, arcade.uicolor.RED_ALIZARIN, 2) + + return super().do_render(surface) + + def on_slot_clicked(self, clicked_slot: InventorySlotUI): + if self.active_slot: + # disable active slot + if clicked_slot == self.active_slot: + self.active_slot = None + return + + else: + # swap items + src_item = self.active_slot.item + dst_item = clicked_slot.item + + self.active_slot.item = dst_item + clicked_slot.item = src_item + + self.active_slot = None + return + + else: + # activate slot if contains item + if clicked_slot.item: + self.active_slot = clicked_slot + + +class InventoryModal(ActiveSlotTrackerMixin, UIFocusGroup, UIAnchorLayout): + def __init__(self, inventory: Inventory, **kwargs): + super().__init__(size_hint=(0.8, 0.8), **kwargs) + self.with_padding(all=10) + self.with_background(color=arcade.uicolor.GREEN_GREEN_SEA) + self._debug = True + + self.add( + UILabel(text="Inventory", font_size=20, font_name="Kenney Blocks", bold=True), + anchor_y="top", + ) + + content = UIBoxLayout(size_hint=(1, 0.9), vertical=False, space_between=10) + self.add(content, anchor_y="bottom") + + inv_ui = content.add(InventoryUI(inventory)) + inv_ui.on_slot_clicked = self.on_slot_clicked # type: ignore + + eq_ui = content.add(EquipmentUI()) + eq_ui.on_slot_clicked = self.on_slot_clicked # type: ignore + + # prepare focusable widgets + widget_grid = inv_ui.grid + setup_grid_focus_transition( + widget_grid # type: ignore + ) # setup default transitions in a grid + + # add transitions to equipment slots + cols = max(x for x, y in widget_grid.keys()) + rows = max(y for x, y in widget_grid.keys()) + + equipment_slots = [eq_ui.head_slot, eq_ui.chest_slot, eq_ui.legs_slot] + + # connect inventory slots with equipment slots + slots_to_eq_ratio = (rows + 1) / len(equipment_slots) + for i in range(rows + 1): + eq_index = int(i // slots_to_eq_ratio) + eq_slot = equipment_slots[eq_index] + + inv_slot = widget_grid[(cols, i)] + + inv_slot.neighbor_right = eq_slot + eq_slot.neighbor_left = inv_slot + + # focusable widgets + self.detect_focusable_widgets() + + # close button, not focusable (controller use B to close) + close_button = self.add( + # todo: find out why X is not in center + UIFlatButton(text="X", width=40, height=40), + anchor_x="right", + anchor_y="top", + ) + close_button.on_click = lambda _: self.close() # type: ignore + + def close(self): + self.trigger_full_render() + self.parent.remove(self) + + +class MyView(UIView): + def __init__(self): + super().__init__() + + self.cb = UIControllerBridge(self.ui) + + self.background_color = arcade.color.BLACK + + self.inventory = Inventory(30) + + self.inventory.add(Item("🍎")) + self.inventory.add(Item("🍌")) + self.inventory.add(Item("🍇")) + + self.root = self.add_widget(UIAnchorLayout()) + self.add_widget(ControllerIndicator()) + + self.show_inventory() + + def show_inventory(self): + self.root.add(InventoryModal(self.inventory)) + + def on_key_press(self, symbol: int, modifiers: int) -> bool | None: + if symbol == arcade.key.I: + print("Show inventory") + for i, item in enumerate(self.inventory): + print(i, item.symbol if item else "-") + return True + + return super().on_key_press(symbol, modifiers) + + def on_draw_before_ui(self): + pass + + +if __name__ == "__main__": + # pixelate the font + pyglet.font.base.Font.texture_min_filter = GL_NEAREST + pyglet.font.base.Font.texture_mag_filter = GL_NEAREST + + load_kenney_fonts() + + open_window(window_title="Minimal example", width=1280, height=720, resizable=True).show_view( + MyView() + ) + arcade.run() diff --git a/arcade/experimental/controller_window.py b/arcade/experimental/controller_window.py new file mode 100644 index 000000000..2e6d9e2cb --- /dev/null +++ b/arcade/experimental/controller_window.py @@ -0,0 +1,154 @@ +import warnings + +from pyglet.input import Controller + +import arcade +from arcade import ControllerManager + + +class _WindowControllerBridge: + """Translates controller events to UIEvents and passes them to the UIManager. + + Controller are automatically connected and disconnected. + + Controller events are consumed by the UIControllerBridge, + if the UIEvent is consumed by the UIManager. + + This implicates, that the UIControllerBridge should be the first listener in the chain and + that other systems should be aware, when not to act on events (like when the UI is active). + """ + + def __init__(self, window: arcade.Window): + self.window = window + + self.cm = ControllerManager() + self.cm.push_handlers(self) + + # bind to existing controllers + for controller in self.cm.get_controllers(): + self.on_connect(controller) + + def on_connect(self, controller: Controller): + controller.push_handlers(self) + + try: + controller.open() + except Exception as e: + warnings.warn(f"Failed to open controller {controller}: {e}") + + self.window.dispatch_event("on_connect", controller) + + def on_disconnect(self, controller: Controller): + controller.remove_handlers(self) + + try: + controller.close() + except Exception as e: + warnings.warn(f"Failed to close controller {controller}: {e}") + + self.window.dispatch_event("on_disconnect", controller) + + # Controller input event mapping + def on_stick_motion(self, controller: Controller, name, value): + return self.window.dispatch_event("on_stick_motion", controller, name, value) + + def on_trigger_motion(self, controller: Controller, name, value): + return self.window.dispatch_event("on_trigger_motion", controller, name, value) + + def on_button_press(self, controller: Controller, button): + return self.window.dispatch_event("on_button_press", controller, button) + + def on_button_release(self, controller: Controller, button): + return self.window.dispatch_event("on_button_release", controller, button) + + def on_dpad_motion(self, controller: Controller, value): + return self.window.dispatch_event("on_dpad_motion", controller, value) + + +class ControllerWindow(arcade.Window): + """A window that automatically opens and listens to controller events + and dispatches them via on_... hooks.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cb = _WindowControllerBridge(self) + + def get_controllers(self) -> list[Controller]: + """Return a list of connected controllers.""" + return self.cb.cm.get_controllers() + + # Controller event mapping + def on_connect(self, controller: Controller): + """Called when a controller is connected. + The controller is already opened and ready to be used. + """ + pass + + def on_disconnect(self, controller: Controller): + """Called when a controller is disconnected.""" + pass + + def on_stick_motion(self, controller: Controller, name, value): + """Called when a stick is moved.""" + pass + + def on_trigger_motion(self, controller: Controller, name, value): + """Called when a trigger is moved.""" + pass + + def on_button_press(self, controller: Controller, button): + """Called when a button is pressed.""" + pass + + def on_button_release(self, controller: Controller, button): + """Called when a button is released.""" + pass + + def on_dpad_motion(self, controller: Controller, value): + """Called when the dpad is moved.""" + pass + + +ControllerWindow.register_event_type("on_connect") +ControllerWindow.register_event_type("on_disconnect") +ControllerWindow.register_event_type("on_stick_motion") +ControllerWindow.register_event_type("on_trigger_motion") +ControllerWindow.register_event_type("on_button_press") +ControllerWindow.register_event_type("on_button_release") +ControllerWindow.register_event_type("on_dpad_motion") + + +class ControllerView(arcade.View): + """A view which predefines the controller event mapping methods. + + Can be used with a ControllerWindow to handle controller events.""" + + def on_connect(self, controller: Controller): + """Called when a controller is connected. + The controller is already opened and ready to be used. + """ + pass + + def on_disconnect(self, controller: Controller): + """Called when a controller is disconnected.""" + pass + + def on_stick_motion(self, controller: Controller, name, value): + """Called when a stick is moved.""" + pass + + def on_trigger_motion(self, controller: Controller, name, value): + """Called when a trigger is moved.""" + pass + + def on_button_press(self, controller: Controller, button): + """Called when a button is pressed.""" + pass + + def on_button_release(self, controller: Controller, button): + """Called when a button is released.""" + pass + + def on_dpad_motion(self, controller: Controller, value): + """Called when the dpad is moved.""" + pass diff --git a/arcade/gl/texture.py b/arcade/gl/texture.py index 479fa23ce..fd88e6217 100644 --- a/arcade/gl/texture.py +++ b/arcade/gl/texture.py @@ -136,8 +136,8 @@ def __init__( self._component_size = 0 self._alignment = 1 self._target = target - self._samples = min(max(0, samples), self._ctx.info.MAX_SAMPLES) - self._depth = depth + self._samples: int = min(max(0, samples), self._ctx.info.MAX_SAMPLES) + self._depth: bool = depth self._immutable = immutable self._compare_func: str | None = None self._anisotropy = 1.0 diff --git a/arcade/gui/events.py b/arcade/gui/events.py index a150d1ce5..d5ca7df20 100644 --- a/arcade/gui/events.py +++ b/arcade/gui/events.py @@ -236,3 +236,78 @@ class UIOnActionEvent(UIEvent): """ action: Any + + +@dataclass +class UIControllerEvent(UIEvent): + """Base class for all UI controller events. + + Args: + source: The controller that triggered the event. + """ + + +@dataclass +class UIControllerStickEvent(UIControllerEvent): + """Triggered when a controller stick is moved. + + Args: + name: The name of the stick. + vector: The value of the stick. + """ + + name: str + vector: Vec2 + + +@dataclass +class UIControllerTriggerEvent(UIControllerEvent): + """Triggered when a controller trigger is moved. + + Args: + name: The name of the trigger. + value: The value of the trigger. + """ + + name: str + value: float + + +@dataclass +class UIControllerButtonEvent(UIControllerEvent): + """Triggered when a controller button used. + + Args: + button: The name of the button. + """ + + button: str + + +@dataclass +class UIControllerButtonPressEvent(UIControllerButtonEvent): + """Triggered when a controller button is pressed. + + Args: + button: The name of the button. + """ + + +@dataclass +class UIControllerButtonReleaseEvent(UIControllerButtonEvent): + """Triggered when a controller button is released. + + Args: + button: The name of the button. + """ + + +@dataclass +class UIControllerDpadEvent(UIControllerEvent): + """Triggered when a controller dpad is moved. + + Args: + vector: The value of the dpad. + """ + + vector: Vec2 diff --git a/arcade/gui/experimental/focus.py b/arcade/gui/experimental/focus.py new file mode 100644 index 000000000..6da1a309c --- /dev/null +++ b/arcade/gui/experimental/focus.py @@ -0,0 +1,392 @@ +import warnings +from typing import Optional + +from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED +from pyglet.math import Vec2 + +import arcade +from arcade import LBWH, MOUSE_BUTTON_LEFT +from arcade.gui.events import ( + UIEvent, + UIKeyPressEvent, + UIKeyReleaseEvent, + UIMousePressEvent, + UIMouseReleaseEvent, +) +from arcade.gui.events import ( + UIControllerButtonPressEvent, + UIControllerButtonReleaseEvent, + UIControllerDpadEvent, +) +from arcade.gui.property import ListProperty, Property, bind +from arcade.gui.surface import Surface +from arcade.gui.ui_manager import UIManager +from arcade.gui.widgets import UIInteractiveWidget, UIWidget +from arcade.gui.widgets.layout import UIAnchorLayout +from arcade.gui.widgets.slider import UIBaseSlider + + +class Focusable(UIWidget): + """ + A widget that can be focused and provides additional information about focus behavior. + + Attributes: + + neighbor_up: The widget above this widget. + neighbor_right: The widget right of this widget. + neighbor_down: The widget below this widget. + neighbor_left: The widget left of this widget. + + """ + + # todo set focused when focused + focused = Property(False) + + neighbor_up: UIWidget | None = None + neighbor_right: UIWidget | None = None + neighbor_down: UIWidget | None = None + neighbor_left: UIWidget | None = None + + @property + def ui(self) -> UIManager | None: + """The UIManager this widget is attached to.""" + w: UIWidget | None = self + while w and w.parent: + parent = w.parent + if isinstance(parent, UIManager): + return parent + + w = parent + return None + + def _render_focus(self, surface: Surface): + # this will be properly integrated into widget + self.prepare_render(surface) + arcade.draw_rect_outline( + rect=LBWH(0, 0, self.content_width, self.content_height), + color=arcade.color.WHITE, + border_width=4, + ) + + def _do_render(self, surface: Surface, force=False) -> bool: + rendered = False + + should_render = force or self._requires_render + if should_render and self.visible: + rendered = True + self.do_render_base(surface) + self.do_render(surface) + + if self.focused: + self._render_focus(surface) + + self._requires_render = False + + # only render children if self is visible + if self.visible: + for child in self.children: + rendered |= child._do_render(surface, should_render) + + return rendered + + +class UIFocusMixin(UIWidget): + """A group of widgets that can be focused. + + UIFocusGroup maintains two lists of widgets: + - The list of focusable widgets. + - The list of widgets in. + + Use `detect_focusable_widgets()` to automatically detect focusable widgets + or add_widget to add them manually. + + The Group can be navigated with the keyboard or controller. + + - DPAD: Navigate between focusable widgets. (up, down, left, right) + - TAB: Navigate between focusable widgets. + - A Button or SPACE: Interact with the focused widget. + + """ + + _focusable_widgets = ListProperty[UIWidget]() + _focused = Property(0) + _interacting: UIWidget | None = None + + _debug = Property(False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + bind(self, "_debug", self.trigger_full_render) + bind(self, "_focused", self.trigger_full_render) + bind(self, "_focusable_widgets", self.trigger_full_render) + + def on_event(self, event: UIEvent) -> Optional[bool]: + if super().on_event(event): + return EVENT_HANDLED + + if isinstance(event, UIKeyPressEvent): + if event.symbol == arcade.key.TAB: + if event.modifiers & arcade.key.MOD_SHIFT: + self.focus_previous() + else: + self.focus_next() + + return EVENT_HANDLED + + elif event.symbol == arcade.key.SPACE: + self.start_interaction() + return EVENT_HANDLED + + elif isinstance(event, UIKeyReleaseEvent): + if event.symbol == arcade.key.SPACE: + self.end_interaction() + return EVENT_HANDLED + + if isinstance(event, UIControllerDpadEvent): + if self._interacting: + # TODO this should be handled in the slider! + # pass dpad events to the interacting widget + if event.vector.x == 1 and isinstance(self._interacting, UIBaseSlider): + self._interacting.norm_value += 0.1 + return EVENT_HANDLED + + elif event.vector.x == -1 and isinstance(self._interacting, UIBaseSlider): + self._interacting.norm_value -= 0.1 + return EVENT_HANDLED + + return EVENT_HANDLED + + else: + # switch focus + if event.vector.x == 1: + self.focus_right() + return EVENT_HANDLED + + elif event.vector.y == 1: + self.focus_up() + return EVENT_HANDLED + + elif event.vector.x == -1: + self.focus_left() + return EVENT_HANDLED + + elif event.vector.y == -1: + self.focus_down() + return EVENT_HANDLED + + elif isinstance(event, UIControllerButtonPressEvent): + if event.button == "a": + self.start_interaction() + return EVENT_HANDLED + elif isinstance(event, UIControllerButtonReleaseEvent): + if event.button == "a": + self.end_interaction() + return EVENT_HANDLED + + return EVENT_UNHANDLED + + def _ensure_focused_property(self): + # TODO this is a hack, to set the focused property on the focused widget + # this should be properly handled in a property or so + + focused = self._get_focused_widget() + + for widget in self._focusable_widgets: + if isinstance(widget, Focusable): + if widget == focused: + widget.focused = True + else: + widget.focused = False + + def _get_focused_widget(self) -> UIWidget | None: + if len(self._focusable_widgets) == 0: + return None + + if len(self._focusable_widgets) <= self._focused < 0: + warnings.warn("Focused widget is out of range") + self._focused = 0 + + return self._focusable_widgets[self._focused] + + def add_widget(self, widget): + self._focusable_widgets.append(widget) + + @classmethod + def _walk_widgets(cls, root: UIWidget): + for child in reversed(root.children): + yield child + yield from cls._walk_widgets(child) + + def detect_focusable_widgets(self, root: UIWidget | None = None): + """Automatically detect focusable widgets.""" + if root is None: + root = self + + widgets = self._walk_widgets(root) + + focusable_widgets = [] + for widget in reversed(list(widgets)): + if self.is_focusable(widget): + focusable_widgets.append(widget) + + self._focusable_widgets = focusable_widgets + + def focus_up(self): + widget = self._get_focused_widget() + if isinstance(widget, Focusable): + if widget.neighbor_up: + _index = self._focusable_widgets.index(widget.neighbor_up) + self._focused = _index + return + + self.focus_previous() + + def focus_down(self): + widget = self._get_focused_widget() + if isinstance(widget, Focusable): + if widget.neighbor_down: + _index = self._focusable_widgets.index(widget.neighbor_down) + self._focused = _index + return + + self.focus_next() + + def focus_left(self): + widget = self._get_focused_widget() + if isinstance(widget, Focusable): + if widget.neighbor_left: + _index = self._focusable_widgets.index(widget.neighbor_left) + self._focused = _index + return + + self.focus_previous() + + def focus_right(self): + widget = self._get_focused_widget() + if isinstance(widget, Focusable): + if widget.neighbor_right: + _index = self._focusable_widgets.index(widget.neighbor_right) + self._focused = _index + return + + self.focus_next() + + def focus_next(self): + self._focused += 1 + if self._focused >= len(self._focusable_widgets): + self._focused = 0 + + def focus_previous(self): + self._focused -= 1 + if self._focused < 0: + self._focused = len(self._focusable_widgets) - 1 + + def start_interaction(self): + widget = self._get_focused_widget() + + if isinstance(widget, UIInteractiveWidget): + widget.dispatch_ui_event( + UIMousePressEvent( + source=self, + x=int(widget.rect.center_x), + y=int(widget.rect.center_y), + button=MOUSE_BUTTON_LEFT, + modifiers=0, + ) + ) + self._interacting = widget + else: + print("Cannot interact widget") + + def end_interaction(self): + widget = self._get_focused_widget() + + if isinstance(widget, UIInteractiveWidget): + if isinstance(self._interacting, UIBaseSlider): + # if slider, release outside the slider + x = self._interacting.rect.left - 1 + y = self._interacting.rect.bottom - 1 + else: + x = widget.rect.center_x + y = widget.rect.center_y + + self._interacting = None + widget.dispatch_ui_event( + UIMouseReleaseEvent( + source=self, + x=int(x), + y=int(y), + button=MOUSE_BUTTON_LEFT, + modifiers=0, + ) + ) + + def _do_render(self, surface: Surface, force=False) -> bool: + # TODO this is a hack, to set the focused property on the focused widget + self._ensure_focused_property() + + # TODO: add a post child render hook to UIWidget + rendered = super()._do_render(surface, force) + + if rendered: + self.do_post_render(surface) + + return rendered + + def do_post_render(self, surface: Surface): + surface.limit(None) + + widget = self._get_focused_widget() + if not widget: + return + + if isinstance(widget, Focusable): + # Focusable widgets care about focus themselves + pass + else: + arcade.draw_rect_outline( + rect=widget.rect, + color=arcade.color.WHITE, + border_width=2, + ) + + if self._debug: + # debugging + if isinstance(widget, Focusable): + if widget.neighbor_up: + self._draw_indicator( + widget.rect.top_center, + widget.neighbor_up.rect.bottom_center, + color=arcade.color.RED, + ) + if widget.neighbor_down: + self._draw_indicator( + widget.rect.bottom_center, + widget.neighbor_down.rect.top_center, + color=arcade.color.GREEN, + ) + if widget.neighbor_left: + self._draw_indicator( + widget.rect.center_left, + widget.neighbor_left.rect.center_right, + color=arcade.color.BLUE, + ) + if widget.neighbor_right: + self._draw_indicator( + widget.rect.center_right, + widget.neighbor_right.rect.center_left, + color=arcade.color.ORANGE, + ) + + def _draw_indicator(self, start: Vec2, end: Vec2, color=arcade.color.WHITE): + arcade.draw_line(start.x, start.y, end.x, end.y, color, 2) + arcade.draw_circle_filled(end.x, end.y, 5, color, num_segments=4) + + @staticmethod + def is_focusable(widget): + return isinstance(widget, (Focusable, UIInteractiveWidget)) + + +class UIFocusGroup(UIFocusMixin, UIAnchorLayout): + pass diff --git a/arcade/gui/experimental/password_input.py b/arcade/gui/experimental/password_input.py index a9b9ecc01..63a36e444 100644 --- a/arcade/gui/experimental/password_input.py +++ b/arcade/gui/experimental/password_input.py @@ -1,4 +1,8 @@ -from arcade.gui import Surface, UIEvent, UIInputText, UITextInputEvent +from __future__ import annotations + +from arcade.gui.events import UIEvent, UITextInputEvent +from arcade.gui.surface import Surface +from arcade.gui.widgets.text import UIInputText class UIPasswordInput(UIInputText): diff --git a/arcade/gui/experimental/scroll_area.py b/arcade/gui/experimental/scroll_area.py index 25b75d5d4..5b93a55f1 100644 --- a/arcade/gui/experimental/scroll_area.py +++ b/arcade/gui/experimental/scroll_area.py @@ -6,21 +6,19 @@ import arcade from arcade import XYWH -from arcade.gui import ( - Property, - Surface, +from arcade.gui.events import ( UIEvent, - UIKeyPressEvent, - UILayout, UIMouseDragEvent, UIMouseEvent, UIMouseMovementEvent, UIMousePressEvent, UIMouseReleaseEvent, UIMouseScrollEvent, - UIWidget, - bind, ) +from arcade.gui.property import Property, bind +from arcade.gui.surface import Surface +from arcade.gui.widgets import UIWidget +from arcade.gui.widgets.layout import UILayout from arcade.types import LBWH W = TypeVar("W", bound="UIWidget") @@ -45,8 +43,6 @@ def __init__(self, scroll_area: UIScrollArea, vertical: bool = True): self.with_border(color=arcade.uicolor.GRAY_CONCRETE) self.vertical = vertical - # self._scroll_bar_size = 20 - bind(self, "_thumb_hover", self.trigger_render) bind(self, "_dragging", self.trigger_render) bind(scroll_area, "scroll_x", self.trigger_full_render) @@ -102,9 +98,6 @@ def on_event(self, event: UIEvent) -> bool | None: self._dragging = False return True - if isinstance(event, UIKeyPressEvent): - print(self._scroll_bar_size()) - return EVENT_UNHANDLED def _scroll_bar_size(self): diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index b97505ce3..352180681 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -12,9 +12,11 @@ from typing import Iterable, TypeVar, Union from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED, EventDispatcher +from pyglet.input import Controller from typing_extensions import TypeGuard import arcade +from arcade.experimental.controller_window import ControllerWindow from arcade.gui import UIEvent from arcade.gui.events import ( UIKeyPressEvent, @@ -29,6 +31,13 @@ UITextMotionEvent, UITextMotionSelectEvent, ) +from arcade.gui.events import ( + UIControllerButtonPressEvent, + UIControllerButtonReleaseEvent, + UIControllerDpadEvent, + UIControllerStickEvent, + UIControllerTriggerEvent, +) from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget from arcade.types import LBWH, AnchorPoint, Point2, Rect @@ -278,6 +287,18 @@ def enable(self) -> None: """ if not self._enabled: self._enabled = True + + if isinstance(self.window, ControllerWindow): + controller_handlers = { + self.on_stick_motion, + self.on_trigger_motion, + self.on_button_press, + self.on_button_release, + self.on_dpad_motion, + } + else: + controller_handlers = set() + self.window.push_handlers( self.on_resize, self.on_update, @@ -291,6 +312,7 @@ def enable(self) -> None: self.on_text, self.on_text_motion, self.on_text_motion_select, + *controller_handlers, ) def disable(self) -> None: @@ -301,6 +323,18 @@ def disable(self) -> None: """ if self._enabled: self._enabled = False + + if isinstance(self.window, ControllerWindow): + controller_handlers = { + self.on_stick_motion, + self.on_trigger_motion, + self.on_button_press, + self.on_button_release, + self.on_dpad_motion, + } + else: + controller_handlers = set() + self.window.remove_handlers( self.on_resize, self.on_update, @@ -314,6 +348,7 @@ def disable(self) -> None: self.on_text, self.on_text_motion, self.on_text_motion_select, + *controller_handlers, ) def on_update(self, time_delta): @@ -450,6 +485,21 @@ def on_resize(self, width, height): self.trigger_render() + def on_stick_motion(self, controller: Controller, name, value): + return self.dispatch_ui_event(UIControllerStickEvent(controller, name, value)) + + def on_trigger_motion(self, controller: Controller, name, value): + return self.dispatch_ui_event(UIControllerTriggerEvent(controller, name, value)) + + def on_button_press(self, controller: Controller, button): + return self.dispatch_ui_event(UIControllerButtonPressEvent(controller, button)) + + def on_button_release(self, controller: Controller, button): + return self.dispatch_ui_event(UIControllerButtonReleaseEvent(controller, button)) + + def on_dpad_motion(self, controller: Controller, value): + return self.dispatch_ui_event(UIControllerDpadEvent(controller, value)) + @property def rect(self) -> Rect: """The rect of the UIManager, which is the window size.""" diff --git a/arcade/gui/widgets/dropdown.py b/arcade/gui/widgets/dropdown.py index a7aa6968e..0c7693d61 100644 --- a/arcade/gui/widgets/dropdown.py +++ b/arcade/gui/widgets/dropdown.py @@ -7,13 +7,15 @@ from arcade import uicolor from arcade.gui import UIEvent, UIMousePressEvent from arcade.gui.events import UIOnChangeEvent, UIOnClickEvent +from arcade.gui.events import UIControllerButtonPressEvent +from arcade.gui.experimental.focus import UIFocusMixin from arcade.gui.ui_manager import UIManager from arcade.gui.widgets import UILayout, UIWidget from arcade.gui.widgets.buttons import UIFlatButton from arcade.gui.widgets.layout import UIBoxLayout -class _UIDropdownOverlay(UIBoxLayout): +class _UIDropdownOverlay(UIFocusMixin, UIBoxLayout): """Represents the dropdown options overlay. Currently only handles closing the overlay when clicked outside of the options. @@ -35,6 +37,13 @@ def on_event(self, event: UIEvent) -> Optional[bool]: if not self.rect.point_in_rect((event.x, event.y)): self.hide() return EVENT_HANDLED + + if isinstance(event, UIControllerButtonPressEvent): + # TODO find a better and more generic way to handle controller events for this + if event.button == "b": + self.hide() + return EVENT_HANDLED + return super().on_event(event) @@ -186,6 +195,8 @@ def _update_options(self): ) button.on_click = self._on_option_click + self._overlay.detect_focusable_widgets() + def _find_ui_manager(self): # search tree for UIManager parent = self.parent diff --git a/arcade/gui/widgets/slider.py b/arcade/gui/widgets/slider.py index 70878e913..e4eea1fc4 100644 --- a/arcade/gui/widgets/slider.py +++ b/arcade/gui/widgets/slider.py @@ -95,6 +95,21 @@ def __init__( self.register_event_type("on_change") + def _change_value(self, value: float): + # TODO changing the value itself should trigger this event + # current problem is, that the property does not pass the old value to change listeners + if value < self.min_value: + value = self.min_value + elif value > self.max_value: + value = self.max_value + + if self.value == value: + return + + old_value = self.value + self.value = value + self.dispatch_event("on_change", UIOnChangeEvent(self, old_value, self.value)) + def _x_for_value(self, value: float): """Provides the x coordinate for the given value.""" @@ -110,7 +125,8 @@ def norm_value(self): @norm_value.setter def norm_value(self, value): """Normalized value between 0.0 and 1.0""" - self.value = min(value * (self.max_value - self.min_value) + self.min_value, self.max_value) + new_value = min(value * (self.max_value - self.min_value) + self.min_value, self.max_value) + self._change_value(new_value) @property def _thumb_x(self): @@ -181,9 +197,8 @@ def on_event(self, event: UIEvent) -> bool | None: if isinstance(event, UIMouseDragEvent): if self.pressed: - old_value = self.value self._thumb_x = event.x - self.dispatch_event("on_change", UIOnChangeEvent(self, old_value, self.value)) + return EVENT_HANDLED return EVENT_UNHANDLED diff --git a/arcade/resources/assets/input_prompt/xbox/button_a.png b/arcade/resources/assets/input_prompt/xbox/button_a.png new file mode 100755 index 000000000..2399fc263 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_a.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_a_outline.png b/arcade/resources/assets/input_prompt/xbox/button_a_outline.png new file mode 100755 index 000000000..8dd7cb9f7 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_a_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_b.png b/arcade/resources/assets/input_prompt/xbox/button_b.png new file mode 100755 index 000000000..c66ce33e0 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_b.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_b_outline.png b/arcade/resources/assets/input_prompt/xbox/button_b_outline.png new file mode 100755 index 000000000..474d894f8 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_b_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_back.png b/arcade/resources/assets/input_prompt/xbox/button_back.png new file mode 100755 index 000000000..3318c6a5a Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_back.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_back_icon.png b/arcade/resources/assets/input_prompt/xbox/button_back_icon.png new file mode 100755 index 000000000..89c56da2c Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_back_icon.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_back_icon_outline.png b/arcade/resources/assets/input_prompt/xbox/button_back_icon_outline.png new file mode 100755 index 000000000..a9ee08889 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_back_icon_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_back_outline.png b/arcade/resources/assets/input_prompt/xbox/button_back_outline.png new file mode 100755 index 000000000..f6f1c4fc2 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_back_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_a.png b/arcade/resources/assets/input_prompt/xbox/button_color_a.png new file mode 100755 index 000000000..d9d8fbd8d Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_a.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_a_outline.png b/arcade/resources/assets/input_prompt/xbox/button_color_a_outline.png new file mode 100755 index 000000000..9699da0f5 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_a_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_b.png b/arcade/resources/assets/input_prompt/xbox/button_color_b.png new file mode 100755 index 000000000..b3b63c8bf Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_b.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_b_outline.png b/arcade/resources/assets/input_prompt/xbox/button_color_b_outline.png new file mode 100755 index 000000000..9b94fcb43 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_b_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_x.png b/arcade/resources/assets/input_prompt/xbox/button_color_x.png new file mode 100755 index 000000000..803a3f15a Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_x.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_x_outline.png b/arcade/resources/assets/input_prompt/xbox/button_color_x_outline.png new file mode 100755 index 000000000..5b9eeb5a5 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_x_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_y.png b/arcade/resources/assets/input_prompt/xbox/button_color_y.png new file mode 100755 index 000000000..9f8b0e4c4 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_y.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_color_y_outline.png b/arcade/resources/assets/input_prompt/xbox/button_color_y_outline.png new file mode 100755 index 000000000..d66141877 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_color_y_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_menu.png b/arcade/resources/assets/input_prompt/xbox/button_menu.png new file mode 100755 index 000000000..546418adf Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_menu.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_menu_outline.png b/arcade/resources/assets/input_prompt/xbox/button_menu_outline.png new file mode 100755 index 000000000..1848da340 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_menu_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_share.png b/arcade/resources/assets/input_prompt/xbox/button_share.png new file mode 100755 index 000000000..1722161ad Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_share.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_share_outline.png b/arcade/resources/assets/input_prompt/xbox/button_share_outline.png new file mode 100755 index 000000000..9f9141784 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_share_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_start.png b/arcade/resources/assets/input_prompt/xbox/button_start.png new file mode 100755 index 000000000..907a954a2 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_start.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_start_icon.png b/arcade/resources/assets/input_prompt/xbox/button_start_icon.png new file mode 100755 index 000000000..ac6c97fa6 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_start_icon.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_start_icon_outline.png b/arcade/resources/assets/input_prompt/xbox/button_start_icon_outline.png new file mode 100755 index 000000000..140a84862 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_start_icon_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_start_outline.png b/arcade/resources/assets/input_prompt/xbox/button_start_outline.png new file mode 100755 index 000000000..ae48df9cd Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_start_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_view.png b/arcade/resources/assets/input_prompt/xbox/button_view.png new file mode 100755 index 000000000..141fda1ad Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_view.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_view_outline.png b/arcade/resources/assets/input_prompt/xbox/button_view_outline.png new file mode 100755 index 000000000..33e28fbe6 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_view_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_x.png b/arcade/resources/assets/input_prompt/xbox/button_x.png new file mode 100755 index 000000000..b04ef414e Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_x.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_x_outline.png b/arcade/resources/assets/input_prompt/xbox/button_x_outline.png new file mode 100755 index 000000000..e5dcc0501 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_x_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_y.png b/arcade/resources/assets/input_prompt/xbox/button_y.png new file mode 100755 index 000000000..29dd0acfe Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_y.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/button_y_outline.png b/arcade/resources/assets/input_prompt/xbox/button_y_outline.png new file mode 100755 index 000000000..affbcfa3c Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/button_y_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/controller_xbox360.png b/arcade/resources/assets/input_prompt/xbox/controller_xbox360.png new file mode 100755 index 000000000..d7b71da57 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/controller_xbox360.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/controller_xbox_adaptive.png b/arcade/resources/assets/input_prompt/xbox/controller_xbox_adaptive.png new file mode 100755 index 000000000..3a3d0bc3e Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/controller_xbox_adaptive.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/controller_xboxone.png b/arcade/resources/assets/input_prompt/xbox/controller_xboxone.png new file mode 100755 index 000000000..33c8b7597 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/controller_xboxone.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/controller_xboxseries.png b/arcade/resources/assets/input_prompt/xbox/controller_xboxseries.png new file mode 100755 index 000000000..8c0a19546 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/controller_xboxseries.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad.png b/arcade/resources/assets/input_prompt/xbox/dpad.png new file mode 100755 index 000000000..8090e775a Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_all.png b/arcade/resources/assets/input_prompt/xbox/dpad_all.png new file mode 100755 index 000000000..7da72adb8 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_all.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_down.png b/arcade/resources/assets/input_prompt/xbox/dpad_down.png new file mode 100755 index 000000000..49e244275 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_down.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_down_outline.png b/arcade/resources/assets/input_prompt/xbox/dpad_down_outline.png new file mode 100755 index 000000000..f31d0a5c8 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_down_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_horizontal.png b/arcade/resources/assets/input_prompt/xbox/dpad_horizontal.png new file mode 100755 index 000000000..09dba4e57 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_horizontal.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_horizontal_outline.png b/arcade/resources/assets/input_prompt/xbox/dpad_horizontal_outline.png new file mode 100755 index 000000000..5f7094ace Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_horizontal_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_left.png b/arcade/resources/assets/input_prompt/xbox/dpad_left.png new file mode 100755 index 000000000..681aee87b Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_left.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_left_outline.png b/arcade/resources/assets/input_prompt/xbox/dpad_left_outline.png new file mode 100755 index 000000000..c813a64ae Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_left_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_none.png b/arcade/resources/assets/input_prompt/xbox/dpad_none.png new file mode 100755 index 000000000..d36e045f2 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_none.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_right.png b/arcade/resources/assets/input_prompt/xbox/dpad_right.png new file mode 100755 index 000000000..0f874acfe Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_right.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_right_outline.png b/arcade/resources/assets/input_prompt/xbox/dpad_right_outline.png new file mode 100755 index 000000000..0c60966d7 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_right_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round.png b/arcade/resources/assets/input_prompt/xbox/dpad_round.png new file mode 100755 index 000000000..ad2ca781b Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_all.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_all.png new file mode 100755 index 000000000..81941b5ee Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_all.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_down.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_down.png new file mode 100755 index 000000000..3dddd3295 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_down.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_horizontal.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_horizontal.png new file mode 100755 index 000000000..f19d5c08b Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_horizontal.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_left.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_left.png new file mode 100755 index 000000000..a031aa25c Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_left.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_right.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_right.png new file mode 100755 index 000000000..c234462bd Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_right.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_up.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_up.png new file mode 100755 index 000000000..f18f71895 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_up.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_round_vertical.png b/arcade/resources/assets/input_prompt/xbox/dpad_round_vertical.png new file mode 100755 index 000000000..b1c36ae9a Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_round_vertical.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_up.png b/arcade/resources/assets/input_prompt/xbox/dpad_up.png new file mode 100755 index 000000000..7b103dae8 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_up.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_up_outline.png b/arcade/resources/assets/input_prompt/xbox/dpad_up_outline.png new file mode 100755 index 000000000..0aa2b5779 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_up_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_vertical.png b/arcade/resources/assets/input_prompt/xbox/dpad_vertical.png new file mode 100755 index 000000000..123229ba9 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_vertical.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/dpad_vertical_outline.png b/arcade/resources/assets/input_prompt/xbox/dpad_vertical_outline.png new file mode 100755 index 000000000..d919814ee Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/dpad_vertical_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/guide.png b/arcade/resources/assets/input_prompt/xbox/guide.png new file mode 100755 index 000000000..ddeb758df Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/guide.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/guide_outline.png b/arcade/resources/assets/input_prompt/xbox/guide_outline.png new file mode 100755 index 000000000..0f9c94977 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/guide_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/lb.png b/arcade/resources/assets/input_prompt/xbox/lb.png new file mode 100755 index 000000000..b7b55df79 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/lb.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/lb_outline.png b/arcade/resources/assets/input_prompt/xbox/lb_outline.png new file mode 100755 index 000000000..4b3527fcc Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/lb_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/ls.png b/arcade/resources/assets/input_prompt/xbox/ls.png new file mode 100755 index 000000000..cb6eb93af Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/ls.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/ls_outline.png b/arcade/resources/assets/input_prompt/xbox/ls_outline.png new file mode 100755 index 000000000..337091d1f Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/ls_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/lt.png b/arcade/resources/assets/input_prompt/xbox/lt.png new file mode 100755 index 000000000..ad5c30a74 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/lt.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/lt_outline.png b/arcade/resources/assets/input_prompt/xbox/lt_outline.png new file mode 100755 index 000000000..8792f4660 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/lt_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/rb.png b/arcade/resources/assets/input_prompt/xbox/rb.png new file mode 100755 index 000000000..6582f391c Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/rb.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/rb_outline.png b/arcade/resources/assets/input_prompt/xbox/rb_outline.png new file mode 100755 index 000000000..e8a78e81d Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/rb_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/rs.png b/arcade/resources/assets/input_prompt/xbox/rs.png new file mode 100755 index 000000000..c729cde45 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/rs.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/rs_outline.png b/arcade/resources/assets/input_prompt/xbox/rs_outline.png new file mode 100755 index 000000000..7ea3310ae Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/rs_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/rt.png b/arcade/resources/assets/input_prompt/xbox/rt.png new file mode 100755 index 000000000..670273091 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/rt.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/rt_outline.png b/arcade/resources/assets/input_prompt/xbox/rt_outline.png new file mode 100755 index 000000000..862cbb297 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/rt_outline.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l.png b/arcade/resources/assets/input_prompt/xbox/stick_l.png new file mode 100755 index 000000000..2fcd5fb6b Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_down.png b/arcade/resources/assets/input_prompt/xbox/stick_l_down.png new file mode 100755 index 000000000..a4f93be20 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_down.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_horizontal.png b/arcade/resources/assets/input_prompt/xbox/stick_l_horizontal.png new file mode 100755 index 000000000..c513f8dfd Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_horizontal.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_left.png b/arcade/resources/assets/input_prompt/xbox/stick_l_left.png new file mode 100755 index 000000000..1cab90bf8 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_left.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_press.png b/arcade/resources/assets/input_prompt/xbox/stick_l_press.png new file mode 100755 index 000000000..79cacbd27 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_press.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_right.png b/arcade/resources/assets/input_prompt/xbox/stick_l_right.png new file mode 100755 index 000000000..256df0ce0 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_right.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_up.png b/arcade/resources/assets/input_prompt/xbox/stick_l_up.png new file mode 100755 index 000000000..aa1ac6a06 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_up.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_l_vertical.png b/arcade/resources/assets/input_prompt/xbox/stick_l_vertical.png new file mode 100755 index 000000000..7b2e6aa84 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_l_vertical.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r.png b/arcade/resources/assets/input_prompt/xbox/stick_r.png new file mode 100755 index 000000000..852e14057 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_down.png b/arcade/resources/assets/input_prompt/xbox/stick_r_down.png new file mode 100755 index 000000000..1930ed680 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_down.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_horizontal.png b/arcade/resources/assets/input_prompt/xbox/stick_r_horizontal.png new file mode 100755 index 000000000..f7fa95fb6 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_horizontal.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_left.png b/arcade/resources/assets/input_prompt/xbox/stick_r_left.png new file mode 100755 index 000000000..0d6b849e5 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_left.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_press.png b/arcade/resources/assets/input_prompt/xbox/stick_r_press.png new file mode 100755 index 000000000..faed4c47f Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_press.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_right.png b/arcade/resources/assets/input_prompt/xbox/stick_r_right.png new file mode 100755 index 000000000..0473f3ac6 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_right.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_up.png b/arcade/resources/assets/input_prompt/xbox/stick_r_up.png new file mode 100755 index 000000000..98d7d1e4a Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_up.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_r_vertical.png b/arcade/resources/assets/input_prompt/xbox/stick_r_vertical.png new file mode 100755 index 000000000..5d2cfe94b Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_r_vertical.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_side_l.png b/arcade/resources/assets/input_prompt/xbox/stick_side_l.png new file mode 100755 index 000000000..863fff625 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_side_l.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_side_r.png b/arcade/resources/assets/input_prompt/xbox/stick_side_r.png new file mode 100755 index 000000000..d0079aca6 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_side_r.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_top_l.png b/arcade/resources/assets/input_prompt/xbox/stick_top_l.png new file mode 100755 index 000000000..4d2ff9d87 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_top_l.png differ diff --git a/arcade/resources/assets/input_prompt/xbox/stick_top_r.png b/arcade/resources/assets/input_prompt/xbox/stick_top_r.png new file mode 100755 index 000000000..04e57d869 Binary files /dev/null and b/arcade/resources/assets/input_prompt/xbox/stick_top_r.png differ diff --git a/tests/unit/resources/test_list_resources.py b/tests/unit/resources/test_list_resources.py index c499e4c8a..e62f1015a 100644 --- a/tests/unit/resources/test_list_resources.py +++ b/tests/unit/resources/test_list_resources.py @@ -10,12 +10,12 @@ def test_all(): resources = arcade.resources.list_built_in_assets() - assert len(resources) == pytest.approx(770, abs=10) + assert len(resources) == pytest.approx(863, abs=10) def test_png(): resources = arcade.resources.list_built_in_assets(extensions=(".png",)) - assert len(resources) == pytest.approx(630, abs=10) + assert len(resources) == pytest.approx(723, abs=10) def test_audio():