tintkit 0.1.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.
- tintkit/__init__.py +99 -0
- tintkit/composites.py +459 -0
- tintkit/containers.py +296 -0
- tintkit/controls.py +794 -0
- tintkit/icons/check.png +0 -0
- tintkit/icons/chevron-down.png +0 -0
- tintkit/icons/chevron-left.png +0 -0
- tintkit/icons/chevron-right.png +0 -0
- tintkit/icons/chevron-up.png +0 -0
- tintkit/icons/crop.png +0 -0
- tintkit/icons/droplets.png +0 -0
- tintkit/icons/folder-check.png +0 -0
- tintkit/icons/folder-open.png +0 -0
- tintkit/icons/info.png +0 -0
- tintkit/icons/layout-grid.png +0 -0
- tintkit/icons/menu.png +0 -0
- tintkit/icons/redo.png +0 -0
- tintkit/icons/rotate-ccw.png +0 -0
- tintkit/icons/save.png +0 -0
- tintkit/icons/search.png +0 -0
- tintkit/icons/settings.png +0 -0
- tintkit/icons/sliders-horizontal.png +0 -0
- tintkit/icons/star.png +0 -0
- tintkit/icons/trash-2.png +0 -0
- tintkit/icons/undo.png +0 -0
- tintkit/icons/upload.png +0 -0
- tintkit/icons/x.png +0 -0
- tintkit/icons.py +72 -0
- tintkit/primitives.py +387 -0
- tintkit/scaling.py +24 -0
- tintkit/theme.py +193 -0
- tintkit-0.1.0.dist-info/METADATA +124 -0
- tintkit-0.1.0.dist-info/RECORD +36 -0
- tintkit-0.1.0.dist-info/WHEEL +5 -0
- tintkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- tintkit-0.1.0.dist-info/top_level.txt +1 -0
tintkit/containers.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""TintKit — containers and surfaces.
|
|
2
|
+
|
|
3
|
+
Rounded cards, dialogs, callouts, the section header, the hero line, the drag
|
|
4
|
+
sashes and a themed scrollbar. These reuse the controls (a dialog footer is
|
|
5
|
+
real :class:`Button` widgets) and the themed plain widgets (:class:`Surface`,
|
|
6
|
+
:class:`Label`, :class:`IconLabel`) so the whole tree restyles live.
|
|
7
|
+
|
|
8
|
+
The rounded **Card** is the trick tk can't do with a plain Frame: a canvas
|
|
9
|
+
draws the rounded fill + hairline, and the content frame sits inset by ``pad``
|
|
10
|
+
(>= the corner radius) so its square corners never poke through the curve.
|
|
11
|
+
|
|
12
|
+
Geometry literals go through ``s()`` to scale to the screen DPI.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import tkinter as tk
|
|
16
|
+
import tkinter.ttk as ttk
|
|
17
|
+
|
|
18
|
+
from .scaling import s
|
|
19
|
+
from .theme import mix
|
|
20
|
+
from .primitives import (CanvasControl, Surface, Label, IconLabel,
|
|
21
|
+
rounded_rect, font)
|
|
22
|
+
from .controls import Button, TextField
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ----------------------------------------------------------------------------
|
|
26
|
+
# Card — a rounded, hairline surface; add content into ``.body``
|
|
27
|
+
# ----------------------------------------------------------------------------
|
|
28
|
+
class Card:
|
|
29
|
+
def __init__(self, parent, theme, pad=16, bg="panel", outer="bg",
|
|
30
|
+
radius=None, width=None):
|
|
31
|
+
self.theme = theme
|
|
32
|
+
self.pad = s(pad)
|
|
33
|
+
self._bg, self._outer, self._radius = bg, outer, radius
|
|
34
|
+
self.canvas = tk.Canvas(parent, highlightthickness=0, bg=theme[outer])
|
|
35
|
+
self._w = width or 1
|
|
36
|
+
if width:
|
|
37
|
+
self.canvas.configure(width=width)
|
|
38
|
+
self.body = tk.Frame(self.canvas, bg=theme[bg])
|
|
39
|
+
self._win = self.canvas.create_window(self.pad, self.pad,
|
|
40
|
+
window=self.body, anchor="nw")
|
|
41
|
+
if width: # fixed-width card: size the body now, as
|
|
42
|
+
self.canvas.itemconfigure( # <Configure> may never change the width
|
|
43
|
+
self._win, width=width - 2 * self.pad)
|
|
44
|
+
self.canvas.bind("<Configure>", self._on_canvas)
|
|
45
|
+
self.body.bind("<Configure>", self._on_body)
|
|
46
|
+
theme.subscribe(self._repaint)
|
|
47
|
+
self.canvas.bind("<Destroy>", self._destroyed)
|
|
48
|
+
self._repaint()
|
|
49
|
+
|
|
50
|
+
def _radius_px(self):
|
|
51
|
+
r = self._radius
|
|
52
|
+
if r is None:
|
|
53
|
+
return self.theme["r_card"]
|
|
54
|
+
return self.theme[r] if isinstance(r, str) else s(r)
|
|
55
|
+
|
|
56
|
+
def _on_canvas(self, e):
|
|
57
|
+
if e.width > 1 and e.width != self._w:
|
|
58
|
+
self._w = e.width
|
|
59
|
+
self.canvas.itemconfigure(self._win, width=self._w - 2 * self.pad)
|
|
60
|
+
self._repaint()
|
|
61
|
+
|
|
62
|
+
def _on_body(self, e):
|
|
63
|
+
h = e.height + 2 * self.pad
|
|
64
|
+
if int(self.canvas.cget("height")) != h:
|
|
65
|
+
self.canvas.configure(height=h)
|
|
66
|
+
self._repaint()
|
|
67
|
+
|
|
68
|
+
def _repaint(self):
|
|
69
|
+
try:
|
|
70
|
+
c, t = self.canvas, self.theme
|
|
71
|
+
c.configure(bg=t[self._outer])
|
|
72
|
+
self.body.configure(bg=t[self._bg])
|
|
73
|
+
c.delete("cardbg")
|
|
74
|
+
w, h = self._w, int(c.cget("height"))
|
|
75
|
+
rounded_rect(c, 0, 0, w - s(1), h - s(1), self._radius_px(),
|
|
76
|
+
fill=t[self._bg], outline=t["border"], width=s(1),
|
|
77
|
+
tags="cardbg")
|
|
78
|
+
c.tag_lower("cardbg")
|
|
79
|
+
except tk.TclError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def _destroyed(self, e):
|
|
83
|
+
if e.widget is self.canvas:
|
|
84
|
+
self.theme.unsubscribe(self._repaint)
|
|
85
|
+
|
|
86
|
+
def pack(self, **k):
|
|
87
|
+
self.canvas.pack(**k)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def grid(self, **k):
|
|
91
|
+
self.canvas.grid(**k)
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def place(self, **k):
|
|
95
|
+
self.canvas.place(**k)
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ----------------------------------------------------------------------------
|
|
100
|
+
# Section header — accent tick + title + divider rule
|
|
101
|
+
# ----------------------------------------------------------------------------
|
|
102
|
+
class SectionHeader(CanvasControl):
|
|
103
|
+
def __init__(self, parent, theme, title, bg="bg"):
|
|
104
|
+
self.title = title
|
|
105
|
+
super().__init__(parent, theme, s(200), s(34), bg=bg, cursor="")
|
|
106
|
+
self.canvas.bind("<Configure>", self._cfg)
|
|
107
|
+
|
|
108
|
+
def _interactive(self):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def _cfg(self, e):
|
|
112
|
+
if e.width > 1 and e.width != self.w:
|
|
113
|
+
self.w = e.width
|
|
114
|
+
self.repaint()
|
|
115
|
+
|
|
116
|
+
def draw(self):
|
|
117
|
+
c, t = self.canvas, self.theme
|
|
118
|
+
c.create_rectangle(0, s(6), s(3), s(30), fill=t["accent"], outline="")
|
|
119
|
+
c.create_text(s(14), s(18), text=self.title.upper(), anchor="w",
|
|
120
|
+
fill=t["fg"], font=font(12, True))
|
|
121
|
+
c.create_line(s(14), s(31), self.w - s(2), s(31), fill=t["divider"])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ----------------------------------------------------------------------------
|
|
125
|
+
# Hero line — the accent-bar dialog heading
|
|
126
|
+
# ----------------------------------------------------------------------------
|
|
127
|
+
def hero_line(parent, theme, title, bg="panel"):
|
|
128
|
+
row = Surface(parent, theme, bg=bg)
|
|
129
|
+
Surface(row.widget, theme, bg="accent", width=s(3)).pack(side="left",
|
|
130
|
+
fill="y")
|
|
131
|
+
Label(row.widget, theme, " " + title, fg="fg", bg=bg, size=14,
|
|
132
|
+
bold=True).pack(side="left")
|
|
133
|
+
return row
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ----------------------------------------------------------------------------
|
|
137
|
+
# Callout — note / tip / warning
|
|
138
|
+
# ----------------------------------------------------------------------------
|
|
139
|
+
_CALLOUTS = {
|
|
140
|
+
"info": ("fg_dim", "info", "Note", "fg"),
|
|
141
|
+
"tip": ("accent", "star", "Tip", "accent"),
|
|
142
|
+
"warn": ("warn", "info", "Warning", "warn"),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def callout(parent, theme, kind, text, title=None):
|
|
147
|
+
edge, icon, deflabel, lcol = _CALLOUTS[kind]
|
|
148
|
+
title = title or deflabel
|
|
149
|
+
card = Card(parent, theme, pad=10, bg="panel")
|
|
150
|
+
Surface(card.body, theme, bg=edge, width=s(3)).pack(side="left", fill="y")
|
|
151
|
+
pad = Surface(card.body, theme, bg="panel")
|
|
152
|
+
pad.widget.pack(side="left", fill="both", expand=True, padx=s(10), pady=s(2))
|
|
153
|
+
head = Surface(pad.widget, theme, bg="panel")
|
|
154
|
+
head.widget.pack(fill="x", anchor="w")
|
|
155
|
+
IconLabel(head.widget, theme, icon, 15, fg=edge, bg="panel").pack(
|
|
156
|
+
side="left", padx=(0, s(7)))
|
|
157
|
+
Label(head.widget, theme, title, fg=lcol, bg="panel", size=9,
|
|
158
|
+
bold=True).pack(side="left")
|
|
159
|
+
Label(pad.widget, theme, text, fg="fg_dim", bg="panel", size=9,
|
|
160
|
+
justify="left", wraplength=s(260)).pack(anchor="w", pady=(s(3), 0))
|
|
161
|
+
return card
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ----------------------------------------------------------------------------
|
|
165
|
+
# Dialog — rounded popup: hero + close + body + optional input + buttons
|
|
166
|
+
# ----------------------------------------------------------------------------
|
|
167
|
+
def dialog(parent, theme, title, body_text, buttons, with_input=None,
|
|
168
|
+
width=340, on_close=None):
|
|
169
|
+
cw = s(width)
|
|
170
|
+
card = Card(parent, theme, pad=20, bg="panel", width=cw)
|
|
171
|
+
b = card.body
|
|
172
|
+
top = Surface(b, theme, bg="panel")
|
|
173
|
+
top.widget.pack(fill="x")
|
|
174
|
+
hero_line(top.widget, theme, title).pack(side="left")
|
|
175
|
+
x = IconLabel(top.widget, theme, "x", 16, fg="fg_dim", bg="panel",
|
|
176
|
+
cursor="hand2")
|
|
177
|
+
x.widget.pack(side="right")
|
|
178
|
+
if on_close:
|
|
179
|
+
x.widget.bind("<Button-1>", lambda e: on_close())
|
|
180
|
+
Label(b, theme, body_text, fg="fg_dim", bg="panel", size=9, justify="left",
|
|
181
|
+
wraplength=cw - s(60)).pack(anchor="w", pady=(s(12), 0))
|
|
182
|
+
if with_input is not None:
|
|
183
|
+
TextField(b, theme, with_input, bg="panel").pack(fill="x",
|
|
184
|
+
pady=(s(12), 0))
|
|
185
|
+
foot = Surface(b, theme, bg="panel")
|
|
186
|
+
foot.widget.pack(fill="x", pady=(s(18), 0))
|
|
187
|
+
for spec in buttons:
|
|
188
|
+
Button(foot.widget, theme, bg="panel", **spec).pack(side="right",
|
|
189
|
+
padx=(s(8), 0))
|
|
190
|
+
return card
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ----------------------------------------------------------------------------
|
|
194
|
+
# Movable panels — drag sashes (look only; reused from the real app)
|
|
195
|
+
# ----------------------------------------------------------------------------
|
|
196
|
+
def theme_frame(theme, frame, **tokens):
|
|
197
|
+
"""Keep a plain ``tk.Frame``'s colours following the theme.
|
|
198
|
+
|
|
199
|
+
``tokens`` maps tk options to theme tokens, e.g.
|
|
200
|
+
``theme_frame(theme, f, bg="bg", highlightbackground="border")``. Without
|
|
201
|
+
this a frame built straight from ``theme[...]`` freezes on the palette it
|
|
202
|
+
was born in and turns dark on a light page after a live theme switch.
|
|
203
|
+
"""
|
|
204
|
+
def restyle():
|
|
205
|
+
if not frame.winfo_exists():
|
|
206
|
+
return
|
|
207
|
+
frame.configure(**{opt: theme[tok] for opt, tok in tokens.items()})
|
|
208
|
+
restyle()
|
|
209
|
+
theme.subscribe(restyle)
|
|
210
|
+
frame.bind("<Destroy>", lambda e: theme.unsubscribe(restyle))
|
|
211
|
+
return frame
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def v_sash(parent, theme):
|
|
215
|
+
"Vertical sidebar↔preview sash: bar + centred grip, accent on hover."
|
|
216
|
+
comp = Surface(parent, theme, bg="bg")
|
|
217
|
+
Surface(comp.widget, theme, bg="sidebar", width=s(120), height=s(90)).pack(
|
|
218
|
+
side="left")
|
|
219
|
+
sash = tk.Frame(comp.widget, bg=theme["bar"], width=s(8), height=s(90),
|
|
220
|
+
cursor="sb_h_double_arrow")
|
|
221
|
+
sash.pack(side="left")
|
|
222
|
+
sash.pack_propagate(False)
|
|
223
|
+
grip = tk.Frame(sash, bg=theme["fg_dim"])
|
|
224
|
+
grip.place(relx=0.5, rely=0.5, anchor="center", width=s(4), height=s(40))
|
|
225
|
+
preview = tk.Frame(comp.widget, bg=theme["bg"], width=s(120), height=s(90),
|
|
226
|
+
highlightthickness=s(1), highlightbackground=theme["border"])
|
|
227
|
+
preview.pack(side="left")
|
|
228
|
+
theme_frame(theme, preview, bg="bg", highlightbackground="border")
|
|
229
|
+
_hover_grip(theme, sash, grip, "bar")
|
|
230
|
+
return comp
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def h_sash(parent, theme):
|
|
234
|
+
"Horizontal folder-list divider: hairline + centred grip, accent on hover."
|
|
235
|
+
comp = Surface(parent, theme, bg="bg")
|
|
236
|
+
Surface(comp.widget, theme, bg="sidebar", width=s(200), height=s(44)).pack(
|
|
237
|
+
fill="x")
|
|
238
|
+
sash = tk.Frame(comp.widget, bg=theme["sidebar"], height=s(11), width=s(200),
|
|
239
|
+
cursor="sb_v_double_arrow")
|
|
240
|
+
sash.pack(fill="x")
|
|
241
|
+
sash.pack_propagate(False)
|
|
242
|
+
line = tk.Frame(sash, bg=theme["border"])
|
|
243
|
+
line.place(relx=0, rely=0.5, relwidth=1, height=s(1), anchor="w")
|
|
244
|
+
theme_frame(theme, line, bg="border")
|
|
245
|
+
grip = tk.Frame(sash, bg=theme["fg_dim"])
|
|
246
|
+
grip.place(relx=0.5, rely=0.5, anchor="center", width=s(40), height=s(4))
|
|
247
|
+
Surface(comp.widget, theme, bg="sidebar", width=s(200), height=s(44)).pack(
|
|
248
|
+
fill="x")
|
|
249
|
+
_hover_grip(theme, sash, grip, "sidebar")
|
|
250
|
+
return comp
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _hover_grip(theme, sash, grip, base_token):
|
|
254
|
+
def on(_e=None):
|
|
255
|
+
sash.configure(bg=theme["hover"])
|
|
256
|
+
grip.configure(bg=theme["accent"])
|
|
257
|
+
|
|
258
|
+
def off(_e=None):
|
|
259
|
+
if not sash.winfo_exists():
|
|
260
|
+
return
|
|
261
|
+
sash.configure(bg=theme[base_token])
|
|
262
|
+
grip.configure(bg=theme["fg_dim"])
|
|
263
|
+
for w in (sash, grip):
|
|
264
|
+
w.bind("<Enter>", on)
|
|
265
|
+
w.bind("<Leave>", off)
|
|
266
|
+
# follow live theme switches while the grip is idle (not hovered)
|
|
267
|
+
theme.subscribe(off)
|
|
268
|
+
sash.bind("<Destroy>", lambda e: theme.unsubscribe(off))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ----------------------------------------------------------------------------
|
|
272
|
+
# Themed scrollbar — slim, arrow-less; restyles on theme change
|
|
273
|
+
# ----------------------------------------------------------------------------
|
|
274
|
+
def themed_scrollbar(parent, theme, command, name="TintKit.Vertical.TScrollbar"):
|
|
275
|
+
style = ttk.Style()
|
|
276
|
+
try:
|
|
277
|
+
style.theme_use("clam")
|
|
278
|
+
except tk.TclError:
|
|
279
|
+
pass
|
|
280
|
+
style.layout(name, [
|
|
281
|
+
("Vertical.Scrollbar.trough", {"sticky": "ns", "children": [
|
|
282
|
+
("Vertical.Scrollbar.thumb", {"expand": "1", "sticky": "nswe"})]})])
|
|
283
|
+
|
|
284
|
+
def restyle():
|
|
285
|
+
style.configure(name, troughcolor=theme["sidebar"],
|
|
286
|
+
background=theme["border"], bordercolor=theme["sidebar"],
|
|
287
|
+
borderwidth=0, relief="flat", arrowcolor=theme["sidebar"],
|
|
288
|
+
width=s(10))
|
|
289
|
+
style.map(name, background=[
|
|
290
|
+
("active", mix(theme["border"], theme["fg"], 0.35)),
|
|
291
|
+
("pressed", mix(theme["border"], theme["fg"], 0.5))])
|
|
292
|
+
restyle()
|
|
293
|
+
theme.subscribe(restyle)
|
|
294
|
+
sb = ttk.Scrollbar(parent, orient="vertical", command=command, style=name)
|
|
295
|
+
sb.bind("<Destroy>", lambda e: theme.unsubscribe(restyle))
|
|
296
|
+
return sb
|