aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--discord/ui/button.py30
-rw-r--r--discord/ui/item.py23
-rw-r--r--discord/ui/select.py32
-rw-r--r--discord/ui/view.py80
4 files changed, 120 insertions, 45 deletions
diff --git a/discord/ui/button.py b/discord/ui/button.py
index 220d55de..777d31ce 100644
--- a/discord/ui/button.py
+++ b/discord/ui/button.py
@@ -66,6 +66,12 @@ class Button(Item[V]):
The label of the button, if any.
emoji: Optional[:class:`PartialEmoji`]
The emoji of the button, if available.
+ row: Optional[:class:`int`]
+ The relative row this button belongs to. A Discord component can only have 5
+ rows. By default, items are arranged automatically into those 5 rows. If you'd
+ like to control the relative positioning of the row then passing an index is advised.
+ For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
+ ordering. The row number cannot be negative or greater than 5.
"""
__item_repr_attributes__: Tuple[str, ...] = (
@@ -74,7 +80,7 @@ class Button(Item[V]):
'disabled',
'label',
'emoji',
- 'group_id',
+ 'row',
)
def __init__(
@@ -86,7 +92,7 @@ class Button(Item[V]):
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, PartialEmoji]] = None,
- group: Optional[int] = None,
+ row: Optional[int] = None,
):
super().__init__()
if custom_id is not None and url is not None:
@@ -110,7 +116,7 @@ class Button(Item[V]):
style=style,
emoji=emoji,
)
- self.group_id = group
+ self.row = row
@property
def style(self) -> ButtonStyle:
@@ -189,7 +195,7 @@ class Button(Item[V]):
custom_id=button.custom_id,
url=button.url,
emoji=button.emoji,
- group=None,
+ row=None,
)
@property
@@ -213,7 +219,7 @@ def button(
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, PartialEmoji]] = None,
- group: Optional[int] = None,
+ row: Optional[int] = None,
) -> Callable[[ItemCallbackType], ItemCallbackType]:
"""A decorator that attaches a button to a component.
@@ -242,12 +248,12 @@ def button(
Whether the button is disabled or not. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`PartialEmoji`.
- group: Optional[:class:`int`]
- The relative group this button belongs to. A Discord component can only have 5
- groups. By default, items are arranged automatically into those 5 groups. If you'd
- like to control the relative positioning of the group then passing an index is advised.
- For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
- ordering.
+ row: Optional[:class:`int`]
+ The relative row this button belongs to. A Discord component can only have 5
+ rows. By default, items are arranged automatically into those 5 rows. If you'd
+ like to control the relative positioning of the row then passing an index is advised.
+ For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
+ ordering. The row number cannot be negative or greater than 5.
"""
def decorator(func: ItemCallbackType) -> ItemCallbackType:
@@ -264,7 +270,7 @@ def button(
'disabled': disabled,
'label': label,
'emoji': emoji,
- 'group': group,
+ 'row': row,
}
return func
diff --git a/discord/ui/item.py b/discord/ui/item.py
index e6892bf6..6744f12d 100644
--- a/discord/ui/item.py
+++ b/discord/ui/item.py
@@ -50,11 +50,12 @@ class Item(Generic[V]):
- :class:`discord.ui.Button`
"""
- __item_repr_attributes__: Tuple[str, ...] = ('group_id',)
+ __item_repr_attributes__: Tuple[str, ...] = ('row',)
def __init__(self):
self._view: Optional[V] = None
- self.group_id: Optional[int] = None
+ self._row: Optional[int] = None
+ self._rendered_row: Optional[int] = None
def to_component_dict(self) -> Dict[str, Any]:
raise NotImplementedError
@@ -81,6 +82,24 @@ class Item(Generic[V]):
return f'<{self.__class__.__name__} {attrs}>'
@property
+ def row(self) -> Optional[int]:
+ return self._row
+
+ @row.setter
+ def row(self, value: Optional[int]):
+ if value is None:
+ self._row = None
+ elif 5 > value >= 0:
+ self._row = value
+ else:
+ raise ValueError('row cannot be negative or greater than or equal to 5')
+
+ @property
+ def width(self) -> int:
+ """:class:`int`: The width of the item."""
+ return 1
+
+ @property
def view(self) -> Optional[V]:
"""Optional[:class:`View`]: The underlying view for this item."""
return self._view
diff --git a/discord/ui/select.py b/discord/ui/select.py
index cbbee3bc..e37b55c0 100644
--- a/discord/ui/select.py
+++ b/discord/ui/select.py
@@ -75,6 +75,12 @@ class Select(Item[V]):
Defaults to 1 and must be between 1 and 25.
options: List[:class:`discord.SelectOption`]
A list of options that can be selected in this menu.
+ row: Optional[:class:`int`]
+ The relative row this select menu belongs to. A Discord component can only have 5
+ rows. By default, items are arranged automatically into those 5 rows. If you'd
+ like to control the relative positioning of the row then passing an index is advised.
+ For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
+ ordering. The row number cannot be negative or greater than 5.
"""
__item_repr_attributes__: Tuple[str, ...] = (
@@ -92,7 +98,7 @@ class Select(Item[V]):
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
- group: Optional[int] = None,
+ row: Optional[int] = None,
) -> None:
self._selected_values: List[str] = []
custom_id = os.urandom(16).hex() if custom_id is MISSING else custom_id
@@ -105,7 +111,7 @@ class Select(Item[V]):
max_values=max_values,
options=options,
)
- self.group_id = group
+ self.row = row
@property
def custom_id(self) -> str:
@@ -229,6 +235,10 @@ class Select(Item[V]):
"""List[:class:`str`]: A list of values that have been selected by the user."""
return self._selected_values
+ @property
+ def width(self) -> int:
+ return 5
+
def to_component_dict(self) -> SelectMenuPayload:
return self._underlying.to_dict()
@@ -247,7 +257,7 @@ class Select(Item[V]):
min_values=component.min_values,
max_values=component.max_values,
options=component.options,
- group=None,
+ row=None,
)
@property
@@ -265,7 +275,7 @@ def select(
min_values: int = 1,
max_values: int = 1,
options: List[SelectOption] = MISSING,
- group: Optional[int] = None,
+ row: Optional[int] = None,
) -> Callable[[ItemCallbackType], ItemCallbackType]:
"""A decorator that attaches a select menu to a component.
@@ -281,12 +291,12 @@ def select(
custom_id: :class:`str`
The ID of the select menu that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
- group: Optional[:class:`int`]
- The relative group this select menu belongs to. A Discord component can only have 5
- groups. By default, items are arranged automatically into those 5 groups. If you'd
- like to control the relative positioning of the group then passing an index is advised.
- For example, group=1 will show up before group=2. Defaults to ``None``, which is automatic
- ordering.
+ row: Optional[:class:`int`]
+ The relative row this select menu belongs to. A Discord component can only have 5
+ rows. By default, items are arranged automatically into those 5 rows. If you'd
+ like to control the relative positioning of the row then passing an index is advised.
+ For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic
+ ordering. The row number cannot be negative or greater than 5.
min_values: :class:`int`
The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25.
@@ -305,7 +315,7 @@ def select(
func.__discord_ui_model_kwargs__ = {
'placeholder': placeholder,
'custom_id': custom_id,
- 'group': group,
+ 'row': row,
'min_values': min_values,
'max_values': max_values,
'options': options,
diff --git a/discord/ui/view.py b/discord/ui/view.py
index aa4d52b5..a8882824 100644
--- a/discord/ui/view.py
+++ b/discord/ui/view.py
@@ -67,6 +67,47 @@ def _component_to_item(component: Component) -> Item:
return Item.from_component(component)
+class _ViewWeights:
+ __slots__ = (
+ 'weights',
+ )
+
+ def __init__(self, children: List[Item]):
+ self.weights: List[int] = [0, 0, 0, 0, 0]
+
+ key = lambda i: sys.maxsize if i.row is None else i.row
+ children = sorted(children, key=key)
+ for row, group in groupby(children, key=key):
+ for item in group:
+ self.add_item(item)
+
+ def find_open_space(self, item: Item) -> int:
+ for index, weight in enumerate(self.weights):
+ if weight + item.width <= 5:
+ return index
+
+ raise ValueError('could not find open space for item')
+
+ def add_item(self, item: Item) -> None:
+ if item.row is not None:
+ total = self.weights[item.row] + item.width
+ if total > 5:
+ raise ValueError(f'item would not fit at row {item.row} ({total} > 5 width)')
+ self.weights[item.row] = total
+ item._rendered_row = item.row
+ else:
+ index = self.find_open_space(item)
+ self.weights[index] += item.width
+ item._rendered_row = index
+
+ def remove_item(self, item: Item) -> None:
+ if item._rendered_row is not None:
+ self.weights[item._rendered_row] -= item.width
+ item._rendered_row = None
+
+ def clear(self) -> None:
+ self.weights = [0, 0, 0, 0, 0]
+
class View:
"""Represents a UI view.
@@ -112,6 +153,7 @@ class View:
setattr(self, func.__name__, item)
self.children.append(item)
+ self.__weights = _ViewWeights(self.children)
loop = asyncio.get_running_loop()
self.id = os.urandom(16).hex()
self._cancel_callback: Optional[Callable[[View], None]] = None
@@ -120,29 +162,21 @@ class View:
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
- if item.group_id is None:
- return sys.maxsize
- return item.group_id
+ return item._rendered_row or 0
children = sorted(self.children, key=key)
components: List[Dict[str, Any]] = []
for _, group in groupby(children, key=key):
- group = list(group)
- if len(group) <= 5:
- components.append(
- {
- 'type': 1,
- 'components': [item.to_component_dict() for item in group],
- }
- )
- else:
- components.extend(
- {
- 'type': 1,
- 'components': [item.to_component_dict() for item in group[index : index + 5]],
- }
- for index in range(0, len(group), 5)
- )
+ children = [item.to_component_dict() for item in group]
+ if not children:
+ continue
+
+ components.append(
+ {
+ 'type': 1,
+ 'components': children,
+ }
+ )
return components
@@ -165,7 +199,8 @@ class View:
TypeError
A :class:`Item` was not passed.
ValueError
- Maximum number of children has been exceeded (25).
+ Maximum number of children has been exceeded (25)
+ or the row the item is trying to be added to is full.
"""
if len(self.children) > 25:
@@ -174,6 +209,8 @@ class View:
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__!r}')
+ self.__weights.add_item(item)
+
item._view = self
self.children.append(item)
@@ -190,10 +227,13 @@ class View:
self.children.remove(item)
except ValueError:
pass
+ else:
+ self.__weights.remove_item(item)
def clear_items(self) -> None:
"""Removes all items from the view."""
self.children.clear()
+ self.__weights.clear()
async def interaction_check(self, interaction: Interaction) -> bool:
"""|coro|