Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/kiwisolver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# kiwisolver Examples

Each sub-directory contains a self-contained example. The order in
which the examples are to appear is specified in `order.json` (an
array of directory names in the expected order).

In each example directory you'll find:

* `config.toml` - must conform to the specification outlined here:
https://docs.pyscript.net/latest/user-guide/configuration/ This is
parsed and ultimately turned into a JSON representation as part of
the package's API object.
* `setup.py` - Python code for contextual and environmental setup,
NOT SEEN BY THE END USER, but is run before the `code.py` code is
evaluated. Allows us to create useful (IPython) shims, avoid
repeating boilerplate and whatnot.
* `code.py` - the actual code added to the editor which forms the
practical example of using the package.
82 changes: 82 additions & 0 deletions examples/kiwisolver/constraint_strengths/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ---------------------------------------------------------------------
# Constraint strengths: required, strong, medium, weak.
# ---------------------------------------------------------------------
#
# When constraints conflict, kiwi uses strengths to decide which
# ones win. REQUIRED constraints must hold; the others are honored
# in order of strength, with weaker ones giving way first.

import kiwisolver as kiwi


heading("Picking a font size that fits")
note(
"We want a heading to be 24pt, but it must also fit inside a "
"container of a given width without going below 10pt. The "
"preferred size is a soft wish; the bounds are hard rules."
)

font_size = kiwi.Variable("font_size")
container_width = kiwi.Variable("container_width")

# Roughly: each point of font size needs ~7 pixels of width for our
# label. So font_size * 7 must fit inside container_width.
solver = kiwi.Solver()
solver.addConstraint(font_size >= 10) # required
solver.addConstraint(font_size <= 72) # required
solver.addConstraint((font_size * 7) <= container_width) # required

# Soft preferences: ideally 24pt, but if we have to shrink, do so
# gently. STRONG beats WEAK when both can't be satisfied.
solver.addConstraint((font_size == 24) | "strong")
solver.addConstraint((font_size == 18) | "weak")

solver.addEditVariable(container_width, "strong")

note("Watch the chosen font size as the container shrinks:")

results = []
for w in [400, 250, 160, 100, 60]:
solver.suggestValue(container_width, w)
solver.updateVariables()
results.append((w, font_size.value()))

rows_html = "".join(
f"<tr><td>{w} px</td><td>{fs:.1f} pt</td></tr>"
for w, fs in results
)
display(HTML(
"<table border='1' cellpadding='6' style='border-collapse:collapse'>"
"<tr><th>container width</th><th>chosen font size</th></tr>"
f"{rows_html}</table>"
), append=True)

note(
"At 400 px the strong preference wins and we get 24 pt. As space "
"tightens, the required upper bound on width forces the size down, "
"but never below the required minimum of 10 pt."
)

heading("Custom strengths")
note(
"You can also build custom strengths from the three weight "
"components (strong, medium, weak). Higher numbers dominate."
)

# kiwi.strength.create(strong, medium, weak[, multiplier]) returns
# a numeric strength you can attach to a constraint with `|`.
prefer_even = kiwi.strength.create(0, 1, 0) # medium
prefer_round = kiwi.strength.create(0, 0, 5) # weak, but heavier weak

x = kiwi.Variable("x")
s2 = kiwi.Solver()
s2.addConstraint(x >= 0)
s2.addConstraint(x <= 100)
s2.addConstraint((x == 42) | prefer_even)
s2.addConstraint((x == 50) | prefer_round)
s2.updateVariables()

note(
f"With a medium preference for 42 and a weak preference for 50, "
f"the solver picks <strong>x = {x.value():.0f}</strong>."
)
1 change: 1 addition & 0 deletions examples/kiwisolver/constraint_strengths/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["kiwisolver"]
19 changes: 19 additions & 0 deletions examples/kiwisolver/constraint_strengths/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Set up names a notebook would already have from the previous cells."""
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
return _display(
*args, **kwargs, target=__pyscript_display_target__,
)


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
54 changes: 54 additions & 0 deletions examples/kiwisolver/constraints_intro/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
A first look at kiwisolver: laying out two boxes side by side.

Cassowary lets you describe relationships ("this is left of that",
"these two are the same width") and lets the solver figure out
concrete numbers that satisfy them. See:
https://kiwisolver.readthedocs.io/en/latest/basis/constraints_definition.html
"""
from IPython.core.display import display, HTML

# Kiwi is the Python binding to the Cassowary constraint solver.
# It's the same engine that powers many GUI layout systems.
import kiwisolver as kiwi

# Variables represent unknown numbers. The solver will assign values
# to them when we ask it to.
left_a = kiwi.Variable("left_a")
right_a = kiwi.Variable("right_a")
left_b = kiwi.Variable("left_b")
right_b = kiwi.Variable("right_b")

# Build a solver and feed it constraints. Constraints are written
# with normal Python operators: ==, <=, >=.
solver = kiwi.Solver()

# The container spans 0..300. Box A starts at the left edge.
solver.addConstraint(left_a == 0)

# Box B sits 20 pixels to the right of box A.
solver.addConstraint(left_b == right_a + 20)

# Both boxes are the same width.
solver.addConstraint((right_a - left_a) == (right_b - left_b))

# Box B's right edge is at 300 (the container's right edge).
solver.addConstraint(right_b == 300)

# Each box must be at least 50 wide. Mark this as STRONG so it
# only kicks in if it doesn't conflict with REQUIRED constraints.
solver.addConstraint(
((right_a - left_a) >= 50) | "strong"
)

# Ask the solver to find a solution and read the values back.
solver.updateVariables()

heading("Two boxes, equal width, with a 20px gap")
note("The solver computed these positions:")
display(HTML(
"<pre>"
f"box A: left = {left_a.value():.0f}, right = {right_a.value():.0f}\\n"
f"box B: left = {left_b.value():.0f}, right = {right_b.value():.0f}"
"</pre>"
), append=True)
1 change: 1 addition & 0 deletions examples/kiwisolver/constraints_intro/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["kiwisolver"]
42 changes: 42 additions & 0 deletions examples/kiwisolver/constraints_intro/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Shim IPython's display API onto PyScript so example code written in a
Jupyter/IPython idiom runs unmodified in the browser.
"""

import sys
import types
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
"""Wrap pyscript.display so output lands in the example target."""
return _display(
*args, **kwargs, target=__pyscript_display_target__,
)


ipython = types.ModuleType("IPython")
core = types.ModuleType("IPython.core")
core_display = types.ModuleType("IPython.core.display")
core_display.display = display
core_display.HTML = HTML
ipython.core = core
core.display = core_display
ipython.get_ipython = lambda: None
ipython.display = core_display
sys.modules["IPython"] = ipython
sys.modules["IPython.core"] = core
sys.modules["IPython.core.display"] = core_display
sys.modules["IPython.display"] = core_display


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

91 changes: 91 additions & 0 deletions examples/kiwisolver/edit_variables/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# ---------------------------------------------------------------------
# Edit variables: re-solving as one input changes.
# ---------------------------------------------------------------------
#
# A common pattern is to build the constraint system once and then
# nudge a single value (a window width, a slider, a mouse position)
# and let the solver propagate the change. That's what edit variables
# are for.
import kiwisolver as kiwi
import matplotlib.pyplot as plt


heading("A resizable three-pane layout")
note(
"Three side-by-side panes: a fixed 80px sidebar on the left, "
"a flexible main pane, and a 120px-minimum inspector on the right. "
"We'll vary the total width and watch the panes adjust."
)

sidebar_l = kiwi.Variable("sidebar_l")
sidebar_r = kiwi.Variable("sidebar_r")
main_l = kiwi.Variable("main_l")
main_r = kiwi.Variable("main_r")
inspect_l = kiwi.Variable("inspect_l")
inspect_r = kiwi.Variable("inspect_r")
total_width = kiwi.Variable("total_width")

solver = kiwi.Solver()

# Required structural constraints: panes meet edge to edge, starting
# at 0.
for c in [
sidebar_l == 0,
main_l == sidebar_r,
inspect_l == main_r,
inspect_r == total_width,
(sidebar_r - sidebar_l) == 80, # fixed sidebar width
(inspect_r - inspect_l) >= 120, # inspector minimum
(main_r - main_l) >= 100, # main pane minimum
]:
solver.addConstraint(c)

# Prefer the inspector to stay near 160 px (a soft preference).
solver.addConstraint(((inspect_r - inspect_l) == 160) | "weak")

# Register total_width as an edit variable so we can change it
# repeatedly without rebuilding the system.
solver.addEditVariable(total_width, "strong")

widths_to_try = [400, 600, 800, 1000, 1200]
rows = []
for w in widths_to_try:
solver.suggestValue(total_width, w)
solver.updateVariables()
rows.append((
w,
sidebar_r.value() - sidebar_l.value(),
main_r.value() - main_l.value(),
inspect_r.value() - inspect_l.value(),
))

# Show the numbers, then draw the layouts stacked on top of each other.
table = "<table border='1' cellpadding='4' style='border-collapse:collapse'>"
table += "<tr><th>total</th><th>sidebar</th><th>main</th><th>inspector</th></tr>"
for total, s, m, i in rows:
table += (
f"<tr><td>{total}</td><td>{s:.0f}</td>"
f"<td>{m:.0f}</td><td>{i:.0f}</td></tr>"
)
table += "</table>"
display(HTML(table), append=True)

fig, ax = plt.subplots(figsize=(9, 3.5))
colors = {"sidebar": "#88aaff", "main": "#ffd28a", "inspector": "#a8e6a3"}
for row_index, (total, s, m, i) in enumerate(rows):
y = row_index
ax.barh(y, s, left=0, color=colors["sidebar"], edgecolor="black")
ax.barh(y, m, left=s, color=colors["main"], edgecolor="black")
ax.barh(y, i, left=s + m, color=colors["inspector"], edgecolor="black")
ax.text(s / 2, y, "side", ha="center", va="center", fontsize=8)
ax.text(s + m / 2, y, "main", ha="center", va="center", fontsize=8)
ax.text(s + m + i / 2, y, "inspector",
ha="center", va="center", fontsize=8)

ax.set_yticks(range(len(rows)))
ax.set_yticklabels([f"{r[0]} px" for r in rows])
ax.set_xlabel("Pixels")
ax.set_title("Three-pane layout solved at five total widths")
ax.invert_yaxis()
fig.tight_layout()
display(fig, append=True)
1 change: 1 addition & 0 deletions examples/kiwisolver/edit_variables/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["kiwisolver", "matplotlib"]
20 changes: 20 additions & 0 deletions examples/kiwisolver/edit_variables/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Set up names a notebook would already have from the previous cell."""
import js
from pyscript import window, HTML, display as _display

js.alert = window.alert


def display(*args, **kwargs):
return _display(
*args, **kwargs, target=__pyscript_display_target__,
)


def heading(text, level=2):
display(HTML(f"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

5 changes: 5 additions & 0 deletions examples/kiwisolver/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"constraints_intro",
"edit_variables",
"constraint_strengths"
]