redesign: UI Work
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
from PIL import Image, ImageColor, ImageDraw
|
||||
|
||||
|
||||
FONT_FAMILY = "Georgia"
|
||||
|
||||
COLORS = {
|
||||
"bg_primary": "#151515",
|
||||
"bg_secondary": "#1d1d1d",
|
||||
"bg_tertiary": "#212121",
|
||||
"bg_card": "#2b2b2b",
|
||||
"bg_hover": "#36312c",
|
||||
"bg_error": "#2f2926",
|
||||
"text_primary": "#f5f2ec",
|
||||
"text_highlight": "#f0a621",
|
||||
"text_body": "#c8c8c8",
|
||||
"text_secondary": "#ffffff",
|
||||
"text_muted": "#979797",
|
||||
"accent_green": "#3a8e35",
|
||||
"accent_red": "#ff4a46",
|
||||
"accent_yellow": "#f0a621",
|
||||
"accent_blue": "#d7b56a",
|
||||
"border_light": "#4d453b",
|
||||
"border_dark": "#0f0f0f",
|
||||
"stripe": "#1d1d1d",
|
||||
"stripe_soft": "#232323",
|
||||
}
|
||||
|
||||
|
||||
def _rgba(color, alpha=255):
|
||||
red, green, blue = ImageColor.getrgb(color)
|
||||
return red, green, blue, alpha
|
||||
|
||||
|
||||
def create_striped_texture(
|
||||
width,
|
||||
height,
|
||||
*,
|
||||
base_color=None,
|
||||
stripe_color=None,
|
||||
stripe_width=16,
|
||||
stripe_gap=18,
|
||||
stripe_alpha=110,
|
||||
corner_radius=0,
|
||||
border_color=None,
|
||||
border_width=0,
|
||||
border_alpha=255,
|
||||
):
|
||||
"""Create a dark diagonal striped texture matching the pack UI style."""
|
||||
width = max(1, int(width))
|
||||
height = max(1, int(height))
|
||||
|
||||
base = Image.new(
|
||||
"RGBA",
|
||||
(width, height),
|
||||
_rgba(base_color or COLORS["bg_card"]),
|
||||
)
|
||||
|
||||
stripes = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
stripe_draw = ImageDraw.Draw(stripes)
|
||||
span = width + height
|
||||
step = max(1, stripe_width + stripe_gap)
|
||||
|
||||
for offset in range(-height, span + height, step):
|
||||
stripe_draw.line(
|
||||
(offset, 0, offset + height, height),
|
||||
fill=_rgba(stripe_color or COLORS["stripe"], stripe_alpha),
|
||||
width=max(1, stripe_width),
|
||||
)
|
||||
|
||||
image = Image.alpha_composite(base, stripes)
|
||||
|
||||
if border_color and border_width > 0:
|
||||
border_draw = ImageDraw.Draw(image)
|
||||
inset = max(1, border_width // 2)
|
||||
bounds = (inset, inset, width - inset - 1, height - inset - 1)
|
||||
if corner_radius > 0:
|
||||
border_draw.rounded_rectangle(
|
||||
bounds,
|
||||
radius=max(1, corner_radius - inset),
|
||||
outline=_rgba(border_color, border_alpha),
|
||||
width=border_width,
|
||||
)
|
||||
else:
|
||||
border_draw.rectangle(
|
||||
bounds,
|
||||
outline=_rgba(border_color, border_alpha),
|
||||
width=border_width,
|
||||
)
|
||||
|
||||
if corner_radius > 0:
|
||||
mask = Image.new("L", (width, height), 0)
|
||||
mask_draw = ImageDraw.Draw(mask)
|
||||
mask_draw.rounded_rectangle(
|
||||
(0, 0, width - 1, height - 1),
|
||||
radius=corner_radius,
|
||||
fill=255,
|
||||
)
|
||||
clipped = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
clipped.paste(image, (0, 0), mask)
|
||||
image = clipped
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def create_striped_panel(
|
||||
width,
|
||||
height,
|
||||
*,
|
||||
panel_color=None,
|
||||
stripe_color=None,
|
||||
stripe_width=18,
|
||||
stripe_gap=18,
|
||||
stripe_alpha=110,
|
||||
halo_padding=22,
|
||||
halo_alpha=135,
|
||||
panel_alpha=240,
|
||||
corner_radius=18,
|
||||
border_color=None,
|
||||
border_width=1,
|
||||
border_alpha=255,
|
||||
):
|
||||
"""Create a mostly solid panel with stripes that extend just beyond its edges."""
|
||||
width = max(1, int(width))
|
||||
height = max(1, int(height))
|
||||
halo_padding = max(0, int(halo_padding))
|
||||
total_width = width + (halo_padding * 2)
|
||||
total_height = height + (halo_padding * 2)
|
||||
|
||||
image = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||
|
||||
stripes = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||
stripe_draw = ImageDraw.Draw(stripes)
|
||||
span = total_width + total_height
|
||||
step = max(1, stripe_width + stripe_gap)
|
||||
|
||||
for offset in range(-total_height, span + total_height, step):
|
||||
stripe_draw.line(
|
||||
(offset, 0, offset + total_height, total_height),
|
||||
fill=_rgba(stripe_color or COLORS["stripe"], halo_alpha),
|
||||
width=max(1, stripe_width),
|
||||
)
|
||||
|
||||
halo_mask = Image.new("L", (total_width, total_height), 0)
|
||||
halo_draw = ImageDraw.Draw(halo_mask)
|
||||
halo_draw.rounded_rectangle(
|
||||
(0, 0, total_width - 1, total_height - 1),
|
||||
radius=max(1, corner_radius + halo_padding),
|
||||
fill=255,
|
||||
)
|
||||
halo_image = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||
halo_image.paste(stripes, (0, 0), halo_mask)
|
||||
image = Image.alpha_composite(image, halo_image)
|
||||
|
||||
panel_left = halo_padding
|
||||
panel_top = halo_padding
|
||||
panel_right = panel_left + width - 1
|
||||
panel_bottom = panel_top + height - 1
|
||||
|
||||
panel = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||
panel_draw = ImageDraw.Draw(panel)
|
||||
panel_draw.rounded_rectangle(
|
||||
(panel_left, panel_top, panel_right, panel_bottom),
|
||||
radius=corner_radius,
|
||||
fill=_rgba(panel_color or COLORS["bg_card"], panel_alpha),
|
||||
)
|
||||
|
||||
if border_color and border_width > 0:
|
||||
inset = max(1, border_width // 2)
|
||||
panel_draw.rounded_rectangle(
|
||||
(
|
||||
panel_left + inset,
|
||||
panel_top + inset,
|
||||
panel_right - inset,
|
||||
panel_bottom - inset,
|
||||
),
|
||||
radius=max(1, corner_radius - inset),
|
||||
outline=_rgba(border_color, border_alpha),
|
||||
width=border_width,
|
||||
)
|
||||
|
||||
image = Image.alpha_composite(image, panel)
|
||||
return image
|
||||
|
||||
|
||||
def load_cover_background(image_path, width, height, *, overlay_color=None, overlay_alpha=125):
|
||||
"""Load an image, scale it to cover, and apply a dark overlay for readability."""
|
||||
width = max(1, int(width))
|
||||
height = max(1, int(height))
|
||||
|
||||
image = Image.open(image_path).convert("RGBA")
|
||||
source_ratio = image.width / image.height
|
||||
target_ratio = width / height
|
||||
|
||||
if source_ratio > target_ratio:
|
||||
new_height = height
|
||||
new_width = int(height * source_ratio)
|
||||
else:
|
||||
new_width = width
|
||||
new_height = int(width / source_ratio)
|
||||
|
||||
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
left = (new_width - width) // 2
|
||||
top = (new_height - height) // 2
|
||||
image = image.crop((left, top, left + width, top + height))
|
||||
|
||||
overlay = Image.new(
|
||||
"RGBA",
|
||||
(width, height),
|
||||
_rgba(overlay_color or COLORS["bg_primary"], overlay_alpha),
|
||||
)
|
||||
return Image.alpha_composite(image, overlay)
|
||||
|
||||
|
||||
def style_text_button(
|
||||
button,
|
||||
foreground,
|
||||
background,
|
||||
*,
|
||||
hover_foreground=None,
|
||||
disabled_foreground=None,
|
||||
):
|
||||
"""Style a tkinter Button to look like a color-coded text action."""
|
||||
button.configure(
|
||||
bg=background,
|
||||
fg=foreground,
|
||||
activebackground=background,
|
||||
activeforeground=hover_foreground or COLORS["text_primary"],
|
||||
relief="flat",
|
||||
bd=0,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
disabledforeground=disabled_foreground or COLORS["text_muted"],
|
||||
cursor="hand2",
|
||||
)
|
||||
|
||||
|
||||
def center_window(window, width, height, parent=None):
|
||||
"""Center a window either on its parent or the current screen."""
|
||||
width = int(width)
|
||||
height = int(height)
|
||||
window.update_idletasks()
|
||||
|
||||
if parent is not None and parent.winfo_exists():
|
||||
x = parent.winfo_rootx() + (parent.winfo_width() // 2) - (width // 2)
|
||||
y = parent.winfo_rooty() + (parent.winfo_height() // 2) - (height // 2)
|
||||
else:
|
||||
x = (window.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (window.winfo_screenheight() // 2) - (height // 2)
|
||||
|
||||
window.geometry(f"{width}x{height}+{max(0, x)}+{max(0, y)}")
|
||||
Reference in New Issue
Block a user