guile 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- guile/__init__.py +606 -0
- guile/_app.py +273 -0
- guile/_app_win32.py +892 -0
- guile/_nodes.py +114 -0
- guile/_template.py +585 -0
- guile/py.typed +0 -0
- guile/state.py +103 -0
- guile/ui.py +1694 -0
- guile-0.4.0.dist-info/METADATA +157 -0
- guile-0.4.0.dist-info/RECORD +12 -0
- guile-0.4.0.dist-info/WHEEL +5 -0
- guile-0.4.0.dist-info/top_level.txt +1 -0
guile/__init__.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""
|
|
2
|
+
guile — A lightweight Python desktop UI framework.
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
|
|
6
|
+
import guile as gui
|
|
7
|
+
|
|
8
|
+
count = gui.state(0)
|
|
9
|
+
|
|
10
|
+
@gui.app("Counter", width=400, height=300)
|
|
11
|
+
def ui():
|
|
12
|
+
with gui.col(align="center", justify="center", style="height:100vh"):
|
|
13
|
+
with gui.card(gap=14):
|
|
14
|
+
gui.title("Counter")
|
|
15
|
+
with gui.row(gap=16, align="center", justify="center"):
|
|
16
|
+
gui.button("−", variant="secondary",
|
|
17
|
+
on_click=lambda: count.update(lambda x: x - 1))
|
|
18
|
+
gui.text(count, size="2xl", bold=True,
|
|
19
|
+
style="min-width:64px;text-align:center")
|
|
20
|
+
gui.button("+",
|
|
21
|
+
on_click=lambda: count.update(lambda x: x + 1))
|
|
22
|
+
|
|
23
|
+
Five source files:
|
|
24
|
+
state.py — reactive State class
|
|
25
|
+
ui.py — render engine + all widget classes
|
|
26
|
+
_app.py — window lifecycle, pywebview bridge
|
|
27
|
+
_template.py — embedded HTML/CSS/JS page
|
|
28
|
+
__init__.py — this file: the public API surface (gui.*)
|
|
29
|
+
|
|
30
|
+
Everything the user ever calls lives in this file as a plain function.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
from typing import Any, Callable, Optional, Union
|
|
35
|
+
|
|
36
|
+
from .state import State
|
|
37
|
+
from .ui import (
|
|
38
|
+
# Layout
|
|
39
|
+
Column, Row, Card, Scroll,
|
|
40
|
+
# Display
|
|
41
|
+
_Text, _Title, _Badge, _Spacer, _Divider, _ProgressBar, _Html,
|
|
42
|
+
# Inputs
|
|
43
|
+
_Button, _Input, _NumberInput, _TextArea, _Checkbox, _Select, _MultiSelect, _Slider,
|
|
44
|
+
_DateInput, _DateTimeInput, _FilePicker, _Tabs,
|
|
45
|
+
# Media
|
|
46
|
+
_Figure, _Map, Marker,
|
|
47
|
+
# Data
|
|
48
|
+
_Table,
|
|
49
|
+
# Overlay
|
|
50
|
+
_Modal,
|
|
51
|
+
# Theme
|
|
52
|
+
_Theme, THEMES,
|
|
53
|
+
)
|
|
54
|
+
from ._app import _App
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── State ──────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def state(initial: Any, *, key: str = "") -> State:
|
|
60
|
+
"""
|
|
61
|
+
Create a reactive value. Setting .value re-renders the UI automatically.
|
|
62
|
+
|
|
63
|
+
count = gui.state(0)
|
|
64
|
+
items = gui.state([])
|
|
65
|
+
|
|
66
|
+
count.set(42)
|
|
67
|
+
count.update(lambda x: x + 1)
|
|
68
|
+
count.toggle() # bool shorthand
|
|
69
|
+
"""
|
|
70
|
+
return State(initial, key=key)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Layout ─────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def col(*, gap: int = 12, padding: Union[int, str] = 0,
|
|
76
|
+
align: str = "stretch", justify: str = "flex-start",
|
|
77
|
+
fill: bool = False, scroll: bool = False,
|
|
78
|
+
style: str = "", key: Optional[str] = None) -> Column:
|
|
79
|
+
"""Vertical stack. Use as `with gui.col():`"""
|
|
80
|
+
return Column(gap=gap, padding=padding, align=align, justify=justify,
|
|
81
|
+
fill=fill, scroll=scroll, style=style, key=key)
|
|
82
|
+
|
|
83
|
+
def row(*, gap: int = 8, padding: Union[int, str] = 0,
|
|
84
|
+
align: str = "center", justify: str = "flex-start",
|
|
85
|
+
fill: bool = False, wrap: bool = False,
|
|
86
|
+
style: str = "", key: Optional[str] = None) -> Row:
|
|
87
|
+
"""Horizontal stack. Use as `with gui.row():`"""
|
|
88
|
+
return Row(gap=gap, padding=padding, align=align, justify=justify,
|
|
89
|
+
fill=fill, wrap=wrap, style=style, key=key)
|
|
90
|
+
|
|
91
|
+
def card(*, gap: int = 12, padding: Union[int, str] = 20,
|
|
92
|
+
margin: Union[int, str] = 0,
|
|
93
|
+
style: str = "", key: Optional[str] = None) -> Card:
|
|
94
|
+
"""Raised surface. Use as `with gui.card():`. margin= adds outer spacing."""
|
|
95
|
+
return Card(gap=gap, padding=padding, margin=margin, style=style, key=key)
|
|
96
|
+
|
|
97
|
+
def scroll(*, max_height: Optional[int] = 400,
|
|
98
|
+
style: str = "", key: Optional[str] = None) -> Scroll:
|
|
99
|
+
"""
|
|
100
|
+
Scrollable container. Use as `with gui.scroll():`.
|
|
101
|
+
|
|
102
|
+
max_height= (default 400) sets the point at which the container starts
|
|
103
|
+
scrolling. Pass max_height=None to let the parent's height constrain it.
|
|
104
|
+
|
|
105
|
+
with gui.scroll(): # scrolls after 400px
|
|
106
|
+
gui.table(data)
|
|
107
|
+
|
|
108
|
+
with gui.scroll(max_height=600): # scrolls after 600px
|
|
109
|
+
gui.table(data)
|
|
110
|
+
|
|
111
|
+
with gui.scroll(max_height=None): # fills parent, parent must have fixed height
|
|
112
|
+
gui.table(data)
|
|
113
|
+
"""
|
|
114
|
+
return Scroll(max_height=max_height, style=style, key=key)
|
|
115
|
+
|
|
116
|
+
def spacer(h: Optional[int] = None, w_: Optional[int] = None,
|
|
117
|
+
fill: bool = False, key: Optional[str] = None) -> _Spacer:
|
|
118
|
+
"""Empty space. fill=True → flex:1 greedy spacer."""
|
|
119
|
+
return _Spacer(h, w_, fill, key)
|
|
120
|
+
|
|
121
|
+
def divider(key: Optional[str] = None) -> _Divider:
|
|
122
|
+
"""Horizontal separator line."""
|
|
123
|
+
return _Divider(key)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── Display ────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def title(content: Any, *, size: str = "xl", muted: bool = False,
|
|
129
|
+
style: str = "", key: Optional[str] = None) -> _Title:
|
|
130
|
+
"""Bold heading. size: xs | sm | md | lg | xl | 2xl | 3xl"""
|
|
131
|
+
return _Title(content, size=size, muted=muted, style=style, key=key)
|
|
132
|
+
|
|
133
|
+
def text(content: Any, *, size: str = "md", bold: bool = False,
|
|
134
|
+
italic: bool = False, muted: bool = False, underline: bool = False,
|
|
135
|
+
mono: bool = False, color: Optional[str] = None,
|
|
136
|
+
style: str = "", key: Optional[str] = None) -> _Text:
|
|
137
|
+
"""Inline or block text."""
|
|
138
|
+
return _Text(content, size=size, bold=bold, italic=italic, muted=muted,
|
|
139
|
+
underline=underline, mono=mono, color=color, style=style, key=key)
|
|
140
|
+
|
|
141
|
+
def badge(text_: Any, *, variant: str = "primary",
|
|
142
|
+
style: str = "", key: Optional[str] = None) -> _Badge:
|
|
143
|
+
"""Colored pill label. variant: primary | success | danger | warning | neutral"""
|
|
144
|
+
return _Badge(text_, variant=variant, style=style, key=key)
|
|
145
|
+
|
|
146
|
+
def progress(value: Any, *, max: int = 100, color: Optional[str] = None,
|
|
147
|
+
style: str = "", key: Optional[str] = None) -> _ProgressBar:
|
|
148
|
+
"""Horizontal progress bar. value goes from 0 to max."""
|
|
149
|
+
return _ProgressBar(value, max=max, color=color, style=style, key=key)
|
|
150
|
+
|
|
151
|
+
def html(raw: str, key: Optional[str] = None) -> _Html:
|
|
152
|
+
"""Raw HTML escape hatch. Use sparingly."""
|
|
153
|
+
return _Html(raw, key)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── Inputs ─────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def button(label: Any, *, on_click: Optional[Callable] = None,
|
|
159
|
+
variant: str = "primary", size: str = "md",
|
|
160
|
+
disabled: bool = False, style: str = "",
|
|
161
|
+
key: Optional[str] = None) -> _Button:
|
|
162
|
+
"""Button. variant: primary | secondary | ghost | danger. size: sm | md | lg"""
|
|
163
|
+
return _Button(label, on_click=on_click, variant=variant, size=size,
|
|
164
|
+
disabled=disabled, style=style, key=key)
|
|
165
|
+
|
|
166
|
+
def input(label: str = "", *, placeholder: str = "",
|
|
167
|
+
value: Optional[Union[str, State]] = None,
|
|
168
|
+
type: str = "text", disabled: bool = False,
|
|
169
|
+
on_change: Optional[Callable] = None,
|
|
170
|
+
style: str = "", key: Optional[str] = None) -> _Input:
|
|
171
|
+
"""Text input. Returns .value (str). Always provide key=."""
|
|
172
|
+
return _Input(label, placeholder=placeholder, value=value, type=type,
|
|
173
|
+
disabled=disabled, on_change=on_change, style=style, key=key)
|
|
174
|
+
|
|
175
|
+
def number_input(label: str = "", *,
|
|
176
|
+
value: Optional[Union[float, State]] = None,
|
|
177
|
+
min: Optional[float] = None,
|
|
178
|
+
max: Optional[float] = None,
|
|
179
|
+
step: float = 1.0,
|
|
180
|
+
unit: str = "",
|
|
181
|
+
disabled: bool = False,
|
|
182
|
+
on_change: Optional[Callable] = None,
|
|
183
|
+
style: str = "",
|
|
184
|
+
key: Optional[str] = None) -> _NumberInput:
|
|
185
|
+
"""
|
|
186
|
+
Numeric input. Returns .value (float) directly — no string conversion needed.
|
|
187
|
+
|
|
188
|
+
Follows the same value= convention as every other input widget.
|
|
189
|
+
Empty or invalid input silently falls back to the initial value.
|
|
190
|
+
|
|
191
|
+
Standalone — widget owns its state:
|
|
192
|
+
depth = gui.number_input("Root depth", value=1.0, step=0.1, unit="m")
|
|
193
|
+
gui.text(f"Depth: {depth.value} m")
|
|
194
|
+
|
|
195
|
+
Bound to an existing State:
|
|
196
|
+
Zr = gui.state(1.0)
|
|
197
|
+
gui.number_input("Root depth", value=Zr, step=0.1, unit="m",
|
|
198
|
+
on_change=Zr.set)
|
|
199
|
+
gui.text(f"Depth: {Zr.value} m")
|
|
200
|
+
|
|
201
|
+
Arguments:
|
|
202
|
+
label — label shown above the field
|
|
203
|
+
value — initial float, or a State[float] for two-way binding.
|
|
204
|
+
Defaults to 0.0 when omitted.
|
|
205
|
+
min — minimum value, enforced on every change
|
|
206
|
+
max — maximum value, enforced on every change
|
|
207
|
+
step — spinner arrow increment
|
|
208
|
+
unit — unit label shown to the right: "mm", "m³/m³", "days" …
|
|
209
|
+
disabled — read-only appearance
|
|
210
|
+
on_change — called with the new float on every valid change
|
|
211
|
+
"""
|
|
212
|
+
return _NumberInput(label, value=value,
|
|
213
|
+
min=min, max=max, step=step,
|
|
214
|
+
unit=unit, disabled=disabled, on_change=on_change,
|
|
215
|
+
style=style, key=key)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def textarea(label: str = "", *, placeholder: str = "",
|
|
219
|
+
value: Optional[Union[str, State]] = None,
|
|
220
|
+
rows: int = 4, disabled: bool = False,
|
|
221
|
+
on_change: Optional[Callable] = None,
|
|
222
|
+
style: str = "", key: Optional[str] = None) -> _TextArea:
|
|
223
|
+
"""Multi-line text input. Returns .value (str). Always provide key=."""
|
|
224
|
+
return _TextArea(label, placeholder=placeholder, value=value, rows=rows,
|
|
225
|
+
disabled=disabled, on_change=on_change, style=style, key=key)
|
|
226
|
+
|
|
227
|
+
def checkbox(label: str = "", *, value: Optional[Union[bool, State]] = None,
|
|
228
|
+
disabled: bool = False, on_change: Optional[Callable] = None,
|
|
229
|
+
key: Optional[str] = None) -> _Checkbox:
|
|
230
|
+
"""Boolean checkbox. Returns .value (bool). Always provide key=."""
|
|
231
|
+
return _Checkbox(label, value=value, disabled=disabled,
|
|
232
|
+
on_change=on_change, key=key)
|
|
233
|
+
|
|
234
|
+
def select(options: Any, label: str = "", *,
|
|
235
|
+
value: Optional[Union[str, State]] = None,
|
|
236
|
+
disabled: bool = False, on_change: Optional[Callable] = None,
|
|
237
|
+
style: str = "", key: Optional[str] = None) -> _Select:
|
|
238
|
+
"""Dropdown. options: list[str] | list[(val, label)] | dict. Returns .value (str)."""
|
|
239
|
+
return _Select(options, label, value=value, disabled=disabled,
|
|
240
|
+
on_change=on_change, style=style, key=key)
|
|
241
|
+
|
|
242
|
+
def multiselect(options: Any, label: str = "", *,
|
|
243
|
+
value: Optional[Union[list, State]] = None,
|
|
244
|
+
rows: int = 4,
|
|
245
|
+
disabled: bool = False,
|
|
246
|
+
on_change: Optional[Callable] = None,
|
|
247
|
+
style: str = "",
|
|
248
|
+
key: Optional[str] = None) -> _MultiSelect:
|
|
249
|
+
"""
|
|
250
|
+
Multi-select dropdown. Returns .value (list[str]), .set(), .update().
|
|
251
|
+
|
|
252
|
+
The user holds Ctrl / Cmd to select multiple items.
|
|
253
|
+
|
|
254
|
+
crops = gui.multiselect(
|
|
255
|
+
["Maize", "Wheat", "Soybean", "Cotton"],
|
|
256
|
+
"Crop types", value=["Maize"], key="crops"
|
|
257
|
+
)
|
|
258
|
+
gui.text(f"Selected: {', '.join(crops.value)}")
|
|
259
|
+
|
|
260
|
+
Arguments:
|
|
261
|
+
options — list[str] | list[(value, label)] | dict
|
|
262
|
+
label — label shown above the list
|
|
263
|
+
value — initial selection as a list of value strings, or a State[list]
|
|
264
|
+
rows — number of visible rows (default 4)
|
|
265
|
+
disabled — read-only appearance
|
|
266
|
+
on_change — called with the new list[str] on every change
|
|
267
|
+
"""
|
|
268
|
+
return _MultiSelect(options, label, value=value, rows=rows,
|
|
269
|
+
disabled=disabled, on_change=on_change,
|
|
270
|
+
style=style, key=key)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def slider(label: str = "", *, min: float = 0, max: float = 100,
|
|
274
|
+
step: float = 1, value: Optional[Union[float, State]] = None,
|
|
275
|
+
on_change: Optional[Callable] = None,
|
|
276
|
+
style: str = "", key: Optional[str] = None) -> _Slider:
|
|
277
|
+
"""Range slider. Returns .value (float). Always provide key=."""
|
|
278
|
+
return _Slider(label, min=min, max=max, step=step, value=value,
|
|
279
|
+
on_change=on_change, style=style, key=key)
|
|
280
|
+
|
|
281
|
+
def date_input(label: str = "", *, value: Optional[Union[str, State]] = None,
|
|
282
|
+
disabled: bool = False, on_change: Optional[Callable] = None,
|
|
283
|
+
style: str = "", key: Optional[str] = None) -> _DateInput:
|
|
284
|
+
"""Native date picker. Returns .value (str) as YYYY-MM-DD. Always provide key=."""
|
|
285
|
+
return _DateInput(label, value=value, disabled=disabled,
|
|
286
|
+
on_change=on_change, style=style, key=key)
|
|
287
|
+
|
|
288
|
+
def datetime_input(label: str = "", *, value: Optional[Union[str, State]] = None,
|
|
289
|
+
disabled: bool = False, on_change: Optional[Callable] = None,
|
|
290
|
+
style: str = "", key: Optional[str] = None) -> _DateTimeInput:
|
|
291
|
+
"""
|
|
292
|
+
Native datetime picker. Returns .value (str) as YYYY-MM-DDTHH:MM.
|
|
293
|
+
|
|
294
|
+
Uses the browser's native datetime-local input — no external library needed.
|
|
295
|
+
To parse the value in Python:
|
|
296
|
+
from datetime import datetime
|
|
297
|
+
dt = datetime.fromisoformat(widget.value) # e.g. 2024-06-15T09:30
|
|
298
|
+
|
|
299
|
+
To pre-fill with a specific datetime:
|
|
300
|
+
gui.datetime_input("Start", value="2024-06-15T09:30", key="start")
|
|
301
|
+
"""
|
|
302
|
+
return _DateTimeInput(label, value=value, disabled=disabled,
|
|
303
|
+
on_change=on_change, style=style, key=key)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def file_picker(label: str = "Choose file…", *,
|
|
307
|
+
value: Optional[Union[str, State]] = None,
|
|
308
|
+
file_types: tuple = (), save: bool = False,
|
|
309
|
+
disabled: bool = False,
|
|
310
|
+
on_change: Optional[Callable] = None,
|
|
311
|
+
style: str = "", key: Optional[str] = None) -> _FilePicker:
|
|
312
|
+
"""OS native file dialog button. Returns .value (str) with the selected path.
|
|
313
|
+
on_change is called with the selected path string after the dialog closes.
|
|
314
|
+
"""
|
|
315
|
+
return _FilePicker(label, value=value, file_types=file_types,
|
|
316
|
+
save=save, disabled=disabled,
|
|
317
|
+
on_change=on_change, style=style, key=key)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def tabs(labels: list, *, value: Optional[Union[str, State]] = None,
|
|
321
|
+
on_change: Optional[Callable] = None,
|
|
322
|
+
style: str = "", key: Optional[str] = None) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Tab strip. Returns the active tab label as a plain string.
|
|
325
|
+
Manages its own internal state — no gui.state() declaration needed.
|
|
326
|
+
Always provide key= so the active tab survives re-renders.
|
|
327
|
+
|
|
328
|
+
Basic usage:
|
|
329
|
+
|
|
330
|
+
tab = gui.tabs(["Overview", "Data", "Info"], key="main")
|
|
331
|
+
|
|
332
|
+
if tab == "Overview":
|
|
333
|
+
gui.text("Summary content")
|
|
334
|
+
elif tab == "Data":
|
|
335
|
+
gui.table(records)
|
|
336
|
+
elif tab == "Info":
|
|
337
|
+
gui.text("About this app")
|
|
338
|
+
|
|
339
|
+
Programmatic switching — bind to an external State so a callback
|
|
340
|
+
can change the active tab:
|
|
341
|
+
|
|
342
|
+
active = gui.state("Overview")
|
|
343
|
+
gui.tabs(["Overview", "Data"], value=active,
|
|
344
|
+
on_change=active.set, key="main")
|
|
345
|
+
|
|
346
|
+
def after_load(path):
|
|
347
|
+
records.set(load(path))
|
|
348
|
+
active.set("Data") # jump to Data tab on load
|
|
349
|
+
"""
|
|
350
|
+
return _Tabs(labels, value=value, on_change=on_change,
|
|
351
|
+
style=style, key=key).value
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ── Data ───────────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
def table(data: Any, *, columns: Optional[list] = None,
|
|
357
|
+
max_rows: int = 2000,
|
|
358
|
+
style: str = "", key: Optional[str] = None) -> _Table:
|
|
359
|
+
"""
|
|
360
|
+
Data table. Accepts common Python data structures directly:
|
|
361
|
+
|
|
362
|
+
gui.table(df) # pandas DataFrame
|
|
363
|
+
gui.table(arr) # numpy 2-D array
|
|
364
|
+
gui.table(records) # list of dicts (native)
|
|
365
|
+
gui.table(rows) # list of lists
|
|
366
|
+
|
|
367
|
+
columns= selects/reorders which keys to show.
|
|
368
|
+
max_rows= caps rendering (default 2000) with a notice row when clipped.
|
|
369
|
+
"""
|
|
370
|
+
return _Table(data, columns=columns, max_rows=max_rows,
|
|
371
|
+
style=style, key=key)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ── Media ───────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
def figure(fig, *, dpi: int = 96, width: str = "100%",
|
|
377
|
+
caption: Optional[str] = None, transparent: bool = True,
|
|
378
|
+
static: bool = False, style: str = "",
|
|
379
|
+
key: Optional[str] = None) -> _Figure:
|
|
380
|
+
"""Embed a matplotlib Figure as an inline PNG."""
|
|
381
|
+
return _Figure(fig, dpi=dpi, width=width, caption=caption,
|
|
382
|
+
transparent=transparent, static=static, style=style, key=key)
|
|
383
|
+
|
|
384
|
+
def leaflet(center: tuple = (0.0, 0.0), *, zoom: int = 10,
|
|
385
|
+
height: int = 380, markers: Optional[list] = None,
|
|
386
|
+
on_click: Optional[Callable] = None,
|
|
387
|
+
on_move: Optional[Callable] = None,
|
|
388
|
+
on_shape: Optional[Callable] = None,
|
|
389
|
+
draw: Any = False,
|
|
390
|
+
style: str = "", key: Optional[str] = None) -> _Map:
|
|
391
|
+
"""
|
|
392
|
+
Embed an interactive Leaflet map (OpenStreetMap tiles). Requires internet.
|
|
393
|
+
|
|
394
|
+
Callbacks:
|
|
395
|
+
on_click(lat, lon) — fires when the user clicks the map background
|
|
396
|
+
on_move(center, zoom) — fires after pan/zoom ends;
|
|
397
|
+
center=(lat, lon) tuple, zoom=int
|
|
398
|
+
on_shape(type, coords) — fires when a shape is drawn (requires draw=);
|
|
399
|
+
type: "rectangle"|"polygon"|"polyline"|
|
|
400
|
+
"circle"|"marker"
|
|
401
|
+
coords: [[lat,lon], ...] for polygon/rect/polyline;
|
|
402
|
+
{"lat","lng","radius"} for circle;
|
|
403
|
+
{"lat","lng"} for marker
|
|
404
|
+
|
|
405
|
+
Draw tools:
|
|
406
|
+
draw=["rectangle","polygon"] — show specific drawing tools
|
|
407
|
+
draw=True — show all tools (rectangle, polygon,
|
|
408
|
+
polyline, circle, marker)
|
|
409
|
+
draw=False — no tools (default)
|
|
410
|
+
|
|
411
|
+
Per-marker callbacks:
|
|
412
|
+
gui.Marker((lat, lon), on_click=fn)
|
|
413
|
+
|
|
414
|
+
Always supply key= when using any callback so the element ID is stable
|
|
415
|
+
across renders — callback cids are derived from that ID.
|
|
416
|
+
"""
|
|
417
|
+
if _App._current:
|
|
418
|
+
_App._current._use_leaflet = True
|
|
419
|
+
if draw:
|
|
420
|
+
_App._current._use_leaflet_draw = True
|
|
421
|
+
return _Map(center=center, zoom=zoom, height=height,
|
|
422
|
+
markers=markers, on_click=on_click, on_move=on_move,
|
|
423
|
+
on_shape=on_shape, draw=draw, style=style, key=key)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ── Notify (imperative toast — bypasses the render cycle) ────────────────────
|
|
428
|
+
|
|
429
|
+
def notify(message: str, *,
|
|
430
|
+
variant: str = "success",
|
|
431
|
+
duration: float = 3.0) -> None:
|
|
432
|
+
"""
|
|
433
|
+
Show a temporary notification toast by injecting it directly into the
|
|
434
|
+
browser DOM. No state variable or re-render required.
|
|
435
|
+
|
|
436
|
+
Call from callbacks only — not from inside ui().
|
|
437
|
+
The window must be open before notify() is called.
|
|
438
|
+
|
|
439
|
+
def save(path):
|
|
440
|
+
df.value.to_csv(path, index=False)
|
|
441
|
+
gui.notify("File saved!", variant="success")
|
|
442
|
+
|
|
443
|
+
def delete():
|
|
444
|
+
rows.set([])
|
|
445
|
+
gui.notify("All rows cleared.", variant="warning", duration=5)
|
|
446
|
+
|
|
447
|
+
Variants: "success", "danger", "warning", "primary", "neutral"
|
|
448
|
+
duration: seconds before auto-dismiss (default 3).
|
|
449
|
+
"""
|
|
450
|
+
from ._app import _App
|
|
451
|
+
import html as _h
|
|
452
|
+
app = _App._current
|
|
453
|
+
if not app or not app._window:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
COLOURS = {
|
|
457
|
+
"success": ("#16a34a", "#dcfce7"),
|
|
458
|
+
"danger": ("#dc2626", "#fee2e2"),
|
|
459
|
+
"warning": ("#d97706", "#fef3c7"),
|
|
460
|
+
"primary": ("#6366f1", "#ede9fe"),
|
|
461
|
+
"neutral": ("#6b7280", "#f3f4f6"),
|
|
462
|
+
}
|
|
463
|
+
fg, bg = COLOURS.get(variant, COLOURS["primary"])
|
|
464
|
+
msg = _h.escape(str(message)).replace("'", "\'")
|
|
465
|
+
ms = int(duration * 1000)
|
|
466
|
+
|
|
467
|
+
close_style = (
|
|
468
|
+
"background:none;border:none;cursor:pointer;"
|
|
469
|
+
f"font-size:16px;line-height:1;color:{fg};"
|
|
470
|
+
"padding:0;margin-left:8px;opacity:.7"
|
|
471
|
+
)
|
|
472
|
+
close_btn = (
|
|
473
|
+
'<button onclick="this.parentNode.remove()"'
|
|
474
|
+
f' style="{close_style}">✕</button>'
|
|
475
|
+
)
|
|
476
|
+
inner = f"<span>{msg}</span>{close_btn}"
|
|
477
|
+
card_css = f"background:{bg};color:{fg};border:1.5px solid {fg};"
|
|
478
|
+
|
|
479
|
+
# Build JS as a single concatenated string — no join(), no list,
|
|
480
|
+
# so semicolons land correctly inside the function body.
|
|
481
|
+
js = (
|
|
482
|
+
"(function(){"
|
|
483
|
+
"var e=document.createElement('div');"
|
|
484
|
+
"e.className='guile-notify';"
|
|
485
|
+
f"e.style.cssText={repr(card_css)};"
|
|
486
|
+
f"e.innerHTML={repr(inner)};"
|
|
487
|
+
"document.body.appendChild(e);"
|
|
488
|
+
f"setTimeout(function(){{if(e.parentNode)e.remove();}},{ms});"
|
|
489
|
+
"})();"
|
|
490
|
+
)
|
|
491
|
+
app._window.evaluate_js(js)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ── Overlays ────────────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
def modal(title: str = "", *,
|
|
497
|
+
visible: bool = True,
|
|
498
|
+
on_close: Optional[Callable] = None,
|
|
499
|
+
width: int = 420,
|
|
500
|
+
style: str = "",
|
|
501
|
+
key: Optional[str] = None) -> _Modal:
|
|
502
|
+
"""
|
|
503
|
+
Blocking modal dialog. Use as a context manager.
|
|
504
|
+
|
|
505
|
+
When visible=False the modal renders nothing (no overhead).
|
|
506
|
+
Always supply on_close= so the backdrop and ✕ button work.
|
|
507
|
+
|
|
508
|
+
confirm = gui.state(False)
|
|
509
|
+
|
|
510
|
+
def request_delete():
|
|
511
|
+
confirm.set(True)
|
|
512
|
+
|
|
513
|
+
def do_delete():
|
|
514
|
+
# perform deletion
|
|
515
|
+
confirm.set(False)
|
|
516
|
+
|
|
517
|
+
@gui.app("My App")
|
|
518
|
+
def ui():
|
|
519
|
+
gui.button("Delete", on_click=request_delete)
|
|
520
|
+
|
|
521
|
+
with gui.modal("Confirm delete",
|
|
522
|
+
visible=confirm.value,
|
|
523
|
+
on_close=lambda: confirm.set(False)):
|
|
524
|
+
gui.text("This cannot be undone.")
|
|
525
|
+
with gui.row(gap=8, justify="flex-end"):
|
|
526
|
+
gui.button("Cancel", variant="ghost",
|
|
527
|
+
on_click=lambda: confirm.set(False))
|
|
528
|
+
gui.button("Delete", variant="danger",
|
|
529
|
+
on_click=do_delete)
|
|
530
|
+
"""
|
|
531
|
+
return _Modal(title, visible=visible, on_close=on_close,
|
|
532
|
+
width=width, style=style, key=key)
|
|
533
|
+
|
|
534
|
+
# ── Theme ──────────────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
def theme(
|
|
537
|
+
preset: Optional[str] = None,
|
|
538
|
+
*,
|
|
539
|
+
primary: Optional[str] = None,
|
|
540
|
+
bg: Optional[str] = None,
|
|
541
|
+
surface: Optional[str] = None,
|
|
542
|
+
surface_2: Optional[str] = None,
|
|
543
|
+
text: Optional[str] = None,
|
|
544
|
+
text_2: Optional[str] = None,
|
|
545
|
+
border: Optional[str] = None,
|
|
546
|
+
radius: Optional[int] = None,
|
|
547
|
+
key: Optional[str] = None,
|
|
548
|
+
) -> _Theme:
|
|
549
|
+
"""
|
|
550
|
+
Apply a colour theme to the entire app.
|
|
551
|
+
|
|
552
|
+
Call this as the FIRST thing inside your ui() function so it takes
|
|
553
|
+
effect before any widgets are rendered.
|
|
554
|
+
|
|
555
|
+
Built-in presets (8 values each, all others derived automatically):
|
|
556
|
+
"light" — indigo on light grey (default)
|
|
557
|
+
"dark" — indigo on near-black
|
|
558
|
+
"neon" — cyan on deep navy
|
|
559
|
+
"rose" — red on warm white
|
|
560
|
+
"forest" — green on soft green
|
|
561
|
+
"slate" — grey on off-white
|
|
562
|
+
|
|
563
|
+
Any argument overrides just that one value in the preset:
|
|
564
|
+
gui.theme("dark", primary="#f43f5e") # dark theme, rose accent
|
|
565
|
+
gui.theme("light", radius=2) # light theme, sharp corners
|
|
566
|
+
|
|
567
|
+
Arguments:
|
|
568
|
+
preset — name of a built-in theme
|
|
569
|
+
primary — accent colour for buttons, sliders, focus rings (#hex)
|
|
570
|
+
bg — page/window background (#hex)
|
|
571
|
+
surface — card and input background (#hex)
|
|
572
|
+
surface_2 — secondary surface, hover rows (#hex)
|
|
573
|
+
text — primary text colour (#hex)
|
|
574
|
+
text_2 — secondary / muted text colour (#hex)
|
|
575
|
+
border — border and separator colour (#hex)
|
|
576
|
+
radius — base border radius for cards and inputs (int, px)
|
|
577
|
+
|
|
578
|
+
All other colours (hover, tints, shadows, danger/success/warning) are
|
|
579
|
+
derived automatically from these 8 values using HLS colour math.
|
|
580
|
+
|
|
581
|
+
To see all built-in preset values:
|
|
582
|
+
import guile; print(guile.THEMES)
|
|
583
|
+
"""
|
|
584
|
+
return _Theme(preset=preset, primary=primary, bg=bg,
|
|
585
|
+
surface=surface, surface_2=surface_2,
|
|
586
|
+
text=text, text_2=text_2,
|
|
587
|
+
border=border, radius=radius, key=key)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ── App decorator ───────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
def app(title_: str = "Guile App", *, width: int = 800, height: int = 600,
|
|
593
|
+
resizable: bool = False, debug: bool = False):
|
|
594
|
+
"""
|
|
595
|
+
Decorator that turns a ui() function into a runnable desktop app.
|
|
596
|
+
|
|
597
|
+
@gui.app("My App", width=480, height=400)
|
|
598
|
+
def ui():
|
|
599
|
+
with gui.card():
|
|
600
|
+
gui.title("Hello, world")
|
|
601
|
+
"""
|
|
602
|
+
def decorator(fn: Callable):
|
|
603
|
+
_App(title_, width=width, height=height,
|
|
604
|
+
resizable=resizable, debug=debug).run(fn)
|
|
605
|
+
return fn
|
|
606
|
+
return decorator
|