Skip to content

Instantly share code, notes, and snippets.

@thegamecracks
Created March 24, 2024 16:12
Show Gist options
  • Save thegamecracks/f5193874e2ae83ff32fc5e39b74366bb to your computer and use it in GitHub Desktop.
Save thegamecracks/f5193874e2ae83ff32fc5e39b74366bb to your computer and use it in GitHub Desktop.
An attempt to make a flexbox-like manager in Tkinter
import sys
from tkinter import Event, Tk
from tkinter.ttk import Button, Frame, Widget
from typing import Literal
FlexboxMode = Literal["horizontal", "vertical"]
class Flexbox(Frame):
def __init__(
self,
*args,
mode: FlexboxMode = "horizontal",
**kwargs,
):
super().__init__(*args, **kwargs)
self.__mode: FlexboxMode = mode
self.__widgets: list[Widget] = []
self.__refreshed = False
self.bind("<Configure>", self.__on_configure)
self.bind("<<FlexboxChange>>", self.__on_flexbox_change)
@property
def mode(self) -> FlexboxMode:
return self.__mode
@mode.setter
def mode(self, mode: FlexboxMode) -> None:
self.__mode = mode
self.event_generate("<<FlexboxChange>>")
def add(self, widget: Widget) -> None:
self.__widgets.append(widget)
self.event_generate("<<FlexboxChange>>")
def remove(self, widget: Widget) -> None:
self.__widgets.remove(widget)
self.event_generate("<<FlexboxChange>>")
def __on_configure(self, event: Event) -> None:
self.__refresh(event.width, event.height)
def __on_flexbox_change(self, event: Event) -> None:
self.__refresh()
def __refresh(self, width: int | None = None, height: int | None = None) -> None:
def add_widget_length(widget: Widget) -> None:
nonlocal length
widget.grid(row=row, column=column)
bbox = self.grid_bbox(row=row, column=column)
assert bbox is not None
if horizontal:
length += bbox[2]
else:
length += bbox[3]
def is_first_widget() -> bool:
return horizontal and column == 0 or not horizontal and row == 0
def show_widget(widget: Widget) -> None:
nonlocal column, i, row
# Widget is already mapped by add_widget_length()
# widget.grid(row=row, column=column)
if horizontal:
column += 1
else:
row += 1
i += 1
def move_to_next_line() -> None:
nonlocal column, length, row
length = 0
if horizontal:
row += 1
column = 0
else:
row = 0
column += 1
width = width or self.winfo_width()
height = height or self.winfo_height()
# Prevent excessive refreshing in one frame
if self.__refreshed:
return
elif width <= 1 or height <= 1:
# JANK: probably not rendered yet, need to wait
self.after(50, self.__refresh)
return
else:
self.__refreshed = True
self.after(16, self.__unset_refresh)
horizontal = self.mode != "vertical"
max_length = width if horizontal else height
widgets = self.__widgets.copy()
i = 0
row = 0
column = 0
length = 0
while i < len(widgets):
widget = widgets[i]
add_widget_length(widget)
if length <= max_length or is_first_widget():
show_widget(widget)
else:
move_to_next_line()
def __unset_refresh(self) -> None:
self.__refreshed = False
def switch_flexbox_mode(mode: FlexboxMode | None = None):
if mode is None:
mode = "vertical" if flexbox.mode == "horizontal" else "horizontal"
flexbox.mode = mode
app.title(f"Flexbox Demo ({mode.capitalize()})")
# Make example look prettier on windows
if sys.platform == "win32":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(2)
app = Tk()
app.geometry("640x480")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)
flexbox = Flexbox(app)
switch_flexbox_mode("horizontal")
flexbox.grid(sticky="nesw")
for i in range(10):
button = Button(
flexbox,
text=f"Button {i+1}",
padding=i * 10,
command=switch_flexbox_mode,
)
flexbox.add(button)
app.mainloop()
@thegamecracks
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment