A draggable tile-based calculator built in Pythonista (iOS).
You build expressions by dragging and merging tiles on a grid—numbers, operators, fractions, roots, split tools, and substitution tiles.
- Tiles are the interface: drag to combine, drop to evaluate, split to decompose.
expr_stris the truth: tiles store a SymPy-safe expression string; labels are display-only.- Pretty rendering: output is formatted for humans (×, 𝑥, no
*, compact powers, √).
- Drag tiles from the top spawners into the board.
- Drop one tile onto another to trigger a merge (depending on types).
- Special tools:
- Split (✂): decomposes fractions and surds (and other supported forms).
- Root (√): defaults to square root when applied; can be bound to nth root by dropping a number onto it.
- Substitution (x=): bind a value (x=3), then apply to expressions and fractions.
This project uses "drop to compute" rather than a classic equals button.
- Results appear when merges resolve an expression.
- (TODO: verify) The equals tile may function as a merge trigger rather than a press-to-render.
- Open the script in Pythonista and run.
- Recommended: iPad for space, but works on iPhone.
- Pythonista view colors must be string hex or RGBA tuple.
- Display “𝑥” is visual-only; internal math uses plain
x.
See:
docs/ARCHITECTURE.mddocs/TILE_TYPES.mddocs/BEHAVIOR_SPEC.mddocs/PATCH_WORKFLOW.md
This project is best understood as a small UI + language engine:
- UI layer: tiles are
ui.Viewobjects with drag/drop. - Math layer: internal expressions use SymPy-friendly strings (
expr_str). - Routing layer: merge logic is centralized in the board’s drop handler.
- Board: owns tiles, resolves drops, spawns results, and applies “tool” tiles.
- Tile: base tile view (numbers, operators, expressions, results, tools).
- FractionTile: specialized tile with numerator/denominator labels and fraction-aware behavior.
- Spawner: top-row “tile factories” (digits, operators, tools).
touch_began: store start statetouch_moved: move tile, highlight/preview overlaps (optional)touch_ended: finalize -> callsBoard.finalize_drop(tile)(or equivalent)
Drop routing is performed by a central method:
Board.finalize_drop(tile)
It routes based on tile.kind and overlap target:
split/supersplit: split logic first, fallback to generic splitroot: root binding or evaluationsubst: bind x=value or apply substitution- fractions/expr/number merges: expression building + evaluation
- result spawning: centralized in
_spawn_eval_result(val, x, y)(or equivalent)
All “computed results” should flow through:
Board._spawn_eval_result(val, x, y)
Important behaviour:
- If SymPy result has denominator ≠ 1, spawn a FractionTile (even if it contains symbols).
- Otherwise spawn a normal result tile or expr tile.
Rules:
expr_strstays SymPy-safe (plainx,*,**etc.)- labels are display-only and are “pretty printed”
pretty_x_text()ensures math-x appears as 𝑥 in UI_sympy_pretty_label(expr)removes*for humans, uses √, powers, π etc.
- Expression tiles widen based on measured display width.
- Fraction tiles widen based on max(display width of numerator/denominator), capped at 2-wide currently.
- Always normalize color inputs (Pythonista expects tuple/string).
- Avoid setting
label.textdirectly; preferTile.set_label()so 𝑥 + sizing rules apply.
Tiles have a kind plus a few key fields.
The most important invariant: expr_str is canonical (SymPy-safe), while labels are for display.
kind: tile category (string)expr_str: internal expression string (SymPy-friendly)label_text: raw label text (often same as expr_str but not always)tile_color: base color;background_coloruses normalized form
Represents a plain integer (and sometimes generic numeric).
expr_str:"7"- Display:
"7" - Color: number color
- Merge: with operators, fractions, roots, etc.
Represents a binary operator (e.g. +, −, ×, ÷).
expr_str:"+"or"*"or"/"- Display: uses × and ÷ for * and /.
- Merge: combines two tiles into a new expression/result.
Important UI rule
- The visible ÷ spawner currently defaults to spawning an empty fraction tile.
- “Division operator” is available as the alternative form.
Represents symbolic expressions.
expr_str: SymPy-safe, e.g."x**2 + 3*x + 1"- Display: pretty-printed, shows 𝑥, ×, removes
*in simple products.
Represents evaluated output (integer/decimal).
expr_str: typically numeric string- Color: result color (int/dec variants)
Represents numerator/denominator with fraction-aware display.
numer: raw numerator string or intdenom: raw denominator string or intexpr_str:"(<numer>)/(<denom>)"when complete- Display: numerator/denominator rendered via SymPy pretty printing
- Color:
- plain fractions: fraction color
- if numer/denom contain x: expression color (EXPR_HEX)
Sizing
- 1-wide unless display text requires widening
- 2-wide (2x1) when numerator or denominator display text exceeds one-tile width
Represents √ or nth root operator.
- Unbound root behaves like sqrt when applied to a target.
- Binding: drop number 2–9 onto root to convert to ²√, ³√, etc.
- Applying: root dropped onto target defaults to sqrt of target.
Tool tile used to decompose supported forms. Special supported splits:
- fractions: pop denominator first, then numerator
- surds: split a*sqrt(b) into [a] [sqrt(b)]
- sqrt(b) into [√] [b] (TODO: list any additional supported split targets)
Substitution tile (x=).
- Unbound: displays “x=” and becomes bound when merged with a number/result
- Bound: stores
subst_val(SymPy value) and can be applied to expr/fraction tiles
This doc describes intended behaviour. Implementation may evolve, but behaviour should stay consistent.
- Display uses:
×instead of*÷for division operator display𝑥for variable x (display only)- compact powers (² ³ …) where supported
- √ where supported
- Internal math (
expr_str) always uses plainxand SymPy syntax.
- Default “÷” spawner creates an empty fraction tile.
- Alternative form provides division operator behaviour.
If a merge/evaluation yields a SymPy expression with denominator ≠ 1:
- spawn a fraction tile result (even if expression contains x)
- Numerator/denominator are displayed with SymPy pretty rendering.
- Fraction widens to 2-wide only if the displayed text does not fit 1-wide.
- If numerator or denominator contains x → fraction uses expression color (EXPR_HEX).
- Otherwise uses normal fraction color.
- Split on a fraction with both parts present:
- first split pops denominator into a new tile
- second split pops numerator
- Spawned tiles:
- numeric -> number tile
- symbolic -> expr tile
- Fraction tile retains remaining part until fully emptied.
- Split on
a*sqrt(b)yields:[a]and[sqrt(b)]
- Split on
sqrt(b)yields:[√]and[b]
- If coefficient is 1, do not create a useless
1tile. (TODO: verify current behaviour for coeff=1)
- If root tile is dragged onto a target and is unbound: treat as square root of target.
- If a number is dragged onto an unbound root:
- if number is 2–9: bind root degree (²√, ³√, …)
- else: apply sqrt to that number
- Drag unbound x= onto a number/result -> becomes x=
- Stores the bound SymPy value in
subst_val.
- Drag bound x=value onto expr tile -> spawns evaluated result tile
- Original expression may remain (non-destructive) depending on design (TODO: confirm).
- Drag bound x=value onto fraction with (numer, denom):
- substitute into numerator and denominator separately
- simplify
- spawn result via the same result spawning rules (fraction if denom ≠ 1) Example:
- x=3 on x^2/4 -> 9/4 as a fraction tile
This project is maintained via a patching tool that applies targeted REPLACE/INSERT edits. The goal is to keep changes surgical and avoid destabilizing drag/merge flows.
- Never break the drop router (
Board.finalize_dropor equivalent). expr_stris canonical. Do not put 𝑥 into expr_str.- All display text should go through
set_label()or pretty rendering. - Normalize colors before assigning to
background_color.
- Drop routing:
finalize_drop - Result creation:
_spawn_eval_result - Pretty printing:
_sympy_pretty_label+pretty_x_text - Fraction display:
FractionTile._refresh_display - Any code that assigns
label.text = ...directly
After any patch that touches merge/split/root/subst:
- Merge number + op + number -> correct result spawns
- Multiply two fractions -> fraction result spawns
- Subst x=3 on expr -> evaluated result spawns
- Subst x=3 on fraction -> fraction result spawns
- Split on fraction pops denom then numer
- Split on a*sqrt(b) -> [a] [sqrt(b)]
- Drag unbound root onto target -> sqrt(target)
- Drag 3 onto root -> cube root operator