diff --git a/examples/kiwisolver/README.md b/examples/kiwisolver/README.md new file mode 100644 index 0000000..99e48ae --- /dev/null +++ b/examples/kiwisolver/README.md @@ -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. diff --git a/examples/kiwisolver/constraint_strengths/code.py b/examples/kiwisolver/constraint_strengths/code.py new file mode 100644 index 0000000..85ae83a --- /dev/null +++ b/examples/kiwisolver/constraint_strengths/code.py @@ -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"{w} px{fs:.1f} pt" + for w, fs in results +) +display(HTML( + "" + "" + f"{rows_html}
container widthchosen font size
" +), 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 x = {x.value():.0f}." +) diff --git a/examples/kiwisolver/constraint_strengths/config.toml b/examples/kiwisolver/constraint_strengths/config.toml new file mode 100644 index 0000000..92f4cfe --- /dev/null +++ b/examples/kiwisolver/constraint_strengths/config.toml @@ -0,0 +1 @@ +packages = ["kiwisolver"] diff --git a/examples/kiwisolver/constraint_strengths/setup.py b/examples/kiwisolver/constraint_strengths/setup.py new file mode 100644 index 0000000..3a206fc --- /dev/null +++ b/examples/kiwisolver/constraint_strengths/setup.py @@ -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"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/kiwisolver/constraints_intro/code.py b/examples/kiwisolver/constraints_intro/code.py new file mode 100644 index 0000000..6cafbb5 --- /dev/null +++ b/examples/kiwisolver/constraints_intro/code.py @@ -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( + "
"
+    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}"
+    "
" +), append=True) diff --git a/examples/kiwisolver/constraints_intro/config.toml b/examples/kiwisolver/constraints_intro/config.toml new file mode 100644 index 0000000..92f4cfe --- /dev/null +++ b/examples/kiwisolver/constraints_intro/config.toml @@ -0,0 +1 @@ +packages = ["kiwisolver"] diff --git a/examples/kiwisolver/constraints_intro/setup.py b/examples/kiwisolver/constraints_intro/setup.py new file mode 100644 index 0000000..07879f9 --- /dev/null +++ b/examples/kiwisolver/constraints_intro/setup.py @@ -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"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/kiwisolver/edit_variables/code.py b/examples/kiwisolver/edit_variables/code.py new file mode 100644 index 0000000..c03219f --- /dev/null +++ b/examples/kiwisolver/edit_variables/code.py @@ -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 += "" +for total, s, m, i in rows: + table += ( + f"" + f"" + ) +table += "
totalsidebarmaininspector
{total}{s:.0f}{m:.0f}{i:.0f}
" +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) diff --git a/examples/kiwisolver/edit_variables/config.toml b/examples/kiwisolver/edit_variables/config.toml new file mode 100644 index 0000000..7ad60b0 --- /dev/null +++ b/examples/kiwisolver/edit_variables/config.toml @@ -0,0 +1 @@ +packages = ["kiwisolver", "matplotlib"] diff --git a/examples/kiwisolver/edit_variables/setup.py b/examples/kiwisolver/edit_variables/setup.py new file mode 100644 index 0000000..87590b6 --- /dev/null +++ b/examples/kiwisolver/edit_variables/setup.py @@ -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"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + diff --git a/examples/kiwisolver/order.json b/examples/kiwisolver/order.json new file mode 100644 index 0000000..a1b4d4c --- /dev/null +++ b/examples/kiwisolver/order.json @@ -0,0 +1,5 @@ +[ + "constraints_intro", + "edit_variables", + "constraint_strengths" +]