slikui-python 0.1.1__cp310-cp310-win32.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.
- slikui/__init__.py +115 -0
- slikui/_py/__init__.py +0 -0
- slikui/_py/app.py +112 -0
- slikui/_py/builder.py +37 -0
- slikui/_py/events.py +103 -0
- slikui/_py/pixels.py +17 -0
- slikui/_py/renderer.py +329 -0
- slikui/_slikui.cp310-win32.pyd +0 -0
- slikui/py.typed +0 -0
- slikui_python-0.1.1.dist-info/METADATA +112 -0
- slikui_python-0.1.1.dist-info/RECORD +14 -0
- slikui_python-0.1.1.dist-info/WHEEL +5 -0
- slikui_python-0.1.1.dist-info/licenses/LICENSE +21 -0
- slikui_python-0.1.1.dist-info/licenses/third_party/fonts/LICENSE-DejaVu.txt +187 -0
slikui/__init__.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""slikui — Python bindings for the slikui C++ UI library.
|
|
2
|
+
|
|
3
|
+
Distribution `slikui`, import package `slikui`, compiled module `slikui._slikui`.
|
|
4
|
+
Layering: `_slikui` binds the C++ API 1:1; `slikui/_py` adds a thin Pythonic
|
|
5
|
+
surface (kwargs → setters, App shell, pixel helpers). See
|
|
6
|
+
docs/python-bindings-plan.md.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from slikui._slikui import ( # noqa: F401 — re-exported
|
|
10
|
+
Accordion,
|
|
11
|
+
Button,
|
|
12
|
+
ButtonVariant,
|
|
13
|
+
Checkbox,
|
|
14
|
+
Color,
|
|
15
|
+
Column,
|
|
16
|
+
Container,
|
|
17
|
+
Context,
|
|
18
|
+
CrossMode,
|
|
19
|
+
DrawCmd,
|
|
20
|
+
DrawCmdType,
|
|
21
|
+
DrawList,
|
|
22
|
+
Divider,
|
|
23
|
+
Dropdown,
|
|
24
|
+
Expander,
|
|
25
|
+
IRenderSink,
|
|
26
|
+
Image,
|
|
27
|
+
ImageFit,
|
|
28
|
+
Label,
|
|
29
|
+
ListView,
|
|
30
|
+
Panel,
|
|
31
|
+
ProgressBar,
|
|
32
|
+
Radio,
|
|
33
|
+
RenderSink,
|
|
34
|
+
Row,
|
|
35
|
+
ScrollArea,
|
|
36
|
+
Sink,
|
|
37
|
+
Slider,
|
|
38
|
+
Spacer,
|
|
39
|
+
Spinner,
|
|
40
|
+
Swatch,
|
|
41
|
+
Switch,
|
|
42
|
+
Tabs,
|
|
43
|
+
Target,
|
|
44
|
+
TextArea,
|
|
45
|
+
TexFormat,
|
|
46
|
+
TextField,
|
|
47
|
+
Theme,
|
|
48
|
+
ThemePreset,
|
|
49
|
+
Time,
|
|
50
|
+
Vec2,
|
|
51
|
+
Widget,
|
|
52
|
+
apply_preset,
|
|
53
|
+
find_system_font,
|
|
54
|
+
theme_presets,
|
|
55
|
+
use_theme,
|
|
56
|
+
)
|
|
57
|
+
from slikui._py import builder
|
|
58
|
+
from slikui._py.app import App
|
|
59
|
+
from slikui._py.renderer import CanvasSink
|
|
60
|
+
from slikui._py.events import (
|
|
61
|
+
focus_lost,
|
|
62
|
+
frame_resize,
|
|
63
|
+
key_press,
|
|
64
|
+
key_release,
|
|
65
|
+
mods,
|
|
66
|
+
mouse_click,
|
|
67
|
+
mouse_move,
|
|
68
|
+
mouse_press,
|
|
69
|
+
mouse_release,
|
|
70
|
+
scroll,
|
|
71
|
+
text_input,
|
|
72
|
+
text_input_str,
|
|
73
|
+
)
|
|
74
|
+
from slikui._py.pixels import to_rgba
|
|
75
|
+
from slikui._slikui import ( # noqa: F401 — input enums
|
|
76
|
+
EventType,
|
|
77
|
+
Key,
|
|
78
|
+
Mod,
|
|
79
|
+
MouseButton,
|
|
80
|
+
set_leak_warnings,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
from slikui._slikui import __version__ # from include/slikui/version.hpp
|
|
84
|
+
|
|
85
|
+
# Pythonic sugar on the bound classes: `container.add(slikui.Label, "hi")` and
|
|
86
|
+
# `ctx.set_root(slikui.Column)`. nanobind classes are heap types, so type-level
|
|
87
|
+
# assignment is allowed; if a future build forbids it, use the module-level
|
|
88
|
+
# helpers `slikui.add(container, ...)` / `slikui.set_root(context, ...)`.
|
|
89
|
+
_PATCHED = builder.try_patch()
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"Accordion", "App", "Button", "ButtonVariant", "Checkbox", "Color",
|
|
93
|
+
"Column", "Container", "Context", "CrossMode", "Divider", "Dropdown",
|
|
94
|
+
"Expander", "Image", "ImageFit", "Label", "ListView", "Panel",
|
|
95
|
+
"ProgressBar", "Radio", "Row", "ScrollArea", "Sink", "Slider",
|
|
96
|
+
"Spacer", "Spinner", "Swatch", "Switch", "Tabs", "Target", "TextArea",
|
|
97
|
+
"TextField", "Theme", "ThemePreset", "Time", "Vec2", "Widget",
|
|
98
|
+
"__version__", "add", "apply_preset", "find_system_font",
|
|
99
|
+
"set_root", "theme_presets", "to_rgba", "use_theme",
|
|
100
|
+
# events
|
|
101
|
+
"EventType", "Key", "Mod", "MouseButton",
|
|
102
|
+
"focus_lost", "frame_resize", "key_press", "key_release", "mods",
|
|
103
|
+
"mouse_click", "mouse_move", "mouse_press", "mouse_release",
|
|
104
|
+
"scroll", "text_input", "text_input_str",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def add(container, cls, *args, **props):
|
|
109
|
+
"""Fallback for `container.add(cls, *args, **props)` if type patching is off."""
|
|
110
|
+
return builder.container_add(container, cls, *args, **props)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def set_root(context, cls, *args, **props):
|
|
114
|
+
"""Fallback for `context.set_root(cls, *args, **props)` if type patching is off."""
|
|
115
|
+
return builder.context_set_root(context, cls, *args, **props)
|
slikui/_py/__init__.py
ADDED
|
File without changes
|
slikui/_py/app.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Headless app shell: Target + Sink + Context with conveniences.
|
|
2
|
+
|
|
3
|
+
`App` holds every owner (target, sink, context), so the C++ side never sees a
|
|
4
|
+
dangling backend even though the bindings are non-owning.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import atexit
|
|
8
|
+
import weakref
|
|
9
|
+
|
|
10
|
+
from slikui._py.builder import context_set_root
|
|
11
|
+
from slikui._slikui import Context, Sink, Target, find_system_font
|
|
12
|
+
|
|
13
|
+
# Weakrefs of Apps waiting for an atexit close; pruned as refs die.
|
|
14
|
+
_exit_close_refs = []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _register_exit_close(app):
|
|
18
|
+
"""Run app.close() during interpreter shutdown, before module teardown.
|
|
19
|
+
|
|
20
|
+
Python-level atexit callbacks run while every module (slikui's extension
|
|
21
|
+
included) is still healthy. Waiting for App.__del__ is too late: nanobind
|
|
22
|
+
starts releasing its internals as soon as its module is freed during
|
|
23
|
+
module clearing, before __main__'s globals are collected. The weakref
|
|
24
|
+
keeps the App collectable; close() unregisters.
|
|
25
|
+
"""
|
|
26
|
+
_exit_close_refs[:] = [r for r in _exit_close_refs if r() is not None]
|
|
27
|
+
ref = weakref.ref(app)
|
|
28
|
+
_exit_close_refs.append(ref)
|
|
29
|
+
|
|
30
|
+
def _close():
|
|
31
|
+
a = ref()
|
|
32
|
+
if a is not None:
|
|
33
|
+
try:
|
|
34
|
+
a.close()
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
app._exit_close = _close
|
|
39
|
+
atexit.register(_close)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class App:
|
|
43
|
+
def __init__(self, width, height, *, font=None, scale=1.0, theme=None):
|
|
44
|
+
self.width = int(width)
|
|
45
|
+
self.height = int(height)
|
|
46
|
+
self.scale = float(scale)
|
|
47
|
+
# Logical frame is width x height; the framebuffer carries the scale.
|
|
48
|
+
self.target = Target(int(self.width * self.scale), int(self.height * self.scale))
|
|
49
|
+
self.sink = Sink(self.target)
|
|
50
|
+
self.sink.set_scale(self.scale)
|
|
51
|
+
self.ctx = Context(self.sink)
|
|
52
|
+
self.ctx.set_frame_size(float(self.width), float(self.height))
|
|
53
|
+
if font is not None:
|
|
54
|
+
self.ctx.load_font_file(font)
|
|
55
|
+
elif not self.ctx.load_font_file(find_system_font()):
|
|
56
|
+
# No system font (minimal containers, e.g. manylinux): fall back
|
|
57
|
+
# to the bundled DejaVu Sans subset so text still renders.
|
|
58
|
+
self.ctx.load_default_font()
|
|
59
|
+
if theme is not None:
|
|
60
|
+
self.ctx.theme = theme
|
|
61
|
+
_register_exit_close(self)
|
|
62
|
+
|
|
63
|
+
def set_root(self, cls, *args, **props):
|
|
64
|
+
return context_set_root(self.ctx, cls, *args, **props)
|
|
65
|
+
|
|
66
|
+
def resize(self, width, height):
|
|
67
|
+
self.width, self.height = int(width), int(height)
|
|
68
|
+
self.target.resize(int(self.width * self.scale), int(self.height * self.scale))
|
|
69
|
+
self.ctx.set_frame_size(float(self.width), float(self.height))
|
|
70
|
+
|
|
71
|
+
def tick(self, now=0.0, dt=0.0):
|
|
72
|
+
self.ctx.tick(now, dt)
|
|
73
|
+
|
|
74
|
+
def handle(self, event):
|
|
75
|
+
self.ctx.handle(event)
|
|
76
|
+
|
|
77
|
+
def render(self, clear=True):
|
|
78
|
+
self.ctx.render(clear)
|
|
79
|
+
return self.target.pixels() # zero-copy (H, W) uint32, ABGR packing
|
|
80
|
+
|
|
81
|
+
def close(self):
|
|
82
|
+
"""Destroy the widget tree now, releasing the Python callbacks it holds.
|
|
83
|
+
|
|
84
|
+
A callback capturing a widget wrapper closes a cycle through C++
|
|
85
|
+
ownership that the GC cannot see; destroying the tree while the
|
|
86
|
+
interpreter is alive is what makes shutdown leak-free. Called
|
|
87
|
+
automatically by __exit__/__del__/atexit; safe to call twice.
|
|
88
|
+
"""
|
|
89
|
+
self.ctx.clear_tree()
|
|
90
|
+
ec = getattr(self, "_exit_close", None)
|
|
91
|
+
if ec is not None:
|
|
92
|
+
atexit.unregister(ec)
|
|
93
|
+
self._exit_close = None
|
|
94
|
+
|
|
95
|
+
def __enter__(self):
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, *exc):
|
|
99
|
+
self.close()
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def __del__(self):
|
|
103
|
+
# Best effort: App is not part of any reference cycle (the cycle lives
|
|
104
|
+
# between the context wrapper and callbacks inside the C++ tree), so in
|
|
105
|
+
# the normal case this runs mid-interpreter and releases everything.
|
|
106
|
+
# At interpreter shutdown the same call is harmless but too late; GUI
|
|
107
|
+
# apps that keep their UI until exit can silence the report with
|
|
108
|
+
# slikui.set_leak_warnings(False).
|
|
109
|
+
try:
|
|
110
|
+
self.close()
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
slikui/_py/builder.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""kwargs → setters, and the `add`/`set_root` factories.
|
|
2
|
+
|
|
3
|
+
No registry to keep in sync: a kwarg name `foo` resolves to the method
|
|
4
|
+
`set_foo`, or to `foo` itself when there is no setter (e.g. `on_click=`).
|
|
5
|
+
A wrong name is a loud AttributeError, not a silent no-op.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def apply_props(widget, props):
|
|
10
|
+
for name, value in props.items():
|
|
11
|
+
fn = getattr(widget, f"set_{name}", None) or getattr(widget, name)
|
|
12
|
+
fn(value)
|
|
13
|
+
return widget
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def container_add(self, cls, *args, **props):
|
|
17
|
+
"""self.add(slikui.Label, "hi", font_size=26) → construct + apply kwargs."""
|
|
18
|
+
fn = getattr(self, f"add_{cls.__name__.lower()}")
|
|
19
|
+
return apply_props(fn(*args), props)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def context_set_root(self, cls, *args, **props):
|
|
23
|
+
"""ctx.set_root(slikui.Column, padding=16) → construct + apply kwargs."""
|
|
24
|
+
fn = getattr(self, f"set_root_{cls.__name__.lower()}")
|
|
25
|
+
return apply_props(fn(*args), props)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def try_patch():
|
|
29
|
+
"""Attach `add`/`set_root` to the bound classes. True on success."""
|
|
30
|
+
try:
|
|
31
|
+
from slikui._slikui import Context, Widget
|
|
32
|
+
|
|
33
|
+
Widget.add = container_add # inherited by every widget, incl. ScrollArea
|
|
34
|
+
Context.set_root = context_set_root
|
|
35
|
+
return True
|
|
36
|
+
except (AttributeError, TypeError):
|
|
37
|
+
return False
|
slikui/_py/events.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Friendly slikui::Event constructors — for tests, apps and window bridges.
|
|
2
|
+
|
|
3
|
+
`Context.handle()` consumes these directly. Positions are logical points
|
|
4
|
+
(Vec2-compatible pairs); `mods` accepts an int mask or a string like
|
|
5
|
+
"ctrl+shift". A typical click is `mouse_press(p)` then `mouse_release(p)`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from slikui._slikui import Event, EventType, Key, Mod, MouseButton, Vec2
|
|
9
|
+
|
|
10
|
+
_MOD_NAMES = {
|
|
11
|
+
"shift": int(Mod.ModShift),
|
|
12
|
+
"ctrl": int(Mod.ModCtrl),
|
|
13
|
+
"control": int(Mod.ModCtrl),
|
|
14
|
+
"alt": int(Mod.ModAlt),
|
|
15
|
+
"option": int(Mod.ModAlt),
|
|
16
|
+
"super": int(Mod.ModSuper),
|
|
17
|
+
"cmd": int(Mod.ModSuper),
|
|
18
|
+
"meta": int(Mod.ModSuper),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def mods(value) -> int:
|
|
23
|
+
"""Int mask, or a string like "ctrl+shift" / "alt|cmd"."""
|
|
24
|
+
if value is None:
|
|
25
|
+
return 0
|
|
26
|
+
if isinstance(value, int):
|
|
27
|
+
return value
|
|
28
|
+
mask = 0
|
|
29
|
+
for part in str(value).replace("|", "+").split("+"):
|
|
30
|
+
name = part.strip().lower()
|
|
31
|
+
if name:
|
|
32
|
+
mask |= _MOD_NAMES[name]
|
|
33
|
+
return mask
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def mouse_move(pos, delta=(0.0, 0.0)):
|
|
37
|
+
e = Event(EventType.MouseMove)
|
|
38
|
+
e.pos = Vec2(*pos)
|
|
39
|
+
e.delta = Vec2(*delta)
|
|
40
|
+
return e
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def mouse_press(pos, button=MouseButton.MouseLeft, mods_=0):
|
|
44
|
+
e = Event(EventType.MousePress)
|
|
45
|
+
e.pos = Vec2(*pos)
|
|
46
|
+
e.button = int(button)
|
|
47
|
+
e.mods = mods(mods_)
|
|
48
|
+
return e
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def mouse_release(pos, button=MouseButton.MouseLeft, mods_=0):
|
|
52
|
+
e = Event(EventType.MouseRelease)
|
|
53
|
+
e.pos = Vec2(*pos)
|
|
54
|
+
e.button = int(button)
|
|
55
|
+
e.mods = mods(mods_)
|
|
56
|
+
return e
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def mouse_click(pos, button=MouseButton.MouseLeft, mods_=0):
|
|
60
|
+
"""Press + release pair at `pos`."""
|
|
61
|
+
return (mouse_press(pos, button, mods_), mouse_release(pos, button, mods_))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def scroll(pos, ticks):
|
|
65
|
+
e = Event(EventType.Scroll)
|
|
66
|
+
e.pos = Vec2(*pos)
|
|
67
|
+
e.scroll = Vec2(*ticks)
|
|
68
|
+
return e
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def key_press(key, mods_=0):
|
|
72
|
+
e = Event(EventType.KeyPress)
|
|
73
|
+
e.key = key
|
|
74
|
+
e.mods = mods(mods_)
|
|
75
|
+
return e
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def key_release(key, mods_=0):
|
|
79
|
+
e = Event(EventType.KeyRelease)
|
|
80
|
+
e.key = key
|
|
81
|
+
e.mods = mods(mods_)
|
|
82
|
+
return e
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def text_input(char: str):
|
|
86
|
+
e = Event(EventType.TextInput)
|
|
87
|
+
e.codepoint = ord(char)
|
|
88
|
+
return e
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def text_input_str(text: str):
|
|
92
|
+
"""One TextInput event per Unicode scalar."""
|
|
93
|
+
return [text_input(ch) for ch in text]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def focus_lost():
|
|
97
|
+
return Event(EventType.FocusLost)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def frame_resize(width, height):
|
|
101
|
+
e = Event(EventType.FrameResize)
|
|
102
|
+
e.pos = Vec2(float(width), float(height))
|
|
103
|
+
return e
|
slikui/_py/pixels.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Pixel helpers.
|
|
2
|
+
|
|
3
|
+
slikui frames are `(H, W) uint32` in ABGR packing — 0xAABBGGRR values, i.e.
|
|
4
|
+
little-endian byte order B, G, R, A. `to_rgba` converts to the conventional
|
|
5
|
+
(H, W, 4) uint8 RGBA layout (a copy; the raw view is the zero-cost path).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_rgba(frame):
|
|
12
|
+
frame = np.asarray(frame)
|
|
13
|
+
r = frame & 0xFF
|
|
14
|
+
g = (frame >> 8) & 0xFF
|
|
15
|
+
b = (frame >> 16) & 0xFF
|
|
16
|
+
a = (frame >> 24) & 0xFF
|
|
17
|
+
return np.stack([r, g, b, a], axis=-1).astype(np.uint8, copy=False)
|
slikui/_py/renderer.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""CanvasSink — a pure-Python (numpy) renderer implementing the slikui contract.
|
|
2
|
+
|
|
3
|
+
This is the "your renderer" seam: subclass `slikui.RenderSink`, and slikui hands
|
|
4
|
+
you every frame as a flat command list (Rect/Line/Glyph/Image/ClipPush/ClipPop)
|
|
5
|
+
plus texture uploads. CanvasSink rasterizes that list into a numpy uint32
|
|
6
|
+
buffer with the exact same math as the C++ software backend in
|
|
7
|
+
`include/slikui/backends/software/software.hpp` — same blend formula, same SDF
|
|
8
|
+
coverage, same sampling — so frames come out pixel-identical.
|
|
9
|
+
|
|
10
|
+
Pixel format: 0xAABBGGRR uint32, straight alpha, row-major (H, W).
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
sink = CanvasSink(320, 240)
|
|
14
|
+
ctx = slikui.Context(sink)
|
|
15
|
+
ctx.set_frame_size(320, 240)
|
|
16
|
+
ctx.load_font_file(slikui.find_system_font())
|
|
17
|
+
...
|
|
18
|
+
ctx.tick(0.0, 1 / 60)
|
|
19
|
+
ctx.render() # slikui calls sink.upload/render with the draw list
|
|
20
|
+
frame = sink.buffer # (240, 320) uint32 view
|
|
21
|
+
|
|
22
|
+
Coordinates: DrawList commands are logical points; `set_scale(s)` maps them
|
|
23
|
+
to device pixels exactly like software.Sink (HiDPI).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from slikui._slikui import DrawCmdType, RenderSink, TexFormat
|
|
31
|
+
|
|
32
|
+
_F32 = np.float32
|
|
33
|
+
_U32 = np.uint32
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _clamp01(v):
|
|
37
|
+
# C++ clamp01: v < 0 ? 0 : (v > 1 ? 1 : v)
|
|
38
|
+
return np.where(v < _F32(0), _F32(0), np.where(v > _F32(1), _F32(1), v))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sd_rounded_box(px, py, bx, by, r):
|
|
42
|
+
"""Approximate SDF of a rounded box, half-extents (bx, by), radius r.
|
|
43
|
+
|
|
44
|
+
Mirrors detail::sd_rounded_box operation-for-operation (float32) so edge
|
|
45
|
+
coverage matches the C++ rasterizer bit for bit.
|
|
46
|
+
"""
|
|
47
|
+
qx = np.abs(px) - (bx - r)
|
|
48
|
+
qy = np.abs(py) - (by - r)
|
|
49
|
+
ax = np.maximum(qx, _F32(0))
|
|
50
|
+
ay = np.maximum(qy, _F32(0))
|
|
51
|
+
outside = np.sqrt(ax * ax + ay * ay)
|
|
52
|
+
return (outside + np.minimum(np.maximum(qx, qy), _F32(0))) - r
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _intersect(ax, ay, aw, ah, bx, by, bw, bh):
|
|
56
|
+
# Rect::intersect in float32
|
|
57
|
+
nx = np.maximum(ax, bx)
|
|
58
|
+
ny = np.maximum(ay, by)
|
|
59
|
+
nr = np.minimum(ax + aw, bx + bw)
|
|
60
|
+
nb = np.minimum(ay + ah, by + bh)
|
|
61
|
+
return nx, ny, np.maximum(_F32(0), nr - nx), np.maximum(_F32(0), nb - ny)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CanvasSink(RenderSink):
|
|
65
|
+
"""Software renderer in numpy. Subclass of slikui.RenderSink."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, width, height, *, scale=1.0):
|
|
68
|
+
super().__init__()
|
|
69
|
+
self._scale = _F32(scale)
|
|
70
|
+
self._buf = np.zeros((int(height), int(width)), np.uint32)
|
|
71
|
+
self._textures = {} # id -> (h, w) or (h, w, 4) uint8
|
|
72
|
+
self._clips = [] # list of (x, y, w, h) float32
|
|
73
|
+
|
|
74
|
+
# -- surface ------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def buffer(self):
|
|
78
|
+
"""The (H, W) uint32 framebuffer, 0xAABBGGRR. Zero-copy view."""
|
|
79
|
+
return self._buf
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def width(self):
|
|
83
|
+
return self._buf.shape[1]
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def height(self):
|
|
87
|
+
return self._buf.shape[0]
|
|
88
|
+
|
|
89
|
+
def resize(self, width, height):
|
|
90
|
+
self._buf = np.zeros((int(height), int(width)), np.uint32)
|
|
91
|
+
|
|
92
|
+
def set_scale(self, scale):
|
|
93
|
+
self._scale = _F32(scale)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def scale(self):
|
|
97
|
+
return float(self._scale)
|
|
98
|
+
|
|
99
|
+
# -- IRenderSink ---------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def upload(self, up):
|
|
102
|
+
n = up.width * up.height * (4 if up.format == TexFormat.Rgba8 else 1)
|
|
103
|
+
data = np.frombuffer(bytes(up.pixels), np.uint8, n).copy()
|
|
104
|
+
if up.format == TexFormat.Rgba8:
|
|
105
|
+
data = data.reshape(up.height, up.width, 4)
|
|
106
|
+
else:
|
|
107
|
+
data = data.reshape(up.height, up.width)
|
|
108
|
+
self._textures[up.id] = data
|
|
109
|
+
|
|
110
|
+
def render(self, cmds, framebuffer_size):
|
|
111
|
+
self._clips = [(_F32(0), _F32(0), _F32(self.width), _F32(self.height))]
|
|
112
|
+
for c in cmds:
|
|
113
|
+
self._execute(c)
|
|
114
|
+
|
|
115
|
+
# -- command dispatch -----------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _execute(self, c):
|
|
118
|
+
s = self._scale
|
|
119
|
+
x = _F32(c.rect.x) * s
|
|
120
|
+
y = _F32(c.rect.y) * s
|
|
121
|
+
w = _F32(c.rect.w) * s
|
|
122
|
+
h = _F32(c.rect.h) * s
|
|
123
|
+
t = c.type
|
|
124
|
+
if t == DrawCmdType.ClipPush:
|
|
125
|
+
self._clips.append(_intersect(x, y, w, h, *self._clips[-1]))
|
|
126
|
+
elif t == DrawCmdType.ClipPop:
|
|
127
|
+
if len(self._clips) > 1:
|
|
128
|
+
self._clips.pop()
|
|
129
|
+
elif t == DrawCmdType.Rect:
|
|
130
|
+
if c.hollow:
|
|
131
|
+
self._stroke_rect(x, y, w, h, c.color,
|
|
132
|
+
_F32(c.thickness) * s, _F32(c.radius) * s)
|
|
133
|
+
else:
|
|
134
|
+
self._fill_rect(x, y, w, h, c.color, _F32(c.radius) * s)
|
|
135
|
+
elif t == DrawCmdType.Line:
|
|
136
|
+
self._stroke_line(x, y, w, h, c.color, _F32(c.thickness) * s)
|
|
137
|
+
elif t == DrawCmdType.Glyph:
|
|
138
|
+
self._draw_glyph(c, x, y, w, h)
|
|
139
|
+
elif t == DrawCmdType.Image:
|
|
140
|
+
self._draw_image(c, x, y, w, h)
|
|
141
|
+
|
|
142
|
+
# -- blending --------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def _blend(self, X, Y, mask, sr, sg, sb, sa):
|
|
145
|
+
"""C++ blend() over selected pixels.
|
|
146
|
+
|
|
147
|
+
sr..sa: scalar _U32 or full-grid arrays (per-pixel src color/alpha);
|
|
148
|
+
anything array-valued is masked down to the selected pixels here.
|
|
149
|
+
"""
|
|
150
|
+
mask = mask & (X >= 0) & (Y >= 0) & (X < self.width) & (Y < self.height)
|
|
151
|
+
if not mask.any():
|
|
152
|
+
return
|
|
153
|
+
ys = np.broadcast_to(Y, mask.shape)[mask]
|
|
154
|
+
xs = np.broadcast_to(X, mask.shape)[mask]
|
|
155
|
+
dst = self._buf[ys, xs]
|
|
156
|
+
dr = dst & _U32(0xFF)
|
|
157
|
+
dg = (dst >> _U32(8)) & _U32(0xFF)
|
|
158
|
+
db = (dst >> _U32(16)) & _U32(0xFF)
|
|
159
|
+
da = dst >> _U32(24)
|
|
160
|
+
sr, sg, sb, sa = (_v[mask] if np.ndim(_v) else _v
|
|
161
|
+
for _v in (sr, sg, sb, sa))
|
|
162
|
+
ia = _U32(255) - sa
|
|
163
|
+
# (s*a + d*ia + 127) / 255 with uint32 floor division == C++ integer math
|
|
164
|
+
out_r = (sr * sa + dr * ia + _U32(127)) // _U32(255)
|
|
165
|
+
out_g = (sg * sa + dg * ia + _U32(127)) // _U32(255)
|
|
166
|
+
out_b = (sb * sa + db * ia + _U32(127)) // _U32(255)
|
|
167
|
+
out_a = sa + da * ia // _U32(255)
|
|
168
|
+
self._buf[ys, xs] = (
|
|
169
|
+
(out_a << _U32(24)) | (out_b << _U32(16)) |
|
|
170
|
+
(out_g << _U32(8)) | out_r)
|
|
171
|
+
|
|
172
|
+
def _faded_alpha(self, color_a, f):
|
|
173
|
+
"""Color::faded: with_alpha(clamp01(a/255 * f)) -> uint8 (truncating)."""
|
|
174
|
+
a = (_F32(color_a) / _F32(255)) * f
|
|
175
|
+
a = np.minimum(np.maximum(a, _F32(0)), _F32(1))
|
|
176
|
+
return (_F32(255) * a).astype(np.uint8)
|
|
177
|
+
|
|
178
|
+
# -- primitives --------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def _grid(self, y0, y1, x0, x1):
|
|
181
|
+
"""Inclusive integer pixel grid as broadcastable (Y, X) int64 arrays."""
|
|
182
|
+
Y = np.arange(int(y0), int(y1) + 1, dtype=np.int64)[:, None]
|
|
183
|
+
X = np.arange(int(x0), int(x1) + 1, dtype=np.int64)[None, :]
|
|
184
|
+
return Y, X
|
|
185
|
+
|
|
186
|
+
def _clip_mask(self, X, Y):
|
|
187
|
+
cx, cy, cw, ch = self._clips[-1]
|
|
188
|
+
return (X >= cx) & (X < cx + cw) & (Y >= cy) & (Y < cy + ch)
|
|
189
|
+
|
|
190
|
+
def _full_mask(self, X, Y):
|
|
191
|
+
return np.ones(np.broadcast_shapes(Y.shape, X.shape), dtype=bool)
|
|
192
|
+
|
|
193
|
+
def _fill_rect(self, x, y, w, h, color, radius):
|
|
194
|
+
clip = self._clips[-1]
|
|
195
|
+
x, y, w, h = _intersect(x, y, w, h, *clip)
|
|
196
|
+
if w <= 0 or h <= 0:
|
|
197
|
+
return
|
|
198
|
+
if radius <= 0:
|
|
199
|
+
# NOTE: matches C++ exactly — the fast path loops from int(r.x)
|
|
200
|
+
# with no per-pixel clip check, so a fractional origin paints the
|
|
201
|
+
# pixel below clip.x too. Quirk kept on purpose.
|
|
202
|
+
Y, X = self._grid(y, np.ceil(y + h) - 1, x, np.ceil(x + w) - 1)
|
|
203
|
+
self._blend(X, Y, self._full_mask(X, Y),
|
|
204
|
+
_U32(color.r), _U32(color.g), _U32(color.b),
|
|
205
|
+
_U32(color.a))
|
|
206
|
+
return
|
|
207
|
+
# Per-pixel SDF coverage (this path does check the clip per pixel).
|
|
208
|
+
Y, X = self._grid(y - 1, np.ceil(y + h), x - 1, np.ceil(x + w))
|
|
209
|
+
px = (X.astype(_F32) + _F32(0.5) - x) - w * _F32(0.5)
|
|
210
|
+
py = (Y.astype(_F32) + _F32(0.5) - y) - h * _F32(0.5)
|
|
211
|
+
d = _sd_rounded_box(px, py, w * _F32(0.5), h * _F32(0.5), radius)
|
|
212
|
+
cov = _clamp01(_F32(0.5) - d)
|
|
213
|
+
mask = (cov > _F32(0)) & self._clip_mask(X, Y)
|
|
214
|
+
sa = self._faded_alpha(color.a, cov).astype(_U32)
|
|
215
|
+
self._blend(X, Y, mask, _U32(color.r), _U32(color.g), _U32(color.b), sa)
|
|
216
|
+
|
|
217
|
+
def _stroke_rect(self, x, y, w, h, color, thickness, radius):
|
|
218
|
+
inner_r = np.maximum(radius - thickness, _F32(0))
|
|
219
|
+
Y, X = self._grid(y - 1, np.ceil(y + h), x - 1, np.ceil(x + w))
|
|
220
|
+
px = (X.astype(_F32) + _F32(0.5) - x) - w * _F32(0.5)
|
|
221
|
+
py = (Y.astype(_F32) + _F32(0.5) - y) - h * _F32(0.5)
|
|
222
|
+
d_out = _sd_rounded_box(px, py, w * _F32(0.5), h * _F32(0.5), radius)
|
|
223
|
+
bi_x = np.maximum(w * _F32(0.5) - thickness, _F32(0))
|
|
224
|
+
bi_y = np.maximum(h * _F32(0.5) - thickness, _F32(0))
|
|
225
|
+
d_in = _sd_rounded_box(px, py, bi_x, bi_y, inner_r)
|
|
226
|
+
cov = _clamp01(_F32(0.5) - d_out) * _clamp01(_F32(0.5) + d_in)
|
|
227
|
+
mask = (cov > _F32(0)) & self._clip_mask(X, Y)
|
|
228
|
+
sa = self._faded_alpha(color.a, cov).astype(_U32)
|
|
229
|
+
self._blend(X, Y, mask, _U32(color.r), _U32(color.g), _U32(color.b), sa)
|
|
230
|
+
|
|
231
|
+
def _stroke_line(self, x, y, dx, dy, color, thickness):
|
|
232
|
+
ax, ay = x, y
|
|
233
|
+
bx = x + dx
|
|
234
|
+
by = y + dy
|
|
235
|
+
half = thickness * _F32(0.5)
|
|
236
|
+
bx0 = np.minimum(ax, bx) - half - _F32(1)
|
|
237
|
+
by0 = np.minimum(ay, by) - half - _F32(1)
|
|
238
|
+
bw = np.abs(bx - ax) + _F32(2) * half + _F32(2)
|
|
239
|
+
bh = np.abs(by - ay) + _F32(2) * half + _F32(2)
|
|
240
|
+
cx, cy, cw, ch = self._clips[-1]
|
|
241
|
+
bx0, by0, bw, bh = _intersect(bx0, by0, bw, bh, cx, cy, cw, ch)
|
|
242
|
+
Y, X = self._grid(by0, np.ceil(by0 + bh), bx0, np.ceil(bx0 + bw))
|
|
243
|
+
px = X.astype(_F32) + _F32(0.5)
|
|
244
|
+
py = Y.astype(_F32) + _F32(0.5)
|
|
245
|
+
ddx = bx - ax
|
|
246
|
+
ddy = by - ay
|
|
247
|
+
len2 = ddx * ddx + ddy * ddy
|
|
248
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
249
|
+
t = ((px - ax) * ddx + (py - ay) * ddy) / len2
|
|
250
|
+
t = np.where(len2 > _F32(0), t, _F32(0))
|
|
251
|
+
t = _clamp01(t)
|
|
252
|
+
proj_x = ax + ddx * t
|
|
253
|
+
proj_y = ay + ddy * t
|
|
254
|
+
dist = np.sqrt((px - proj_x) * (px - proj_x) +
|
|
255
|
+
(py - proj_y) * (py - proj_y))
|
|
256
|
+
cov = _clamp01((half + _F32(0.5)) - dist)
|
|
257
|
+
mask = cov > _F32(0)
|
|
258
|
+
sa = self._faded_alpha(color.a, cov).astype(_U32)
|
|
259
|
+
self._blend(X, Y, mask, _U32(color.r), _U32(color.g), _U32(color.b), sa)
|
|
260
|
+
|
|
261
|
+
def _uv_maps(self, c, x, y, w, h, X, Y):
|
|
262
|
+
# shared uv math for glyph/image: sample point from pixel center
|
|
263
|
+
fx = X.astype(_F32) + _F32(0.5)
|
|
264
|
+
fy = Y.astype(_F32) + _F32(0.5)
|
|
265
|
+
u = _F32(c.uv.x) + ((fx - x) / w) * _F32(c.uv.w)
|
|
266
|
+
v = _F32(c.uv.y) + ((fy - y) / h) * _F32(c.uv.h)
|
|
267
|
+
return u, v
|
|
268
|
+
|
|
269
|
+
def _draw_glyph(self, c, x, y, w, h):
|
|
270
|
+
tex = self._textures.get(c.tex_id)
|
|
271
|
+
if tex is None or c.uv.w <= 0 or c.uv.h <= 0:
|
|
272
|
+
return
|
|
273
|
+
clip = self._clips[-1]
|
|
274
|
+
cx, cy, cw, ch = _intersect(x, y, w, h, *clip)
|
|
275
|
+
if cw <= 0 or ch <= 0:
|
|
276
|
+
return
|
|
277
|
+
Y, X = self._grid(cy, np.ceil(cy + ch) - 1, cx, np.ceil(cx + cw) - 1)
|
|
278
|
+
u, v = self._uv_maps(c, x, y, w, h, X, Y)
|
|
279
|
+
alpha = self._sample_alpha(tex, u, v)
|
|
280
|
+
# Color c = cmd.color.faded(a.a / 255); blend only where a > 0.
|
|
281
|
+
f = alpha.astype(_F32) / _F32(255)
|
|
282
|
+
sa = self._faded_alpha(c.color.a, f).astype(_U32)
|
|
283
|
+
mask = sa > _U32(0)
|
|
284
|
+
self._blend(X, Y, mask, _U32(c.color.r), _U32(c.color.g),
|
|
285
|
+
_U32(c.color.b), sa)
|
|
286
|
+
|
|
287
|
+
def _sample_alpha(self, tex, u, v):
|
|
288
|
+
"""detail::sample_alpha — bilinear alpha, clamped, rounded (+0.5)."""
|
|
289
|
+
th, tw = tex.shape
|
|
290
|
+
fx = np.maximum(np.minimum(u, _F32(tw - 1)), _F32(0))
|
|
291
|
+
fy = np.maximum(np.minimum(v, _F32(th - 1)), _F32(0))
|
|
292
|
+
x0 = fx.astype(np.int64)
|
|
293
|
+
y0 = fy.astype(np.int64)
|
|
294
|
+
x1 = np.minimum(x0 + 1, tw - 1)
|
|
295
|
+
y1 = np.minimum(y0 + 1, th - 1)
|
|
296
|
+
tx = fx - x0.astype(_F32)
|
|
297
|
+
ty = fy - y0.astype(_F32)
|
|
298
|
+
a00 = tex[y0, x0].astype(_F32)
|
|
299
|
+
a10 = tex[y0, x1].astype(_F32)
|
|
300
|
+
a01 = tex[y1, x0].astype(_F32)
|
|
301
|
+
a11 = tex[y1, x1].astype(_F32)
|
|
302
|
+
a = (a00 * (_F32(1) - tx) + a10 * tx) * (_F32(1) - ty) + \
|
|
303
|
+
(a01 * (_F32(1) - tx) + a11 * tx) * ty
|
|
304
|
+
return (a + _F32(0.5)).astype(np.uint8)
|
|
305
|
+
|
|
306
|
+
def _draw_image(self, c, x, y, w, h):
|
|
307
|
+
tex = self._textures.get(c.tex_id)
|
|
308
|
+
if tex is None:
|
|
309
|
+
return
|
|
310
|
+
clip = self._clips[-1]
|
|
311
|
+
cx, cy, cw, ch = _intersect(x, y, w, h, *clip)
|
|
312
|
+
if cw <= 0 or ch <= 0:
|
|
313
|
+
return
|
|
314
|
+
Y, X = self._grid(cy, np.ceil(cy + ch) - 1, cx, np.ceil(cx + cw) - 1)
|
|
315
|
+
u, v = self._uv_maps(c, x, y, w, h, X, Y)
|
|
316
|
+
if tex.ndim == 3:
|
|
317
|
+
# Rgba8: nearest sample, blend the texel's own color/alpha.
|
|
318
|
+
th, tw = tex.shape[:2]
|
|
319
|
+
sx = np.clip(u.astype(np.int64), 0, tw - 1)
|
|
320
|
+
sy = np.clip(v.astype(np.int64), 0, th - 1)
|
|
321
|
+
px = tex[sy, sx]
|
|
322
|
+
self._blend(X, Y, self._full_mask(X, Y),
|
|
323
|
+
px[..., 0].astype(_U32), px[..., 1].astype(_U32),
|
|
324
|
+
px[..., 2].astype(_U32), px[..., 3].astype(_U32))
|
|
325
|
+
else:
|
|
326
|
+
# Alpha8 image: white with the sampled alpha (sample_alpha path).
|
|
327
|
+
alpha = self._sample_alpha(tex, u, v)
|
|
328
|
+
self._blend(X, Y, alpha > 0, _U32(255), _U32(255), _U32(255),
|
|
329
|
+
alpha.astype(_U32))
|
|
Binary file
|
slikui/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slikui-python
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python bindings for slikui — a header-only C++20 UI library
|
|
5
|
+
Author: slikui contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: third_party/fonts/LICENSE-DejaVu.txt
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: C++
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Project-URL: Homepage, https://github.com/jamesgregson/slikui
|
|
17
|
+
Project-URL: Documentation, https://github.com/jamesgregson/slikui/tree/main/python
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
22
|
+
Provides-Extra: demo
|
|
23
|
+
Requires-Dist: glfw>=2.5; extra == "demo"
|
|
24
|
+
Requires-Dist: PyOpenGL; extra == "demo"
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# slikui — Python bindings for slikui
|
|
28
|
+
|
|
29
|
+
Install from PyPI · import `slikui` · compiled module `slikui._slikui`.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install slikui-python
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
pip picks the wheel matching your OS, CPU and Python (3.10–3.14 on Linux
|
|
36
|
+
x86_64/aarch64, macOS arm64, Windows x64). No wheel for your platform (e.g.
|
|
37
|
+
Intel macOS)? pip builds from the sdist instead — that needs a C++20 compiler.
|
|
38
|
+
|
|
39
|
+
The same wheels and sdist are attached to every
|
|
40
|
+
[GitHub Release](https://github.com/jamesgregson/slikui/releases).
|
|
41
|
+
|
|
42
|
+
Headless, testable UI: the software rasterizer renders into a `numpy`
|
|
43
|
+
framebuffer — no window, no GPU, no event loop required.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import slikui
|
|
47
|
+
|
|
48
|
+
ui = slikui.App(320, 200) # Target + Sink + Context + font (system, else bundled)
|
|
49
|
+
root = ui.set_root(slikui.Column, padding=16, gap=8)
|
|
50
|
+
root.add(slikui.Label, "Hello, slikui!", font_size=26)
|
|
51
|
+
root.add(slikui.Button, "Save", variant=slikui.ButtonVariant.Primary)
|
|
52
|
+
|
|
53
|
+
ui.tick(0.0, 1 / 60) # layout + animation
|
|
54
|
+
frame = ui.render() # zero-copy np.ndarray (H, W) uint32
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`App.close()` — also run by `with App(...)` and automatically at exit —
|
|
58
|
+
destroys the tree and releases Python callbacks it holds, so shutdown is
|
|
59
|
+
leak-free even when callbacks capture widget wrappers.
|
|
60
|
+
|
|
61
|
+
Build from source (the C++ side is header-only, nothing to link):
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
pip install -e . # editable, into your venv — from the repo root
|
|
65
|
+
python -m pytest python/tests # 38 tests: widgets, events, renderer parity
|
|
66
|
+
python python/examples/hello.py # writes hello.ppm
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Events are first-class: build slikui events in Python and drive the real
|
|
70
|
+
dispatch path (see `python/tests/test_events.py`):
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
ui.handle(slikui.mouse_press((60, 40))) # click through hit-testing
|
|
74
|
+
ui.handle(slikui.text_input("h")) # into the focused TextField
|
|
75
|
+
ui.handle(slikui.scroll(pos, (0, -1))) # wheel, "ctrl+shift" mods supported
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Interactive demo — a Python-native GLFW + OpenGL host with the slikui software
|
|
79
|
+
rasterizer as the UI layer (`pip install glfw PyOpenGL`):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
python python/examples/glfw_demo.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Lit cube/sphere/torus over a procedural sky; the controls panel is rasterized
|
|
86
|
+
on the CPU into a `uint32` buffer and blended over the GL scene as one
|
|
87
|
+
texture. Drag orbits, wheel zooms, the panel edits material/sky/light/camera.
|
|
88
|
+
|
|
89
|
+
**Your own renderer.** `slikui.RenderSink` is a trampoline over C++
|
|
90
|
+
`IRenderSink`: subclass it, implement `upload(tex)` and `render(commands,
|
|
91
|
+
fb_size)`, and slikui hands you every frame as a flat command list — point it at
|
|
92
|
+
pygame, moderngl, PIL, anything. `slikui.CanvasSink` is a worked example: a
|
|
93
|
+
pure-numpy rasterizer that produces **pixel-identical** output to the C++
|
|
94
|
+
software backend (same blend/SDF/sampling math, verified by test):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
sink = slikui.CanvasSink(320, 240)
|
|
98
|
+
ctx = slikui.Context(sink) # any RenderSink subclass works here
|
|
99
|
+
ctx.set_frame_size(320, 240)
|
|
100
|
+
ctx.load_default_font() # bundled DejaVu Sans subset; or load_font_file(path)
|
|
101
|
+
# ... build the tree, tick ...
|
|
102
|
+
ctx.render()
|
|
103
|
+
frame = sink.buffer # (240, 320) uint32, 0xAABBGGRR
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See it side by side: `python python/examples/pyrenderer_demo.py` renders one
|
|
107
|
+
tree through both renderers and reports the pixel diff (it is 0).
|
|
108
|
+
|
|
109
|
+
Design and milestones: `docs/python-bindings-plan.md` — M0 (values, software
|
|
110
|
+
backend, core widgets), M1 (full widget surface + themes) and M2 (events,
|
|
111
|
+
popups, tooltips, simulated-input tests) are done; M3 (wheels/CI/docs) and
|
|
112
|
+
M4 (GLFW+GL backend bindings) are tabled.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
slikui/__init__.py,sha256=LsRYckcm820yzbx1cR5y2WpbblL4NcrWSOM421sPnFg,3230
|
|
2
|
+
slikui/_py/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
slikui/_py/app.py,sha256=-1mLfkHDRHoFeD4_h4AjpbmswOJ_iaTLc77KZdNwg00,4211
|
|
4
|
+
slikui/_py/builder.py,sha256=gv5cyd3iT0msvDYlxxqsK_RYZ8u0a4BhiiJn-OHDfdk,1305
|
|
5
|
+
slikui/_py/events.py,sha256=AyRu0G8zkr6vYSMLn1RTROzmMWasOVuW5YzpiQR0ziw,2584
|
|
6
|
+
slikui/_py/pixels.py,sha256=wAjVtVxoYFbP-qGj-CtWdCak4bIhXawBTSGqQafO2Qw,519
|
|
7
|
+
slikui/_py/renderer.py,sha256=21dfP0uXuMCu_Ya51qu3-6Xu8c7m5iRMuMfFc0dBm5A,13801
|
|
8
|
+
slikui/_slikui.cp310-win32.pyd,sha256=CP6uZ1aCFe8HRC2mJB9jDSzcByK0_gAcH03RFpSegp4,435200
|
|
9
|
+
slikui/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
slikui_python-0.1.1.dist-info/METADATA,sha256=TwSE-2_hNCPt8oepRBOmwPO5zUIPEPfH-IZAltCP478,4583
|
|
11
|
+
slikui_python-0.1.1.dist-info/WHEEL,sha256=WR2gNbyWAK2A1soqGQb-YCxatrzCZ2LAO6P0FLTYzyQ,101
|
|
12
|
+
slikui_python-0.1.1.dist-info/licenses/LICENSE,sha256=aBfC36-nE3my79yfCCxAgso5fE_miKCKPmwr2aWLKtU,1097
|
|
13
|
+
slikui_python-0.1.1.dist-info/licenses/third_party/fonts/LICENSE-DejaVu.txt,sha256=PxoRvJmiHdti0OufPtLhtTDtEyMPx55apovioKGoCC8,9003
|
|
14
|
+
slikui_python-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 slikui contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
|
2
|
+
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Bitstream Vera Fonts Copyright
|
|
6
|
+
------------------------------
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
|
|
9
|
+
a trademark of Bitstream, Inc.
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of the fonts accompanying this license ("Fonts") and associated
|
|
13
|
+
documentation files (the "Font Software"), to reproduce and distribute the
|
|
14
|
+
Font Software, including without limitation the rights to use, copy, merge,
|
|
15
|
+
publish, distribute, and/or sell copies of the Font Software, and to permit
|
|
16
|
+
persons to whom the Font Software is furnished to do so, subject to the
|
|
17
|
+
following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright and trademark notices and this permission notice shall
|
|
20
|
+
be included in all copies of one or more of the Font Software typefaces.
|
|
21
|
+
|
|
22
|
+
The Font Software may be modified, altered, or added to, and in particular
|
|
23
|
+
the designs of glyphs or characters in the Fonts may be modified and
|
|
24
|
+
additional glyphs or characters may be added to the Fonts, only if the fonts
|
|
25
|
+
are renamed to names not containing either the words "Bitstream" or the word
|
|
26
|
+
"Vera".
|
|
27
|
+
|
|
28
|
+
This License becomes null and void to the extent applicable to Fonts or Font
|
|
29
|
+
Software that has been modified and is distributed under the "Bitstream
|
|
30
|
+
Vera" names.
|
|
31
|
+
|
|
32
|
+
The Font Software may be sold as part of a larger software package but no
|
|
33
|
+
copy of one or more of the Font Software typefaces may be sold by itself.
|
|
34
|
+
|
|
35
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
36
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
|
38
|
+
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
|
39
|
+
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
|
40
|
+
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
|
41
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
|
42
|
+
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
|
43
|
+
FONT SOFTWARE.
|
|
44
|
+
|
|
45
|
+
Except as contained in this notice, the names of Gnome, the Gnome
|
|
46
|
+
Foundation, and Bitstream Inc., shall not be used in advertising or
|
|
47
|
+
otherwise to promote the sale, use or other dealings in this Font Software
|
|
48
|
+
without prior written authorization from the Gnome Foundation or Bitstream
|
|
49
|
+
Inc., respectively. For further information, contact: fonts at gnome dot
|
|
50
|
+
org.
|
|
51
|
+
|
|
52
|
+
Arev Fonts Copyright
|
|
53
|
+
------------------------------
|
|
54
|
+
|
|
55
|
+
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
|
|
56
|
+
|
|
57
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
58
|
+
a copy of the fonts accompanying this license ("Fonts") and
|
|
59
|
+
associated documentation files (the "Font Software"), to reproduce
|
|
60
|
+
and distribute the modifications to the Bitstream Vera Font Software,
|
|
61
|
+
including without limitation the rights to use, copy, merge, publish,
|
|
62
|
+
distribute, and/or sell copies of the Font Software, and to permit
|
|
63
|
+
persons to whom the Font Software is furnished to do so, subject to
|
|
64
|
+
the following conditions:
|
|
65
|
+
|
|
66
|
+
The above copyright and trademark notices and this permission notice
|
|
67
|
+
shall be included in all copies of one or more of the Font Software
|
|
68
|
+
typefaces.
|
|
69
|
+
|
|
70
|
+
The Font Software may be modified, altered, or added to, and in
|
|
71
|
+
particular the designs of glyphs or characters in the Fonts may be
|
|
72
|
+
modified and additional glyphs or characters may be added to the
|
|
73
|
+
Fonts, only if the fonts are renamed to names not containing either
|
|
74
|
+
the words "Tavmjong Bah" or the word "Arev".
|
|
75
|
+
|
|
76
|
+
This License becomes null and void to the extent applicable to Fonts
|
|
77
|
+
or Font Software that has been modified and is distributed under the
|
|
78
|
+
"Tavmjong Bah Arev" names.
|
|
79
|
+
|
|
80
|
+
The Font Software may be sold as part of a larger software package but
|
|
81
|
+
no copy of one or more of the Font Software typefaces may be sold by
|
|
82
|
+
itself.
|
|
83
|
+
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
|
88
|
+
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
93
|
+
|
|
94
|
+
Except as contained in this notice, the name of Tavmjong Bah shall not
|
|
95
|
+
be used in advertising or otherwise to promote the sale, use or other
|
|
96
|
+
dealings in this Font Software without prior written authorization
|
|
97
|
+
from Tavmjong Bah. For further information, contact: tavmjong @ free
|
|
98
|
+
. fr.
|
|
99
|
+
|
|
100
|
+
TeX Gyre DJV Math
|
|
101
|
+
-----------------
|
|
102
|
+
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
|
103
|
+
|
|
104
|
+
Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski
|
|
105
|
+
(on behalf of TeX users groups) are in public domain.
|
|
106
|
+
|
|
107
|
+
Letters imported from Euler Fraktur from AMSfonts are (c) American
|
|
108
|
+
Mathematical Society (see below).
|
|
109
|
+
Bitstream Vera Fonts Copyright
|
|
110
|
+
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera
|
|
111
|
+
is a trademark of Bitstream, Inc.
|
|
112
|
+
|
|
113
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
114
|
+
of the fonts accompanying this license (“Fonts”) and associated
|
|
115
|
+
documentation
|
|
116
|
+
files (the “Font Software”), to reproduce and distribute the Font Software,
|
|
117
|
+
including without limitation the rights to use, copy, merge, publish,
|
|
118
|
+
distribute,
|
|
119
|
+
and/or sell copies of the Font Software, and to permit persons to whom
|
|
120
|
+
the Font Software is furnished to do so, subject to the following
|
|
121
|
+
conditions:
|
|
122
|
+
|
|
123
|
+
The above copyright and trademark notices and this permission notice
|
|
124
|
+
shall be
|
|
125
|
+
included in all copies of one or more of the Font Software typefaces.
|
|
126
|
+
|
|
127
|
+
The Font Software may be modified, altered, or added to, and in particular
|
|
128
|
+
the designs of glyphs or characters in the Fonts may be modified and
|
|
129
|
+
additional
|
|
130
|
+
glyphs or characters may be added to the Fonts, only if the fonts are
|
|
131
|
+
renamed
|
|
132
|
+
to names not containing either the words “Bitstream” or the word “Vera”.
|
|
133
|
+
|
|
134
|
+
This License becomes null and void to the extent applicable to Fonts or
|
|
135
|
+
Font Software
|
|
136
|
+
that has been modified and is distributed under the “Bitstream Vera”
|
|
137
|
+
names.
|
|
138
|
+
|
|
139
|
+
The Font Software may be sold as part of a larger software package but
|
|
140
|
+
no copy
|
|
141
|
+
of one or more of the Font Software typefaces may be sold by itself.
|
|
142
|
+
|
|
143
|
+
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
144
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
|
145
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
|
146
|
+
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
|
147
|
+
FOUNDATION
|
|
148
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
|
|
149
|
+
SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN
|
|
150
|
+
ACTION
|
|
151
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
|
|
152
|
+
INABILITY TO USE
|
|
153
|
+
THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
154
|
+
Except as contained in this notice, the names of GNOME, the GNOME
|
|
155
|
+
Foundation,
|
|
156
|
+
and Bitstream Inc., shall not be used in advertising or otherwise to promote
|
|
157
|
+
the sale, use or other dealings in this Font Software without prior written
|
|
158
|
+
authorization from the GNOME Foundation or Bitstream Inc., respectively.
|
|
159
|
+
For further information, contact: fonts at gnome dot org.
|
|
160
|
+
|
|
161
|
+
AMSFonts (v. 2.2) copyright
|
|
162
|
+
|
|
163
|
+
The PostScript Type 1 implementation of the AMSFonts produced by and
|
|
164
|
+
previously distributed by Blue Sky Research and Y&Y, Inc. are now freely
|
|
165
|
+
available for general use. This has been accomplished through the
|
|
166
|
+
cooperation
|
|
167
|
+
of a consortium of scientific publishers with Blue Sky Research and Y&Y.
|
|
168
|
+
Members of this consortium include:
|
|
169
|
+
|
|
170
|
+
Elsevier Science IBM Corporation Society for Industrial and Applied
|
|
171
|
+
Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS)
|
|
172
|
+
|
|
173
|
+
In order to assure the authenticity of these fonts, copyright will be
|
|
174
|
+
held by
|
|
175
|
+
the American Mathematical Society. This is not meant to restrict in any way
|
|
176
|
+
the legitimate use of the fonts, such as (but not limited to) electronic
|
|
177
|
+
distribution of documents containing these fonts, inclusion of these fonts
|
|
178
|
+
into other public domain or commercial font collections or computer
|
|
179
|
+
applications, use of the outline data to create derivative fonts and/or
|
|
180
|
+
faces, etc. However, the AMS does require that the AMS copyright notice be
|
|
181
|
+
removed from any derivative versions of the fonts which have been altered in
|
|
182
|
+
any way. In addition, to ensure the fidelity of TeX documents using Computer
|
|
183
|
+
Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
|
|
184
|
+
has requested that any alterations which yield different font metrics be
|
|
185
|
+
given a different name.
|
|
186
|
+
|
|
187
|
+
$Id$
|