From 603af4f378405655954290f5df8081580a07740d Mon Sep 17 00:00:00 2001 From: HRiggs Date: Tue, 14 Apr 2026 14:26:10 -0400 Subject: [PATCH] fix: improve redraw speed --- steam_workshop_gui.py | 177 ++++++++++++++++++++++++++++++++---------- ui_theme.py | 12 ++- 2 files changed, 144 insertions(+), 45 deletions(-) diff --git a/steam_workshop_gui.py b/steam_workshop_gui.py index 8384122..54dae0c 100644 --- a/steam_workshop_gui.py +++ b/steam_workshop_gui.py @@ -1639,6 +1639,14 @@ class SteamWorkshopGUI: self.merge_btn = None self.is_rimworld_valid = False self._panel_shells = [] + self._resize_after_id = None + self._bg_image_id = None + self._main_content_window_id = None + self._last_background_size = None + self._last_main_content_geometry = None + self._header_render_after_id = None + self._last_header_requested_size = None + self._last_header_render_size = None # Store references for responsive layout self.left_frame = None @@ -1810,16 +1818,28 @@ class SteamWorkshopGUI: if hasattr(self, 'right_frame') and self.right_frame: self.create_output_section(self.right_frame) - def _update_main_background(self): + def _update_main_background(self, width=None, height=None): """Update the main window background image""" - width = self.root.winfo_width() or 1920 - height = self.root.winfo_height() or 1080 + width = width or self.root.winfo_width() or 1920 + height = height or self.root.winfo_height() or 1080 if width > 1 and height > 1: + size = (int(width), int(height)) + if size == self._last_background_size: + return self.bg_image_tk = self.load_background_image(width, height) if self.bg_image_tk: - self.bg_canvas.delete('bg') - self.bg_canvas.create_image(0, 0, image=self.bg_image_tk, anchor='nw', tags='bg') - self.bg_canvas.tag_lower('bg') + if self._bg_image_id is None: + self._bg_image_id = self.bg_canvas.create_image( + 0, + 0, + image=self.bg_image_tk, + anchor='nw', + tags='bg', + ) + self.bg_canvas.tag_lower('bg') + else: + self.bg_canvas.itemconfigure(self._bg_image_id, image=self.bg_image_tk) + self._last_background_size = size def _position_main_content(self): """Position the main content frame on the canvas with responsive sizing""" @@ -1831,16 +1851,28 @@ class SteamWorkshopGUI: x_pos = max(12, (width - content_width) // 2) y_pos = max(12, (height - content_height) // 2) - self.bg_canvas.delete('main_content') - self.bg_canvas.create_window( - x_pos, - y_pos, - window=self.main_content_frame, - anchor='nw', - width=content_width, - height=content_height, - tags='main_content', - ) + geometry = (x_pos, y_pos, content_width, content_height) + if geometry == self._last_main_content_geometry: + return + + if self._main_content_window_id is None: + self._main_content_window_id = self.bg_canvas.create_window( + x_pos, + y_pos, + window=self.main_content_frame, + anchor='nw', + width=content_width, + height=content_height, + tags='main_content', + ) + else: + self.bg_canvas.coords(self._main_content_window_id, x_pos, y_pos) + self.bg_canvas.itemconfigure( + self._main_content_window_id, + width=content_width, + height=content_height, + ) + self._last_main_content_geometry = geometry def load_background_image(self, width, height): """Load the Desolation art as the main-app backdrop.""" @@ -1862,32 +1894,47 @@ class SteamWorkshopGUI: if event.widget == self.root and hasattr(self, 'bg_canvas'): # First enforce minimum size self._enforce_minimum_size(event) - - # Update background image - self._update_main_background() - + current_width = self.root.winfo_width() current_height = self.root.winfo_height() - - if hasattr(self, 'header_canvas') and self.header_canvas.winfo_exists(): - header_height = max(70, min(102, int(current_height * 0.082))) - if int(self.header_canvas.cget('height')) != header_height: - self.header_canvas.configure(height=header_height) - self._render_header_canvas() - - desired_layout_mode = ( - 'side_by_side' - if self._should_use_side_by_side(current_width, current_height) - else 'stacked' - ) - if desired_layout_mode != getattr(self, '_current_layout_mode', None): - self.root.after(100, self._update_responsive_layout) - - # Store current window size - self._last_window_size = (current_width, current_height) - - # Update content positioning self._position_main_content() + + if self._resize_after_id is not None: + self.root.after_cancel(self._resize_after_id) + self._resize_after_id = self.root.after( + 70, + lambda width=current_width, height=current_height: self._apply_main_resize(width, height), + ) + + def _apply_main_resize(self, width, height): + """Apply the expensive resize work after the user pauses dragging.""" + self._resize_after_id = None + if not self.root.winfo_exists(): + return + + current_width = self.root.winfo_width() + current_height = self.root.winfo_height() + width = current_width or width + height = current_height or height + + self._update_main_background(width, height) + + if hasattr(self, 'header_canvas') and self.header_canvas.winfo_exists(): + header_height = max(70, min(102, int(height * 0.082))) + if int(self.header_canvas.cget('height')) != header_height: + self.header_canvas.configure(height=header_height) + else: + self._schedule_header_render() + + desired_layout_mode = ( + 'side_by_side' + if self._should_use_side_by_side(width, height) + else 'stacked' + ) + if desired_layout_mode != getattr(self, '_current_layout_mode', None): + self._update_responsive_layout() + + self._last_window_size = (width, height) def _on_window_map(self, event): """Handle window map event (window becomes visible)""" @@ -1942,14 +1989,27 @@ class SteamWorkshopGUI: self.progression_logo_source = None self.header_canvas.bind('', self._on_header_canvas_configure) - self.root.after_idle(self._render_header_canvas) + self.root.after_idle(self._schedule_header_render) def _on_header_canvas_configure(self, event): """Refresh the header texture and logo when the header size changes.""" - self._render_header_canvas() + if event.widget != self.header_canvas: + return + requested_size = (max(1, int(event.width)), max(1, int(event.height))) + if requested_size == self._last_header_requested_size: + return + self._last_header_requested_size = requested_size + self._schedule_header_render() + + def _schedule_header_render(self): + """Debounce expensive header redraw work during window resizes.""" + if self._header_render_after_id is not None: + self.root.after_cancel(self._header_render_after_id) + self._header_render_after_id = self.root.after(40, self._render_header_canvas) def _render_header_canvas(self): """Render the striped header background and scale the logo to fit it.""" + self._header_render_after_id = None if not hasattr(self, 'header_canvas') or not self.header_canvas.winfo_exists(): return @@ -1957,6 +2017,9 @@ class SteamWorkshopGUI: height = max(1, self.header_canvas.winfo_height()) if width < 40 or height < 24: return + render_size = (width, height) + if render_size == self._last_header_render_size: + return header_texture = create_striped_texture( width, @@ -2017,6 +2080,7 @@ class SteamWorkshopGUI: anchor='center', tags='header_content', ) + self._last_header_render_size = render_size def create_footer_frame(self, parent): """Create footer frame with HR Systems logo in bottom right - responsive sizing""" @@ -2403,17 +2467,40 @@ class SteamWorkshopGUI: 'inner_padding': (pad_x, pad_y), } canvas._panel_window_id = window_id + canvas._panel_after_id = None + canvas._panel_requested_size = None + canvas._panel_rendered_size = None canvas.bind('', self._refresh_panel_shell) self._panel_shells.append(canvas) - self.root.after_idle(lambda current_canvas=canvas: self._render_panel_shell(current_canvas)) + self.root.after_idle(lambda current_canvas=canvas: self._schedule_panel_shell_render(current_canvas)) return shell, inner_frame def _refresh_panel_shell(self, event): """Repaint a striped shell when its canvas changes size.""" - self._render_panel_shell(event.widget) + canvas = event.widget + if not canvas.winfo_exists(): + return + requested_size = (max(1, int(event.width)), max(1, int(event.height))) + if requested_size == getattr(canvas, '_panel_requested_size', None): + return + canvas._panel_requested_size = requested_size + self._schedule_panel_shell_render(canvas) + + def _schedule_panel_shell_render(self, canvas): + """Debounce striped shell redraw work during resizes.""" + if not canvas.winfo_exists(): + return + after_id = getattr(canvas, '_panel_after_id', None) + if after_id is not None: + self.root.after_cancel(after_id) + canvas._panel_after_id = self.root.after( + 40, + lambda current_canvas=canvas: self._render_panel_shell(current_canvas), + ) def _render_panel_shell(self, canvas): """Render the shared striped halo panel background onto a shell canvas.""" + canvas._panel_after_id = None if not canvas.winfo_exists() or not hasattr(canvas, '_panel_options'): return @@ -2426,6 +2513,9 @@ class SteamWorkshopGUI: minimum_size = (halo_padding * 2) + 6 if width < minimum_size or height < minimum_size: return + render_size = (width, height) + if render_size == getattr(canvas, '_panel_rendered_size', None): + return panel_width = max(1, width - (halo_padding * 2)) panel_height = max(1, height - (halo_padding * 2)) @@ -2457,6 +2547,7 @@ class SteamWorkshopGUI: content_height = max(1, height - (2 * (halo_padding + inner_pad_y))) canvas.coords(canvas._panel_window_id, halo_padding + inner_pad_x, halo_padding + inner_pad_y) canvas.itemconfigure(canvas._panel_window_id, width=content_width, height=content_height) + canvas._panel_rendered_size = render_size def create_steam_card(self, parent, title=None, padding=8): """Create a striped charcoal card container.""" diff --git a/ui_theme.py b/ui_theme.py index 66ee958..21ce16f 100644 --- a/ui_theme.py +++ b/ui_theme.py @@ -1,3 +1,5 @@ +from functools import lru_cache + from PIL import Image, ImageColor, ImageDraw @@ -26,6 +28,12 @@ COLORS = { } +@lru_cache(maxsize=4) +def _load_rgba_source(image_path): + """Load and cache a source image in RGBA form for repeated resizes.""" + return Image.open(image_path).convert("RGBA") + + def _rgba(color, alpha=255): red, green, blue = ImageColor.getrgb(color) return red, green, blue, alpha @@ -187,7 +195,7 @@ def load_cover_background(image_path, width, height, *, overlay_color=None, over width = max(1, int(width)) height = max(1, int(height)) - image = Image.open(image_path).convert("RGBA") + image = _load_rgba_source(image_path) source_ratio = image.width / image.height target_ratio = width / height @@ -198,7 +206,7 @@ def load_cover_background(image_path, width, height, *, overlay_color=None, over new_width = width new_height = int(width / source_ratio) - image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) + image = image.resize((new_width, new_height), Image.Resampling.BICUBIC) left = (new_width - width) // 2 top = (new_height - height) // 2 image = image.crop((left, top, left + width, top + height))