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"
| container width | chosen font size |
|---|
{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) + 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 = "| total | sidebar | main | inspector |
|---|---|---|---|
| {total} | {s:.0f} | " + f"{m:.0f} | {i:.0f} |
{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" +]