Skip to content

Commit 4421e14

Browse files
committed
Frontend optimization
1 parent 6e9b3d3 commit 4421e14

7 files changed

Lines changed: 98 additions & 102 deletions

File tree

src/frontend/ui_dialogs/config_dialog.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from frontend.ui_dialogs.collapsible_section import CollapsibleSection
1919
from frontend.ui_dialogs.keyboard_nav import setup_arrow_enter_navigation
2020
from frontend.ui_dialogs.scrollable_frame import ScrollableFrame
21-
from frontend.window_utils import fit_and_center, make_modal
21+
from frontend.window_utils import bind_wraplength, fit_and_center, make_modal
2222
from utils import get_logger
2323

2424
logger = get_logger(__name__)
@@ -223,15 +223,7 @@ def _build_ui(self) -> None:
223223

224224
self._scroll.bind_new_children()
225225

226-
def _update_wraplength(_e: tk.Event | None = None) -> None: # type: ignore[type-arg]
227-
w = form.winfo_width()
228-
if w > 100:
229-
wrap = max(200, w - 60)
230-
for lbl in self._desc_labels:
231-
lbl.configure(wraplength=wrap)
232-
233-
form.bind("<Configure>", _update_wraplength)
234-
self.win.after(50, _update_wraplength)
226+
bind_wraplength(form, self._desc_labels, pad=60, min_wrap=200)
235227

236228
btn_save.focus_set()
237229

src/frontend/ui_dialogs/help_dialog.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from frontend.ui_dialogs.collapsible_section import CollapsibleSection
1818
from frontend.ui_dialogs.keyboard_nav import setup_arrow_enter_navigation
1919
from frontend.ui_dialogs.scrollable_frame import ScrollableFrame
20-
from frontend.window_utils import fit_and_center, make_modal
20+
from frontend.window_utils import bind_wraplength, fit_and_center, make_modal
2121
from utils import get_logger
2222

2323
YOUTUBE_CHANNEL_URL = "https://www.youtube.com/@whenphysics"
@@ -257,15 +257,7 @@ def _build_ui(self) -> None:
257257

258258
self._scroll.bind_new_children()
259259

260-
def _update_wraplength(_e: tk.Event | None = None) -> None: # type: ignore[type-arg]
261-
w = inner.winfo_width()
262-
if w > 100:
263-
wrap = max(200, w - 48)
264-
for lbl in self._body_labels:
265-
lbl.configure(wraplength=wrap)
266-
267-
inner.bind("<Configure>", _update_wraplength)
268-
self.win.after(50, lambda: _update_wraplength(None))
260+
bind_wraplength(inner, self._body_labels, pad=48, min_wrap=200)
269261

270262
def _add_section(
271263
self,

src/frontend/ui_dialogs/parameters_dialog.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from frontend.ui_dialogs.keyboard_nav import setup_arrow_enter_navigation
2121
from frontend.ui_dialogs.scrollable_frame import ScrollableFrame
2222
from frontend.ui_dialogs.tooltip import ToolTip
23-
from frontend.window_utils import fit_and_center, make_modal
23+
from frontend.window_utils import bind_wraplength, fit_and_center, make_modal
2424
from utils import DifferentialLabError, get_logger
2525

2626
logger = get_logger(__name__)
@@ -456,23 +456,14 @@ def _display_name_for(pname: str) -> str:
456456
self._stats_desc_label.pack(anchor=tk.W, pady=(4, 0))
457457
self._stats_listbox.bind("<<ListboxSelect>>", self._on_stats_select)
458458

459-
def _update_wraplength(_e: tk.Event | None = None) -> None: # type: ignore[type-arg]
460-
w = scroll_frame.winfo_width()
461-
if w > 100:
462-
formula_wrap = max(200, w - 2 * pad)
463-
formula_lbl.configure(wraplength=formula_wrap)
464-
# Use actual width of stats_frame for solver text (always visible)
465-
mw = stats_frame.winfo_width()
466-
if mw > 100:
467-
col_wrap = max(150, mw - 2 * pad)
468-
self.method_desc.configure(wraplength=col_wrap)
469-
self._stats_desc_label.configure(wraplength=col_wrap)
470-
471-
scroll_frame.bind("<Configure>", _update_wraplength)
472-
stats_frame.bind("<Configure>", _update_wraplength)
459+
bind_wraplength(scroll_frame, formula_lbl, pad=2 * pad, min_wrap=200)
460+
bind_wraplength(
461+
stats_frame,
462+
[self.method_desc, self._stats_desc_label],
463+
pad=2 * pad,
464+
min_wrap=150,
465+
)
473466
scroll.bind_new_children()
474-
self.win.after(50, _update_wraplength)
475-
self.win.after(150, _update_wraplength)
476467
btn_solve.focus_set()
477468

478469
# ------------------------------------------------------------------

src/frontend/ui_dialogs/result_dialog.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import tkinter as tk
66
from pathlib import Path
77
from tkinter import filedialog, messagebox, ttk
8-
from typing import TYPE_CHECKING, Any
8+
from typing import TYPE_CHECKING, Any, Callable
99

1010
import numpy as np
1111

@@ -237,6 +237,38 @@ def _build_transform_controls(
237237
combo.pack(side=tk.LEFT, padx=(0, 4))
238238
combo.bind("<<ComboboxSelected>>", lambda _e: callback())
239239

240+
def _create_styled_listbox(
241+
self,
242+
parent: tk.Widget,
243+
labels: list[str],
244+
on_select: Callable[[], None],
245+
*,
246+
height: int = 4,
247+
width: int = 12,
248+
) -> tk.Listbox:
249+
"""Create a themed multi-select Listbox for derivative/component selection."""
250+
bg: str = get_env_from_schema("UI_BUTTON_BG")
251+
fg: str = get_env_from_schema("UI_FOREGROUND")
252+
select_bg: str = get_env_from_schema("UI_BUTTON_FG")
253+
select_fg: str = get_contrast_foreground(select_bg)
254+
lb = tk.Listbox(
255+
parent,
256+
selectmode=tk.EXTENDED,
257+
height=min(len(labels), height),
258+
width=width,
259+
bg=bg,
260+
fg=fg,
261+
selectbackground=select_bg,
262+
selectforeground=select_fg,
263+
exportselection=False,
264+
)
265+
for lbl in labels:
266+
lb.insert(tk.END, lbl)
267+
lb.select_set(0)
268+
lb.pack(side=tk.LEFT, padx=4)
269+
lb.bind("<<ListboxSelect>>", lambda _e: on_select())
270+
return lb
271+
240272
# ------------------------------------------------------------------
241273
# Plot tab construction
242274
# ------------------------------------------------------------------
@@ -275,21 +307,9 @@ def _build_ode_scalar_tabs(self) -> None:
275307
self._sol_labels = generate_derivative_labels(notation)
276308
ttk.Label(ctrl, text="Show:").pack(side=tk.LEFT, padx=(0, 4))
277309

278-
btn_bg: str = get_env_from_schema("UI_BUTTON_BG")
279-
fg: str = get_env_from_schema("UI_FOREGROUND")
280-
select_bg: str = get_env_from_schema("UI_BUTTON_FG")
281-
select_fg: str = get_contrast_foreground(select_bg)
282-
283-
self._sol_listbox = tk.Listbox(
284-
ctrl, selectmode=tk.EXTENDED, height=min(len(self._sol_labels), 4),
285-
width=12, bg=btn_bg, fg=fg, selectbackground=select_bg,
286-
selectforeground=select_fg, exportselection=False,
310+
self._sol_listbox = self._create_styled_listbox(
311+
ctrl, self._sol_labels, self._update_solution_plot, height=4
287312
)
288-
for lbl in self._sol_labels:
289-
self._sol_listbox.insert(tk.END, lbl)
290-
self._sol_listbox.select_set(0)
291-
self._sol_listbox.pack(side=tk.LEFT, padx=4)
292-
self._sol_listbox.bind("<<ListboxSelect>>", lambda _e: self._update_solution_plot())
293313

294314
self._build_transform_controls(ctrl, self._update_solution_plot, "sol")
295315

@@ -582,23 +602,8 @@ def _build_vector_ode_tabs(self) -> None:
582602
self._vec_sol_labels = generate_derivative_labels(notation)
583603
ttk.Label(ctrl, text="Show:").pack(side=tk.LEFT, padx=(0, 4))
584604

585-
btn_bg: str = get_env_from_schema("UI_BUTTON_BG")
586-
fg: str = get_env_from_schema("UI_FOREGROUND")
587-
select_bg: str = get_env_from_schema("UI_BUTTON_FG")
588-
select_fg: str = get_contrast_foreground(select_bg)
589-
590-
self._vec_sol_listbox = tk.Listbox(
591-
ctrl, selectmode=tk.EXTENDED,
592-
height=min(len(self._vec_sol_labels), 6), width=12,
593-
bg=btn_bg, fg=fg, selectbackground=select_bg,
594-
selectforeground=select_fg, exportselection=False,
595-
)
596-
for lbl in self._vec_sol_labels:
597-
self._vec_sol_listbox.insert(tk.END, lbl)
598-
self._vec_sol_listbox.select_set(0)
599-
self._vec_sol_listbox.pack(side=tk.LEFT, padx=4)
600-
self._vec_sol_listbox.bind(
601-
"<<ListboxSelect>>", lambda _e: self._update_vec_solution_plot()
605+
self._vec_sol_listbox = self._create_styled_listbox(
606+
ctrl, self._vec_sol_labels, self._update_vec_solution_plot, height=6
602607
)
603608

604609
self._build_transform_controls(ctrl, self._update_vec_solution_plot, "vec_sol")

src/frontend/ui_dialogs/transform_dialog.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from frontend.ui_dialogs.keyboard_nav import setup_arrow_enter_navigation
2121
from frontend.ui_dialogs.scrollable_frame import ScrollableFrame
2222
from frontend.ui_dialogs.tooltip import ToolTip
23-
from frontend.window_utils import center_window, make_modal
23+
from frontend.window_utils import bind_wraplength, center_window, make_modal
2424
from transforms import (
2525
DisplayMode,
2626
TransformKind,
@@ -191,13 +191,8 @@ def _build_ui(self) -> None:
191191
unicode_text.config(state="disabled")
192192
unicode_text.pack(fill=tk.X, pady=(2, 4))
193193

194-
def _update_func_hint_wrap(_e: tk.Event | None = None) -> None: # type: ignore[type-arg]
195-
w = func_lf.winfo_width()
196-
if w > 100:
197-
func_hint_lbl.configure(wraplength=max(150, w - 2 * pad))
194+
bind_wraplength(func_lf, func_hint_lbl, pad=2 * pad, min_wrap=150)
198195

199-
func_lf.bind("<Configure>", _update_func_hint_wrap)
200-
self.win.after(50, lambda: _update_func_hint_wrap(None))
201196
self._func_entry = tk.Text(
202197
func_lf,
203198
height=2,
@@ -689,15 +684,7 @@ def _build_ui(self) -> None:
689684

690685
self._scroll.bind_new_children()
691686

692-
def _update_wraplength(_e: tk.Event | None = None) -> None: # type: ignore[type-arg]
693-
w = inner.winfo_width()
694-
if w > 100:
695-
wrap = max(200, w - 48)
696-
for lbl in self._body_labels:
697-
lbl.configure(wraplength=wrap)
698-
699-
inner.bind("<Configure>", _update_wraplength)
700-
self.win.after(50, lambda: _update_wraplength(None))
687+
bind_wraplength(inner, self._body_labels, pad=48, min_wrap=200)
701688

702689
def _add_section(
703690
self,

src/frontend/ui_main_menu.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from config import APP_NAME, APP_VERSION, get_env_from_schema
1010
from frontend.theme import configure_ttk_styles
1111
from frontend.ui_dialogs import ToolTip, setup_arrow_enter_navigation
12-
from frontend.window_utils import fit_and_center
12+
from frontend.window_utils import bind_wraplength, fit_and_center
1313
from utils import get_logger
1414

1515
logger = get_logger(__name__)
@@ -84,13 +84,7 @@ def _build_ui(self) -> None:
8484
)
8585
desc_lbl.pack(pady=(0, padding * 2))
8686

87-
def _update_desc_wrap(_e: tk.Event | None = None) -> None: # type: ignore[type-arg]
88-
w = main_frame.winfo_width()
89-
if w > 100:
90-
desc_lbl.configure(wraplength=max(200, w - 6 * padding))
91-
92-
main_frame.bind("<Configure>", _update_desc_wrap)
93-
self.root.after(50, lambda: _update_desc_wrap(None))
87+
bind_wraplength(main_frame, desc_lbl, pad=6 * padding, min_wrap=200)
9488

9589
# Buttons
9690
btn_frame = ttk.Frame(main_frame)

src/frontend/window_utils.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,58 @@ def make_modal(dialog: tk.Toplevel, parent: tk.Tk | tk.Toplevel) -> None:
113113

114114
def bind_wraplength(
115115
frame: tk.Widget,
116-
label: tk.Widget,
116+
label_or_labels: tk.Widget | list[tk.Widget],
117117
pad: int = 20,
118+
min_wrap: int = 200,
119+
debounce_ms: int = 50,
118120
) -> None:
119-
"""Bind a label's wraplength to the width of a frame.
121+
"""Bind label(s) wraplength to the width of a frame.
120122
121-
Automatically adjusts the label's wraplength when the frame is resized,
122-
ensuring text wraps nicely within the available space.
123+
Automatically adjusts wraplength when the frame is resized, ensuring text
124+
wraps nicely within the available space. Supports debouncing to avoid
125+
excessive updates during rapid resize.
123126
124127
Args:
125-
frame: The frame whose width determines the label's wraplength.
126-
label: The label widget to update.
128+
frame: The frame whose width determines the wraplength.
129+
label_or_labels: Single label widget or list of labels to update.
127130
pad: Padding in pixels to subtract from frame width.
131+
min_wrap: Minimum wraplength in pixels.
132+
debounce_ms: Debounce delay for Configure events (0 = no debounce).
128133
"""
129-
def _update(event=None) -> None:
134+
labels = (
135+
[label_or_labels]
136+
if isinstance(label_or_labels, tk.Widget)
137+
else list(label_or_labels)
138+
)
139+
140+
def _update(event: object | None = None) -> None:
130141
w = frame.winfo_width()
131142
if w > 100:
132-
label.configure(wraplength=w - pad)
143+
wrap = max(min_wrap, w - pad)
144+
for lbl in labels:
145+
if lbl.winfo_exists():
146+
lbl.configure(wraplength=wrap)
147+
148+
if debounce_ms > 0:
149+
_job: str | None = None
150+
151+
def _debounced(event: object | None = None) -> None:
152+
nonlocal _job
153+
if _job is not None:
154+
try:
155+
frame.after_cancel(_job)
156+
except tk.TclError:
157+
pass
158+
159+
def _run() -> None:
160+
nonlocal _job
161+
_job = None
162+
_update(event)
163+
164+
_job = frame.after(debounce_ms, _run)
165+
166+
frame.bind("<Configure>", _debounced)
167+
else:
168+
frame.bind("<Configure>", _update)
133169

134-
frame.bind("<Configure>", _update)
135170
frame.after(100, _update)

0 commit comments

Comments
 (0)