codefig 0.17.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.
- codefig/__init__.py +210 -0
- codefig/_validate.py +12 -0
- codefig/autowrap.py +277 -0
- codefig/charts.py +1806 -0
- codefig/code.py +602 -0
- codefig/compose.py +374 -0
- codefig/constraints.py +1571 -0
- codefig/dim.py +214 -0
- codefig/edge_align.py +231 -0
- codefig/edges.py +2824 -0
- codefig/export.py +204 -0
- codefig/figure.py +1923 -0
- codefig/fills.py +146 -0
- codefig/flows.py +102 -0
- codefig/glyphs.py +395 -0
- codefig/icon.py +288 -0
- codefig/icons/LICENSE-tabler-icons.txt +21 -0
- codefig/icons/__init__.py +101 -0
- codefig/icons/tabler.json +1 -0
- codefig/legend.py +483 -0
- codefig/lint.py +1737 -0
- codefig/lint_geom.py +528 -0
- codefig/node.py +864 -0
- codefig/notices.py +34 -0
- codefig/overlaps.py +392 -0
- codefig/policy.py +137 -0
- codefig/primitives.py +2721 -0
- codefig/py.typed +0 -0
- codefig/regions.py +727 -0
- codefig/render_svg.py +512 -0
- codefig/routing.py +932 -0
- codefig/shapes.py +642 -0
- codefig/solids.py +514 -0
- codefig/steps.py +203 -0
- codefig/styles.py +893 -0
- codefig/textutil.py +889 -0
- codefig/timeline.py +1288 -0
- codefig/tree.py +277 -0
- codefig/venues.py +93 -0
- codefig-0.17.0.dist-info/METADATA +542 -0
- codefig-0.17.0.dist-info/RECORD +44 -0
- codefig-0.17.0.dist-info/WHEEL +5 -0
- codefig-0.17.0.dist-info/licenses/LICENSE +21 -0
- codefig-0.17.0.dist-info/top_level.txt +1 -0
codefig/compose.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Repetition idioms (v0.4.0): Stacked decks, "× N" repeats, and "0 … N-1" series.
|
|
2
|
+
|
|
3
|
+
``Stacked(node, n=3)`` — the node drawn as the front card of a deck (instances /
|
|
4
|
+
replicas / multi-head), back cards offset up-right by default.
|
|
5
|
+
``Repeat(node, n=12)`` — the node framed (or bracketed) with a "× N" annotation,
|
|
6
|
+
Transformer style.
|
|
7
|
+
``Series(a, b, c)`` — items in a row / column with an ellipsis before the last one,
|
|
8
|
+
for "Worker 0, Worker 1, …, Worker N-1".
|
|
9
|
+
|
|
10
|
+
All three are ordinary nodes (flex layout, constraints, ports); none takes coordinates.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, List, Optional, Sequence, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from .node import Container, Node, Row
|
|
18
|
+
from .primitives import Ellipsis, Text
|
|
19
|
+
from .styles import themed
|
|
20
|
+
from .textutil import measure_text
|
|
21
|
+
|
|
22
|
+
_OFFSETS = {"up-right": (1, -1), "down-right": (1, 1), "up-left": (-1, -1), "down-left": (-1, 1),
|
|
23
|
+
"right": (1, 0), "down": (0, 1), "up": (0, -1), "left": (-1, 0)}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Stacked(Node):
|
|
27
|
+
"""A deck of ``n`` cards: ``node`` in front, ``n - 1`` copies peeking out behind.
|
|
28
|
+
|
|
29
|
+
offset: px per card (number, or ``(dx, dy)``); direction: up-right | down-right |
|
|
30
|
+
up-left | down-left | right | down | …. copies: "blank" (default — the card's frame
|
|
31
|
+
and fill only, like replicated instances), "full" (each copy redrawn, contents and
|
|
32
|
+
all), or "outline" (frame with no fill). ``back_fill`` overrides the copies' fill.
|
|
33
|
+
``deck.port(side)`` lands on the FRONT card (v0.7.0) — the card the reader sees —
|
|
34
|
+
so ``Edge(deck, x)`` / ``deck.port("right")`` start at the front card's outline, not
|
|
35
|
+
at the edge of the peeking back cards; ``deck.deck_port(side)`` is the old
|
|
36
|
+
whole-stack bounding-box port. ``back_tint`` (default 0.25 per card, ``0`` = off)
|
|
37
|
+
lightens each card further back so the front card reads as the object and the
|
|
38
|
+
copies as depth; ``back_stroke_width`` defaults to the thin theme stroke.
|
|
39
|
+
Role: inherits the front card's (hard for Box / shapes, background for Group).
|
|
40
|
+
``same_width(deck, x)`` / ``same_height`` size the front card (v0.13.0).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, node: Node, n: int = 3, *,
|
|
44
|
+
offset: Union[float, Tuple[float, float], None] = None,
|
|
45
|
+
direction: str = "up-right", copies: str = "blank",
|
|
46
|
+
back_fill: Optional[Any] = None, back_stroke: Optional[str] = None,
|
|
47
|
+
back_tint: float = 0.25, back_stroke_width: Optional[float] = None,
|
|
48
|
+
**kwargs: Any) -> None:
|
|
49
|
+
if n < 1:
|
|
50
|
+
raise ValueError("Stacked needs n >= 1")
|
|
51
|
+
if copies not in ("blank", "full", "outline"):
|
|
52
|
+
raise ValueError("copies must be blank|full|outline")
|
|
53
|
+
if direction not in _OFFSETS:
|
|
54
|
+
raise ValueError(f"direction must be one of {tuple(_OFFSETS)}")
|
|
55
|
+
self._role_explicit = None
|
|
56
|
+
super().__init__(**kwargs)
|
|
57
|
+
self.node = node
|
|
58
|
+
node.parent = self
|
|
59
|
+
self.n = int(n)
|
|
60
|
+
self._offset = offset
|
|
61
|
+
self._direction = direction
|
|
62
|
+
self.copies = copies
|
|
63
|
+
self.back_fill = back_fill
|
|
64
|
+
self.back_stroke = back_stroke
|
|
65
|
+
self.back_tint = float(back_tint)
|
|
66
|
+
self.back_stroke_width = back_stroke_width
|
|
67
|
+
|
|
68
|
+
def port(self, side: str = "right", **kw: Any):
|
|
69
|
+
"""Port in line with the FRONT card; same arguments as :meth:`Node.port`.
|
|
70
|
+
|
|
71
|
+
On the sides the back cards do not peek out of, it is the front card's own
|
|
72
|
+
outline. On the sides they do (``right`` / ``top`` for the default up-right
|
|
73
|
+
deck) it sits on the outermost card, level with the front card's port, so an
|
|
74
|
+
edge attaches to the deck without crossing the copies.
|
|
75
|
+
"""
|
|
76
|
+
if side in ("left", "right", "top", "bottom") and set(kw) <= {"at"}:
|
|
77
|
+
from .edges import Port
|
|
78
|
+
|
|
79
|
+
return Port(self, kind="side", side=side, at=float(kw.get("at", 0.5)))
|
|
80
|
+
return self.node.port(side, **kw)
|
|
81
|
+
|
|
82
|
+
def resolve_port_side(self, side: str, at: float = 0.5):
|
|
83
|
+
"""(x, y, direction) of :meth:`port` — used by :class:`~codefig.edges.Port`."""
|
|
84
|
+
fg = self.node.geom
|
|
85
|
+
k = self.n - 1
|
|
86
|
+
at = max(0.0, min(1.0, at))
|
|
87
|
+
ex, ey = self.dx * k, self.dy * k # rearmost card offset from the front card
|
|
88
|
+
if side == "right":
|
|
89
|
+
return fg.right + max(0.0, ex), fg.y + fg.h * at, "right"
|
|
90
|
+
if side == "left":
|
|
91
|
+
return fg.x + min(0.0, ex), fg.y + fg.h * at, "left"
|
|
92
|
+
if side == "top":
|
|
93
|
+
return fg.x + fg.w * at, fg.y + min(0.0, ey), "up"
|
|
94
|
+
return fg.x + fg.w * at, fg.bottom + max(0.0, ey), "down"
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def _size_proxy(self) -> Node:
|
|
98
|
+
"""same_width / same_height size the front card (v0.13.0)."""
|
|
99
|
+
return self.node
|
|
100
|
+
|
|
101
|
+
def deck_port(self, side: str = "right", **kw: Any):
|
|
102
|
+
"""Port on the whole deck's bounding box (front + peeking back cards)."""
|
|
103
|
+
return Node.port(self, side, **kw)
|
|
104
|
+
|
|
105
|
+
def _offset_xy(self) -> Tuple[float, float]:
|
|
106
|
+
off = self._offset
|
|
107
|
+
if off is None: # theme length: 7 px classic, ≈ 3.9 pt paper
|
|
108
|
+
off = self.theme.L(7.0)
|
|
109
|
+
if isinstance(off, (int, float)):
|
|
110
|
+
sx, sy = _OFFSETS[self._direction]
|
|
111
|
+
return sx * float(off), sy * float(off)
|
|
112
|
+
return float(off[0]), float(off[1])
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def dx(self) -> float:
|
|
116
|
+
"""Per-card x offset (from ``offset`` / ``direction``; default a theme length)."""
|
|
117
|
+
return self._offset_xy()[0]
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def dy(self) -> float:
|
|
121
|
+
"""Per-card y offset."""
|
|
122
|
+
return self._offset_xy()[1]
|
|
123
|
+
|
|
124
|
+
# role follows the front card unless set explicitly
|
|
125
|
+
@property
|
|
126
|
+
def obstacle(self) -> Optional[str]:
|
|
127
|
+
if self._role_explicit is not None:
|
|
128
|
+
return self._role_explicit
|
|
129
|
+
from .policy import role_of
|
|
130
|
+
|
|
131
|
+
return role_of(self.node) if hasattr(self, "node") else None
|
|
132
|
+
|
|
133
|
+
@obstacle.setter
|
|
134
|
+
def obstacle(self, v: Optional[str]) -> None:
|
|
135
|
+
self._role_explicit = v
|
|
136
|
+
|
|
137
|
+
def children(self) -> Sequence[Node]:
|
|
138
|
+
return [self.node]
|
|
139
|
+
|
|
140
|
+
def invalidate(self) -> None:
|
|
141
|
+
super().invalidate()
|
|
142
|
+
if hasattr(self, "node"):
|
|
143
|
+
self.node.invalidate()
|
|
144
|
+
|
|
145
|
+
def _measure(self) -> Tuple[float, float]:
|
|
146
|
+
w, h = self.node.measure()
|
|
147
|
+
k = self.n - 1
|
|
148
|
+
return w + abs(self.dx) * k, h + abs(self.dy) * k
|
|
149
|
+
|
|
150
|
+
def _layout_children(self) -> None:
|
|
151
|
+
g = self.geom
|
|
152
|
+
cw, ch = self.node.measure()
|
|
153
|
+
k = self.n - 1
|
|
154
|
+
x = g.x + (abs(self.dx) * k if self.dx < 0 else 0.0)
|
|
155
|
+
y = g.y + (abs(self.dy) * k if self.dy < 0 else 0.0)
|
|
156
|
+
self.node.layout(x, y, min(cw, g.w - abs(self.dx) * k) if g.w > 0 else cw,
|
|
157
|
+
min(ch, g.h - abs(self.dy) * k) if g.h > 0 else ch)
|
|
158
|
+
|
|
159
|
+
def _draw_card(self, buf: List[str], x: float, y: float, w: float, h: float,
|
|
160
|
+
depth: int = 1) -> None:
|
|
161
|
+
from .render_svg import draw_rect
|
|
162
|
+
from .styles import tint
|
|
163
|
+
|
|
164
|
+
nd = self.node
|
|
165
|
+
fill = self.back_fill if self.back_fill is not None else getattr(nd, "fill", None)
|
|
166
|
+
if self.copies == "outline":
|
|
167
|
+
fill = "none"
|
|
168
|
+
if fill is None:
|
|
169
|
+
fill = "#ffffff"
|
|
170
|
+
if (self.back_tint > 0 and isinstance(fill, str) and fill.startswith("#")
|
|
171
|
+
and len(fill) in (4, 7)):
|
|
172
|
+
fill = tint(fill, min(0.85, self.back_tint * depth))
|
|
173
|
+
stroke = self.back_stroke or getattr(nd, "stroke", None) or self.theme.stroke
|
|
174
|
+
sw = (self.back_stroke_width if self.back_stroke_width is not None
|
|
175
|
+
else (getattr(nd, "stroke_width", None) or self.theme.stroke_width))
|
|
176
|
+
if hasattr(nd, "_draw_frame") and hasattr(nd, "_path"): # a Shape: same outline
|
|
177
|
+
from .render_svg import draw_path
|
|
178
|
+
|
|
179
|
+
draw_path(buf, nd._path(x, y, w, h), fill=fill, stroke=stroke, stroke_width=sw)
|
|
180
|
+
if hasattr(nd, "_decorate"):
|
|
181
|
+
nd._decorate(buf, x, y, w, h)
|
|
182
|
+
return
|
|
183
|
+
r = 0.0
|
|
184
|
+
if getattr(nd, "rounded", False):
|
|
185
|
+
r = getattr(nd, "radius", None) or self.theme.corner_radius
|
|
186
|
+
draw_rect(buf, x, y, w, h, fill=fill, stroke=stroke, stroke_width=sw, radius=r,
|
|
187
|
+
dasharray=nd._dash(sw) if hasattr(nd, "_dash") else None)
|
|
188
|
+
|
|
189
|
+
def draw(self, buf: List[str], ox: float = 0.0, oy: float = 0.0) -> None:
|
|
190
|
+
ng = self.node.geom
|
|
191
|
+
for k in range(self.n - 1, 0, -1):
|
|
192
|
+
if self.copies == "full":
|
|
193
|
+
self.node.draw(buf, ox + self.dx * k, oy + self.dy * k)
|
|
194
|
+
else:
|
|
195
|
+
self._draw_card(buf, ng.x + ox + self.dx * k, ng.y + oy + self.dy * k, ng.w, ng.h,
|
|
196
|
+
depth=k)
|
|
197
|
+
self.node.draw(buf, ox, oy)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Repeat(Node):
|
|
201
|
+
"""``node`` repeated N times, shown once with an "× N" mark (Transformer "N×").
|
|
202
|
+
|
|
203
|
+
style: "frame" (rounded dashed frame around the node, label beside it), "bracket"
|
|
204
|
+
(a square bracket on ``side`` with the label next to it) or "label" (just the text).
|
|
205
|
+
side: right | left | top | bottom. label: default "× {n}" (pass e.g. "N×" or "L layers").
|
|
206
|
+
The label is a real soft Text child (knockout / overlap aware); the frame is
|
|
207
|
+
background (edges may cross it, like a Group).
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, node: Node, n: Any = "N", *, label: Optional[str] = None,
|
|
211
|
+
style: str = "frame", side: str = "right", gap: Optional[float] = None,
|
|
212
|
+
pad: Optional[float] = None, color: str = "#555555",
|
|
213
|
+
font_size: Optional[float] = None, border: Any = "dashed",
|
|
214
|
+
radius: Optional[float] = None, fill: Optional[Any] = None,
|
|
215
|
+
weight: str = "bold", **kwargs: Any) -> None:
|
|
216
|
+
if style not in ("frame", "bracket", "label"):
|
|
217
|
+
raise ValueError("style must be frame|bracket|label")
|
|
218
|
+
if side not in ("right", "left", "top", "bottom"):
|
|
219
|
+
raise ValueError("side must be right|left|top|bottom")
|
|
220
|
+
super().__init__(border=border if style == "frame" else None, **kwargs)
|
|
221
|
+
self.node = node
|
|
222
|
+
node.parent = self
|
|
223
|
+
self.n = n
|
|
224
|
+
self.style = style
|
|
225
|
+
self.side = side
|
|
226
|
+
self.gap = gap
|
|
227
|
+
self._pad = pad
|
|
228
|
+
self.color = color
|
|
229
|
+
self.radius = radius
|
|
230
|
+
self.fill = fill
|
|
231
|
+
txt = label if label is not None else f"× {n}"
|
|
232
|
+
self.label = Text(txt, font_size=font_size, weight=weight, color=color,
|
|
233
|
+
theme=self._theme)
|
|
234
|
+
self.label.parent = self
|
|
235
|
+
|
|
236
|
+
gap = themed(6.0)
|
|
237
|
+
radius = themed(6.0)
|
|
238
|
+
frame_width = themed(1.2, "SW")
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def pad(self) -> float:
|
|
242
|
+
"""Frame padding around the node (theme length; 0 unless ``style="frame"``)."""
|
|
243
|
+
if self.style != "frame":
|
|
244
|
+
return 0.0
|
|
245
|
+
return float(self._pad) if self._pad is not None else self.theme.L(6.0)
|
|
246
|
+
|
|
247
|
+
def children(self) -> Sequence[Node]:
|
|
248
|
+
return [self.node, self.label]
|
|
249
|
+
|
|
250
|
+
def invalidate(self) -> None:
|
|
251
|
+
super().invalidate()
|
|
252
|
+
if hasattr(self, "node"):
|
|
253
|
+
self.node.invalidate()
|
|
254
|
+
self.label.invalidate()
|
|
255
|
+
|
|
256
|
+
def _bracket_w(self) -> float:
|
|
257
|
+
return self.theme.L(7.0) if self.style == "bracket" else 0.0
|
|
258
|
+
|
|
259
|
+
def _measure(self) -> Tuple[float, float]:
|
|
260
|
+
cw, ch = self.node.measure()
|
|
261
|
+
lw, lh = self.label.measure()
|
|
262
|
+
fw, fh = cw + 2 * self.pad, ch + 2 * self.pad
|
|
263
|
+
bw = self._bracket_w()
|
|
264
|
+
if self.side in ("left", "right"):
|
|
265
|
+
ext = bw + (self.gap if bw else 0) + self.gap + lw
|
|
266
|
+
return fw + ext, max(fh, lh)
|
|
267
|
+
ext = bw + (self.gap if bw else 0) + self.gap + lh
|
|
268
|
+
return max(fw, lw), fh + ext
|
|
269
|
+
|
|
270
|
+
def _frame_rect(self) -> Tuple[float, float, float, float]:
|
|
271
|
+
g = self.geom
|
|
272
|
+
cw, ch = self.node.measure()
|
|
273
|
+
fw, fh = cw + 2 * self.pad, ch + 2 * self.pad
|
|
274
|
+
lw, lh = self.label.measure()
|
|
275
|
+
ext_h = self._bracket_w() + (self.gap if self._bracket_w() else 0) + self.gap
|
|
276
|
+
if self.side == "right":
|
|
277
|
+
return g.x, g.y + (g.h - fh) / 2, fw, fh
|
|
278
|
+
if self.side == "left":
|
|
279
|
+
return g.x + ext_h + lw, g.y + (g.h - fh) / 2, fw, fh
|
|
280
|
+
if self.side == "bottom":
|
|
281
|
+
return g.x + (g.w - fw) / 2, g.y, fw, fh
|
|
282
|
+
return g.x + (g.w - fw) / 2, g.y + ext_h + lh, fw, fh
|
|
283
|
+
|
|
284
|
+
def _layout_children(self) -> None:
|
|
285
|
+
self._follow = False
|
|
286
|
+
fx, fy, fw, fh = self._frame_rect()
|
|
287
|
+
cw, ch = self.node.measure()
|
|
288
|
+
self.node.layout(fx + self.pad, fy + self.pad, cw, ch)
|
|
289
|
+
self._place_label(fx, fy, fw, fh)
|
|
290
|
+
|
|
291
|
+
def _after_solve(self) -> None:
|
|
292
|
+
"""Constraints moved the repeated node: frame and label follow it."""
|
|
293
|
+
g = self.node.geom
|
|
294
|
+
fr = (g.x - self.pad, g.y - self.pad, g.w + 2 * self.pad, g.h + 2 * self.pad)
|
|
295
|
+
if max(abs(a - b) for a, b in zip(fr, self._frame_rect())) < 1e-6:
|
|
296
|
+
return
|
|
297
|
+
self._follow = True
|
|
298
|
+
self._place_label(*fr)
|
|
299
|
+
|
|
300
|
+
def _drawn_frame(self) -> Tuple[float, float, float, float]:
|
|
301
|
+
if not getattr(self, "_follow", False):
|
|
302
|
+
return self._frame_rect()
|
|
303
|
+
g = self.node.geom # later whole-subtree translations move this too
|
|
304
|
+
return g.x - self.pad, g.y - self.pad, g.w + 2 * self.pad, g.h + 2 * self.pad
|
|
305
|
+
|
|
306
|
+
def _place_label(self, fx: float, fy: float, fw: float, fh: float) -> None:
|
|
307
|
+
lw, lh = self.label.measure()
|
|
308
|
+
bw = self._bracket_w()
|
|
309
|
+
step = bw + (self.gap if bw else 0) + self.gap
|
|
310
|
+
if self.side == "right":
|
|
311
|
+
self.label.layout(fx + fw + step, fy + (fh - lh) / 2, lw, lh)
|
|
312
|
+
elif self.side == "left":
|
|
313
|
+
self.label.layout(fx - step - lw, fy + (fh - lh) / 2, lw, lh)
|
|
314
|
+
elif self.side == "bottom":
|
|
315
|
+
self.label.layout(fx + (fw - lw) / 2, fy + fh + step, lw, lh)
|
|
316
|
+
else:
|
|
317
|
+
self.label.layout(fx + (fw - lw) / 2, fy - step - lh, lw, lh)
|
|
318
|
+
|
|
319
|
+
def draw(self, buf: List[str], ox: float = 0.0, oy: float = 0.0) -> None:
|
|
320
|
+
from .render_svg import draw_path, draw_rect
|
|
321
|
+
|
|
322
|
+
fx, fy, fw, fh = self._drawn_frame()
|
|
323
|
+
fx, fy = fx + ox, fy + oy
|
|
324
|
+
if self.style == "frame":
|
|
325
|
+
draw_rect(buf, fx, fy, fw, fh, fill=self.fill if self.fill is not None else "none",
|
|
326
|
+
stroke=self.color, stroke_width=self.frame_width, radius=self.radius,
|
|
327
|
+
dasharray=self._dash(self.frame_width))
|
|
328
|
+
elif self.style == "bracket":
|
|
329
|
+
b, t = self._bracket_w(), self.theme.L(3.0)
|
|
330
|
+
if self.side in ("right", "left"):
|
|
331
|
+
x0 = fx + fw + self.gap if self.side == "right" else fx - self.gap
|
|
332
|
+
s = 1 if self.side == "right" else -1
|
|
333
|
+
d = (f"M {x0:.2f},{fy + t:.2f} L {x0 + s * b:.2f},{fy + t:.2f} "
|
|
334
|
+
f"L {x0 + s * b:.2f},{fy + fh - t:.2f} L {x0:.2f},{fy + fh - t:.2f}")
|
|
335
|
+
else:
|
|
336
|
+
y0 = fy + fh + self.gap if self.side == "bottom" else fy - self.gap
|
|
337
|
+
s = 1 if self.side == "bottom" else -1
|
|
338
|
+
d = (f"M {fx + t:.2f},{y0:.2f} L {fx + t:.2f},{y0 + s * b:.2f} "
|
|
339
|
+
f"L {fx + fw - t:.2f},{y0 + s * b:.2f} L {fx + fw - t:.2f},{y0:.2f}")
|
|
340
|
+
draw_path(buf, d, stroke=self.color, stroke_width=self.theme.SW(1.3))
|
|
341
|
+
self.node.draw(buf, ox, oy)
|
|
342
|
+
self.label.draw(buf, ox, oy)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class Series(Row):
|
|
346
|
+
"""Items with an ellipsis before the last: ``Series(w0, w1, wN)`` → w0 w1 … wN.
|
|
347
|
+
|
|
348
|
+
direction: "row" (default, "⋯") or "column" ("⋮"). ``ellipsis=False`` drops the
|
|
349
|
+
mark; ``at`` = index the ellipsis is inserted before (default: last item).
|
|
350
|
+
Takes every Row / Col keyword (gap, align, …). Layout-only role, like Row.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, *items: Any, ellipsis: bool = True, at: Optional[int] = None,
|
|
354
|
+
direction: str = "row", ellipsis_text: Optional[str] = None,
|
|
355
|
+
ellipsis_size: Optional[float] = None, ellipsis_color: str = "#444444",
|
|
356
|
+
**kwargs: Any) -> None:
|
|
357
|
+
if len(items) == 1 and isinstance(items[0], (list, tuple)):
|
|
358
|
+
items = tuple(items[0])
|
|
359
|
+
if direction not in ("row", "column", "col"):
|
|
360
|
+
raise ValueError("direction must be row|column")
|
|
361
|
+
kids: List[Node] = list(items)
|
|
362
|
+
self.ellipsis_node: Optional[Node] = None
|
|
363
|
+
if ellipsis and len(kids) >= 2:
|
|
364
|
+
vertical = direction != "row"
|
|
365
|
+
txt = ellipsis_text or ("⋮" if vertical else "⋯")
|
|
366
|
+
self.ellipsis_node = Ellipsis(txt, font_size=ellipsis_size or 16, color=ellipsis_color)
|
|
367
|
+
i = len(kids) - 1 if at is None else at
|
|
368
|
+
kids.insert(i, self.ellipsis_node)
|
|
369
|
+
kwargs.setdefault("align", "center")
|
|
370
|
+
kwargs["direction"] = "row" if direction == "row" else "column"
|
|
371
|
+
super().__init__(kids, **kwargs)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
__all__ = ["Repeat", "Series", "Stacked"]
|