Skip to content

Commit b54de2f

Browse files
committed
Create PyScript UIs in Python
1 parent 583dda1 commit b54de2f

File tree

11 files changed

+899
-0
lines changed

11 files changed

+899
-0
lines changed

README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,114 @@
11
# ltk
22
LTK is a little toolkit for writing UIs in PyScript
3+
4+
LTK is implemented as a declarative Python library and leverages `jQuery` for DOM operations.
5+
6+
## Widget Specification
7+
8+
New widget types are created by symply subclassing `ltk.Widget`:
9+
10+
```python
11+
class HBox(Widget):
12+
classes = [ "ltk-hbox" ]
13+
```
14+
15+
By default, widgets are created as `div` DOM elements. You can choose a different tag:
16+
17+
```python
18+
class Preformatted(Widget):
19+
classes = [ "ltk-pre" ]
20+
tag = "pre"
21+
```
22+
23+
## Creating a UI
24+
25+
To create a UI, elements are constructed declaratively:
26+
27+
```python
28+
ltk.Table(
29+
ltk.TableRow(
30+
ltk.TableHeader("header1")
31+
ltk.TableHeader("header2")
32+
),
33+
[
34+
ltk.TableRow(
35+
ltk.TableData(value1),
36+
ltk.TableData(value2),
37+
)
38+
for value1, value2 in data
39+
],
40+
)
41+
```
42+
43+
Widgets are added to others by using jQuery's `append` and `appendTo` calls:
44+
```python
45+
ltk.body.append(
46+
ltk.Table(...)
47+
)
48+
49+
container = ltk.VBox(...)
50+
ltk.H1("This is a header").appendTo(container)
51+
```
52+
53+
## Styling
54+
55+
Widgets can be styled using element styles:
56+
```python
57+
ltk.Text("Some text")
58+
.css("background-color", "red")
59+
.css("color", "white")
60+
.css("padding", 8)
61+
```
62+
63+
Widgets can also be styled using an external stylesheet:
64+
```python
65+
ltk.Text("Some text").addClass("my-special-text)
66+
```
67+
68+
```css
69+
.ltk-text {
70+
font-family: Arial;
71+
}
72+
73+
.my-special-text {
74+
font-family: Courier;
75+
background-color: red;
76+
color: white;
77+
padding: 8px;
78+
}
79+
```
80+
81+
## Events
82+
83+
Event handlers are attached using jQuery mechanisms. As the functions cross PyOdide and JavaScript namespaces, they need to be wrapped with `pyodide.ffi.create_proxy` calls. We use the shortcut offered by `ltk.proxy`:
84+
```python
85+
def buy(event):
86+
purchase(...)
87+
88+
Card("Buy Now").on("click", ltk.proxy(buy))
89+
```
90+
91+
## Examples
92+
93+
See the `LTK Kitchen Sink` or explore the `examples` folder
94+
95+
96+
## License
97+
98+
LTK is covered under the Apache License:
99+
100+
- The Apache license is a permissive open source software license.
101+
102+
- It allows users to freely use, modify, and distribute the software (including for commercial purposes).
103+
104+
- Modified versions can be distributed without having to release the source code. Though source code changes should be documented.
105+
106+
- The license requires modified versions to retain the Apache license and copyright notice.
107+
108+
- The software is provided by the authors "as is" with no warranties.
109+
110+
- Users are not granted patent rights by contributors, but contributors cannot revoke patent grants for previous contributions.
111+
112+
- The license does not require derived works to adopt the Apache license. Though this is encouraged for consistency.
113+
114+

examples/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from examples import custom
2+
from examples import table
3+
from examples import tictactoe
4+
5+
elements = [
6+
tictactoe.create(),
7+
table.create(),
8+
custom.create(),
9+
]

examples/custom.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import ltk
2+
3+
# Create a new widget, based on VBox
4+
class ImageWithLabel(ltk.VBox):
5+
classes = [ "custom" ]
6+
7+
def __init__(self, src, label):
8+
ltk.VBox.__init__(self,
9+
# Add two widgets. Styling could be done in external CSS
10+
ltk.Image(src)
11+
# CSS selector would be ".custom .ltk-image"
12+
.css("width", 200),
13+
ltk.Text(label)
14+
# CSS selector would be ".custom .ltk-text"
15+
.css("width", "100%")
16+
.css("text-align", "center")
17+
)
18+
19+
20+
def create():
21+
# Create the custom widget and add an orange border for clarity
22+
custom_widget = ImageWithLabel(
23+
"https://chrislaffra.com/chris.png",
24+
"Chris laffra"
25+
).css("border", "2px solid orange")
26+
27+
return ltk.VBox(
28+
ltk.H2("Showing a Card with a custom widget inside of it"),
29+
ltk.Card(custom_widget)
30+
.css("width", 200)
31+
.draggable(),
32+
ltk.Text("For clarity, we marked the custom widget orange.")
33+
.css("margin-top", 20),
34+
ltk.H4("Tip: drag the card."),
35+
).attr("name", "Custom")

examples/table.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import ltk
2+
3+
# Create an HTML table to display country temperature data
4+
# Use LTK widgets to generate the table structure
5+
6+
def create():
7+
# In this example we construct the UI by iteratively appending to a container.
8+
# Elements are added to the container using jQuery "append" and "appendTo" calls.
9+
# The table itself is created with a more declarative creation pattern.
10+
container = ltk.Container().attr("name", "HTML Tables")
11+
12+
# DOM elements can be created using ltk objects, or directly with HTML.
13+
container.append(
14+
ltk.create("""
15+
<h1>
16+
HTML Table created in Python
17+
</h1>
18+
""")
19+
)
20+
21+
# Resize options to be used for the first colum. We pass a Python dict
22+
# to JavaScript and convert it to a map using the "to_js" function.
23+
resize_options = ltk.to_js({
24+
"handles": "e",
25+
"alsoResize": ".country",
26+
})
27+
28+
ltk.Table(
29+
# Create a table with headers for country and temperature.
30+
ltk.TableRow(
31+
ltk.TableHeader("Country")
32+
.css("border-right", "2px solid orange")
33+
.resizable(resize_options),
34+
ltk.TableHeader("Yearly Average Temperature")
35+
),
36+
# Populate the table rows with country and temperature data.
37+
[
38+
ltk.TableRow(
39+
ltk.TableData(country).addClass("country"),
40+
ltk.TableData(temperature),
41+
)
42+
for country, temperature in get_averages().items()
43+
],
44+
).appendTo(container)
45+
46+
container.append(ltk.H4("Tip: resize the country column using the orange handle."))
47+
48+
return container
49+
50+
51+
def get_averages():
52+
return {
53+
"Afghanistan": 12.6,
54+
"Albania": 11.4,
55+
"Algeria": 22.5,
56+
"Andorra": 7.6,
57+
"Angola": 21.55,
58+
"Antigua and Barbuda": 26,
59+
"Argentina": 14.8,
60+
"Armenia": 7.15,
61+
"Australia": 21.65,
62+
}

examples/tictactoe.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.square {
2+
width: 70px;
3+
height: 70px;
4+
border: 1px solid gray;
5+
cursor: pointer;
6+
}
7+
8+
.choice {
9+
font-family: "Brush Script MT";
10+
font-size: 42px;
11+
font-family: Brush Script MT;
12+
text-align: center;
13+
margin: auto;
14+
width: 90%;
15+
height: 90%;
16+
}

examples/tictactoe.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ltk
2+
3+
# Tic Tac Toe Game
4+
#
5+
# This implements a simple Tic Tac Toe game in PyScript using the LTK toolkit.
6+
# It draws a Tic Tac Toe board that allows clicking squares to place X and O marks alternately.
7+
#
8+
# The UI uses LTK widget classes:
9+
# - Container for each square
10+
# - VBox and HBox to create the grid
11+
# - Text for the X and O labels
12+
#
13+
# This provides an interactive grid layout without needing much explicit layout code.
14+
15+
def choose(event):
16+
#
17+
# This event handler is attached to each square container:
18+
# - The handler checks if the square is empty
19+
# - If empty, it adds an X or O text label based on whose turn it is
20+
#
21+
square = ltk.find(event.target)
22+
if not square.text():
23+
square.append(
24+
ltk.Text("X" if ltk.find(".choice").length % 2 else "O")
25+
.addClass("choice")
26+
)
27+
28+
29+
def enter(event):
30+
# First erase all existing squares
31+
ltk.find(".square").css("background", "white")
32+
33+
# Highlight the current square, but only if does not contain a choice already
34+
square = ltk.jQuery(event.target)
35+
if not square.text():
36+
square.css("background", "lightblue")
37+
38+
39+
def create():
40+
# Set up the game board:
41+
# - Create a 3x3 grid of Container widgets to represent the board squares
42+
# - Attach a click handler to each square container
43+
# - Add the grid containers into a nested VBox/HBox layout
44+
# - Render the layout into the #example div
45+
return ltk.Div(
46+
ltk.H2("Tic Tac Toe Game"),
47+
ltk.VBox(
48+
ltk.HBox(
49+
ltk.Container()
50+
.addClass("square")
51+
.on("click", ltk.proxy(choose))
52+
.on("mouseenter", ltk.proxy(enter))
53+
for column in range(3)
54+
)
55+
for row in range(3)
56+
),
57+
ltk.H4("Tip: Click inside the squares."),
58+
).attr("name", "Tic Tac Toe")

index.html

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>LTK</title>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width,initial-scale=1">
7+
8+
<!-- Import PyScript CSS and JS -->
9+
<link rel="stylesheet" href="https://pyscript.net/releases/2023.11.1/core.css" />
10+
<script defer type="module" src="https://pyscript.net/releases/2023.11.1/core.js"></script>
11+
12+
<!-- Import LTK and jQuery -->
13+
<link rel="stylesheet" href="ltk/ltk.css" />
14+
<script src="https://code.jquery.com/jquery-3.6.0.js"></script>
15+
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.js"></script>
16+
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
17+
18+
<!-- Import custom CSS for our examples -->
19+
<link rel="stylesheet" href="examples/tictactoe.css" />
20+
</head>
21+
<body>
22+
<h1>The PyScript LTK Kitchen Sink</h1>
23+
24+
<!-- Include LTK itself and its examples -->
25+
<py-config>
26+
[[fetch]]
27+
files = [
28+
"ltk/__init__.py",
29+
"ltk/jquery.py",
30+
"ltk/widgets.py",
31+
"examples/__init__.py",
32+
"examples/tictactoe.py",
33+
"examples/custom.py",
34+
"examples/table.py",
35+
]
36+
</py-config>
37+
38+
<!-- Create all the examples and add them to the DOM -->
39+
<py-script>
40+
import ltk, examples
41+
ltk.body.append(
42+
ltk.Tabs(examples.elements)
43+
)
44+
</py-script>
45+
</body>
46+
</html>

0 commit comments

Comments
 (0)