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/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