-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathui.py
More file actions
executable file
·284 lines (251 loc) · 9.72 KB
/
Copy pathui.py
File metadata and controls
executable file
·284 lines (251 loc) · 9.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
from dataclasses import dataclass
import threading
from typing import TYPE_CHECKING
from PIL import Image
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetype
from loguru import logger
from settings import (
AbstractSetting,
ButtonMenuSetting,
FloatSetting,
GroupSetting,
IntSetting,
StringOptionSetting,
get_visible_menu_items,
)
from state import UIState
if TYPE_CHECKING:
from main import ScannerGui
@dataclass
class FontConfig:
toolbar_font_name: str
toolbar_font_size: int
regular_font_name: str
regular_font_size: int
@dataclass
class DisplayInfo:
width: int
height: int
@dataclass
class UiParams:
toolbar_height: int
target_width: int
target_height: int
visible_settings: int
@dataclass
class ConnectionData:
udc_connected: bool
bt_connected: bool = False
class UserInterface:
def __init__(
self, gui: "ScannerGui", font_config: FontConfig, display_info: DisplayInfo
):
self.app = gui
self.font_config = font_config
self.display = display_info
self.tb_font = truetype(
self.font_config.toolbar_font_name, self.font_config.toolbar_font_size
)
self.reg_font = truetype(
self.font_config.regular_font_name, self.font_config.regular_font_size
)
self.settings_stack: list[GroupSetting] = []
self.settings_index = 0
self.active_setting: AbstractSetting | None = None
self.image_lock = threading.Lock()
def visible_settings(self, settings: list[AbstractSetting]):
if not self.settings_stack:
return settings
return [self.make_exit_setting()] + self.settings_stack[-1].children
def make_exit_setting(self):
return StringOptionSetting(
id="exit",
name="← Exit",
default_value="",
value="",
apply_callback=lambda v: None,
options=[""],
)
def draw(
self,
img: Image.Image,
settings: list[AbstractSetting],
connection: ConnectionData,
ui_params: UiParams,
state: UIState,
settings_lock: threading.Lock,
) -> Image.Image:
# Create a copy of the image
draw = Draw(img)
# draw toolbar
draw.rectangle(
(0, 0, self.display.width, ui_params.toolbar_height), fill="black"
)
# Center toolbar text vertically
state_text = f"State: {state.value}"
text_bbox = self.tb_font.getbbox(state_text)
text_height = text_bbox[3] - text_bbox[1]
y = (ui_params.toolbar_height - text_height) // 2
draw.text((10, y), state_text, font=self.tb_font, fill="white")
# draw right-aligned connection status
conn = next((s for s in settings if s.id == "connection"), None)
if conn and conn.value == "USB":
conn_text = f"Conn: {'OK' if connection.udc_connected else 'NO'}"
text_bbox = self.tb_font.getbbox(conn_text)
draw.text(
(
self.display.width - text_bbox[2] - 10,
(ui_params.toolbar_height - text_bbox[3] + text_bbox[1]) // 2,
),
conn_text,
font=self.tb_font,
fill="green" if connection.udc_connected else "red",
)
elif conn and conn.value == "BT":
conn_text = f"BT: {'OK' if connection.bt_connected else 'NO'}"
text_bbox = self.tb_font.getbbox(conn_text)
draw.text(
(
self.display.width - text_bbox[2] - 10,
(ui_params.toolbar_height - text_bbox[3] + text_bbox[1]) // 2,
),
conn_text,
font=self.tb_font,
fill="green" if connection.bt_connected else "red",
)
if state in [
UIState.IDLE,
UIState.SCAN,
UIState.TARGET_ADJUST_W,
UIState.TARGET_ADJUST_H,
]:
# Draw centered target rectangle
x0 = (self.display.width - ui_params.target_width) // 2
y0 = (
ui_params.toolbar_height
+ (
(self.display.height - ui_params.toolbar_height)
- ui_params.target_height
)
// 2
)
x1 = x0 + ui_params.target_width
y1 = y0 + ui_params.target_height
match state:
case UIState.IDLE:
color = "red"
case UIState.SCAN:
color = "blue"
case _:
color = "yellow"
draw.rectangle((x0, y0, x1, y1), outline=color, width=3)
# draw crosshair
draw.line(
(
x0 + ui_params.target_width // 2,
y0,
x0 + ui_params.target_width // 2,
y1,
),
fill=color,
width=1,
)
draw.line(
(
x0,
y0 + ui_params.target_height // 2,
x1,
y0 + ui_params.target_height // 2,
),
fill=color,
width=1,
)
elif state == UIState.SETTINGS:
visible_count = ui_params.visible_settings
with settings_lock:
visible_settings = get_visible_menu_items(
self.visible_settings(settings),
self.settings_index,
visible_count=visible_count,
)
total = len(self.visible_settings(settings))
# Dynamically calculate overlay area based on visible_count
min_overlay_height = self.display.height // 2.8
max_overlay_height = int(self.display.height * 0.6)
overlay_height = max(
min_overlay_height,
min(
max_overlay_height,
int(self.display.height * 0.12 * visible_count),
),
)
overlay_top = self.display.height - overlay_height
overlay_bottom = self.display.height
overlay = Image.new(
"RGBA", (self.display.width, self.display.height), (0, 0, 0, 0)
)
draw_overlay = Draw(overlay)
draw_overlay.rectangle(
(0, overlay_top, self.display.width, overlay_bottom),
fill=(0, 0, 0, 200),
)
# Calculate item height and spacing
item_height = overlay_height // visible_count
vertical_padding = max(8, item_height // 8)
text_y_offset = vertical_padding
# Draw setting text
for draw_index, (i, setting) in enumerate(visible_settings):
color = "white"
if i == self.settings_index:
if setting == self.active_setting:
color = "cyan"
else:
color = "yellow"
if setting.id == "exit":
state_text = setting.name
elif isinstance(setting, GroupSetting):
state_text = f"{setting.name}"
elif isinstance(setting, FloatSetting):
state_text = f"{setting.name}: {round(setting.value, setting.precision)}{setting.suffix}"
elif isinstance(setting, IntSetting):
state_text = f"{setting.name}: {setting.value}{setting.suffix}"
elif isinstance(setting, StringOptionSetting):
state_text = f"{setting.name}: {setting.value}"
elif isinstance(setting, ButtonMenuSetting):
state_text = f"<{setting.name}>"
else:
logger.error(f"Unrecognized setting, {setting}")
state_text = "ERROR"
y_pos = overlay_top + (text_y_offset // 2) + draw_index * item_height
draw_overlay.text(
(16, y_pos),
state_text,
font=self.reg_font,
fill=color,
)
# Draw scroll indicator if needed
if total > visible_count:
scrollbar_left = self.display.width - 18
scrollbar_right = self.display.width - 8
scrollbar_top = overlay_top + 8
scrollbar_bottom = overlay_bottom - 8
scrollbar_height = scrollbar_bottom - scrollbar_top
track_height = scrollbar_height
thumb_height = max(10, int(track_height * (visible_count / total)))
max_scroll = total - visible_count
scroll_pos = min(
max(self.settings_index - (visible_count // 2), 0),
max_scroll,
)
scroll_ratio = scroll_pos / max_scroll if max_scroll > 0 else 0
thumb_top = scrollbar_top + int(
(track_height - thumb_height) * scroll_ratio
)
thumb_bottom = thumb_top + thumb_height
draw_overlay.rectangle(
(scrollbar_left, thumb_top, scrollbar_right, thumb_bottom),
fill="gray",
)
img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
return img