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)}")