// Run: make test-cpp // The rect and hit assertions from test/ui_layout.lua, against the C++ node arena. // Panel and font match test/fake_device.lua so the numbers are the same ones. #include "../src/ui/layout.h" #include #include #include using namespace ui; static const int PANEL_W = 320, PANEL_H = 480; static const int CHAR_W = 6, FONT_H = 8; static int failures = 0; static void check(const char* what, const std::string& got, const std::string& want) { if (got == want) return; printf("\n %s: got %s, want %s", what, got.c_str(), want.c_str()); failures++; } static void checkTrue(const char* what, bool ok) { if (ok) return; printf("\n %s", what); failures++; } static std::string rect(const Tree& tree, uint16_t id) { const Node& n = tree.nodes[id]; char buffer[64]; snprintf(buffer, sizeof(buffer), "%d,%d %dx%d", n.x, n.y, n.w, n.h); return buffer; } // Builders ----------------------------------------------------------------- static Spec boxSpec() { return Spec(); } static Spec textSpec(const char* label) { Spec spec; spec.intrinsicW = static_cast(strlen(label) * CHAR_W); spec.intrinsicH = FONT_H; return spec; } static uint16_t addText(Tree& tree, uint16_t parent, const char* label) { uint16_t id = tree.add(parent, textSpec(label), TEXT); tree.setLabel(id, label); return id; } // Mirrors ui.button: pad 8 all round, contents centred on the cross axis. static uint16_t addButton(Tree& tree, uint16_t parent, const char* label, bool interactive = true) { Spec spec; spec.padT = spec.padR = spec.padB = spec.padL = 8; spec.align = CENTER; uint16_t id = tree.add(parent, spec, BUTTON, interactive ? INTERACTIVE : 0); addText(tree, id, label); return id; } // Mirrors ui.screen: the root is the panel, so it must measure that way too. static uint16_t addRoot(Tree& tree, Spec spec = Spec()) { spec.w = Size::fill(); spec.h = Size::fill(); return tree.add(NONE, spec); } static bool run(Tree& tree, uint16_t root) { return tree.layout(root, 0, 0, PANEL_W, PANEL_H); } // Cases -------------------------------------------------------------------- static void flowStacksChildren() { Tree tree; Spec spec; spec.padT = spec.padR = spec.padB = spec.padL = 12; spec.gap = 8; uint16_t root = addRoot(tree, spec); uint16_t title = addText(tree, root, "settings"); uint16_t first = addButton(tree, root, "one"); uint16_t second = addButton(tree, root, "two"); checkTrue("flow laid out", run(tree, root)); check("title", rect(tree, title), "12,12 296x8"); // Button height is text plus its own padding: 8 + 8 + 8 = 24. check("first button", rect(tree, first), "12,28 296x24"); check("second button", rect(tree, second), "12,60 296x24"); } static void fractionsResolveAgainstContent() { Tree tree; Spec spec; spec.padT = spec.padR = spec.padB = spec.padL = 10; uint16_t root = addRoot(tree, spec); Spec halfSpec = boxSpec(); halfSpec.w = Size::fraction(500); halfSpec.h = Size::px(40); uint16_t half = tree.add(root, halfSpec); Spec fixedSpec = boxSpec(); fixedSpec.w = Size::px(100); fixedSpec.h = Size::px(40); uint16_t fixed = tree.add(root, fixedSpec); checkTrue("fractions laid out", run(tree, root)); check("half", rect(tree, half), "10,10 150x40"); check("fixed", rect(tree, fixed), "10,50 100x40"); } static void rowCentresOnCrossAxis() { Tree tree; uint16_t root = addRoot(tree); Spec rowSpec; rowSpec.gap = 6; rowSpec.align = CENTER; rowSpec.h = Size::px(40); uint16_t row = tree.add(root, rowSpec, BOX, ROW); Spec tallSpec; tallSpec.w = Size::px(40); tallSpec.h = Size::px(40); uint16_t tall = tree.add(row, tallSpec); Spec shortSpec; shortSpec.w = Size::px(40); shortSpec.h = Size::px(10); uint16_t shortBox = tree.add(row, shortSpec); checkTrue("row laid out", run(tree, root)); check("tall", rect(tree, tall), "0,0 40x40"); check("short", rect(tree, shortBox), "46,15 40x10"); } // justify distributes the main axis: "between" pushes the last child to the far edge, // which is how a header keeps a title left and a clock right without arithmetic. static void justifyBetweenSpreads() { Tree tree; Spec rootSpec; rootSpec.padT = rootSpec.padR = rootSpec.padB = rootSpec.padL = 10; uint16_t root = addRoot(tree, rootSpec); Spec rowSpec; rowSpec.justify = BETWEEN; uint16_t row = tree.add(root, rowSpec, BOX, ROW); Spec leftSpec; leftSpec.w = Size::px(40); leftSpec.h = Size::px(10); uint16_t left = tree.add(row, leftSpec); Spec rightSpec; rightSpec.w = Size::px(60); rightSpec.h = Size::px(10); uint16_t right = tree.add(row, rightSpec); checkTrue("between laid out", run(tree, root)); check("left", rect(tree, left), "10,10 40x10"); check("right", rect(tree, right), "250,10 60x10"); } // The other modes shift the whole run rather than spreading it. static void justifyEndShiftsRun() { Tree tree; uint16_t root = addRoot(tree); Spec rowSpec; rowSpec.gap = 10; rowSpec.justify = END; uint16_t row = tree.add(root, rowSpec, BOX, ROW); Spec aSpec; aSpec.w = Size::px(40); aSpec.h = Size::px(10); uint16_t a = tree.add(row, aSpec); Spec bSpec; bSpec.w = Size::px(60); bSpec.h = Size::px(10); uint16_t b = tree.add(row, bSpec); checkTrue("end laid out", run(tree, root)); check("a", rect(tree, a), "210,0 40x10"); check("b", rect(tree, b), "260,0 60x10"); } // One child cannot be spread against anything, so "between" degrades to "start". static void betweenWithOneChild() { Tree tree; uint16_t root = addRoot(tree); Spec rowSpec; rowSpec.justify = BETWEEN; uint16_t row = tree.add(root, rowSpec, BOX, ROW); Spec onlySpec; onlySpec.w = Size::px(40); onlySpec.h = Size::px(10); uint16_t only = tree.add(row, onlySpec); checkTrue("lone child laid out", run(tree, root)); check("only", rect(tree, only), "0,0 40x10"); } // The root always fills the screen, so alignment there spans the whole panel. static void alignCentresOnTheRoot() { Tree tree; Spec rootSpec; rootSpec.align = CENTER; uint16_t root = addRoot(tree, rootSpec); Spec loneSpec; loneSpec.w = Size::px(40); loneSpec.h = Size::px(40); uint16_t lone = tree.add(root, loneSpec); checkTrue("centred root laid out", run(tree, root)); check("lone", rect(tree, lone), "140,0 40x40"); } // A fraction inside an auto-sized parent is an error, not a silent zero. static void fractionInsideAutoParentFails() { Tree tree; uint16_t root = addRoot(tree); uint16_t autoBox = tree.add(root, boxSpec()); Spec childSpec; childSpec.w = Size::px(20); childSpec.h = Size::fraction(500); tree.add(autoBox, childSpec); checkTrue("fraction inside an auto parent must fail loudly", !run(tree, root)); } // Hit testing: deepest interactive node wins over its tappable ancestor. static void hitPrefersTheDeepestNode() { Tree tree; Spec outerSpec; outerSpec.padT = outerSpec.padR = outerSpec.padB = outerSpec.padL = 20; outerSpec.w = Size::fill(); outerSpec.h = Size::fill(); uint16_t outer = tree.add(NONE, outerSpec, BOX, INTERACTIVE); uint16_t inner = addButton(tree, outer, "inner"); checkTrue("hit tree laid out", run(tree, outer)); checkTrue("inner button should win inside its rect", tree.hit(outer, 160, 30) == inner); checkTrue("the container claims its own padding", tree.hit(outer, 2, 2) == outer); checkTrue("outside the tree is a miss", tree.hit(outer, -50, -50) == NONE); } // A dialog is an ordinary node the app includes, placed absolutely so it covers the flow // and swallows the taps that would otherwise reach what is underneath it. static void dialogCoversAndCaptures() { Tree tree; uint16_t root = addRoot(tree); Spec behindSpec; behindSpec.padT = behindSpec.padR = behindSpec.padB = behindSpec.padL = 12; uint16_t behind = tree.add(root, behindSpec); uint16_t buried = addButton(tree, behind, "do not press"); Spec layerSpec; layerSpec.absolute = true; layerSpec.atX = Size::px(0); layerSpec.atY = Size::px(0); layerSpec.w = Size::fill(); layerSpec.h = Size::fill(); layerSpec.align = CENTER; layerSpec.justify = CENTER; uint16_t layer = tree.add(root, layerSpec, BOX, CAPTURE); Spec cardSpec; cardSpec.w = Size::fraction(850); cardSpec.padT = cardSpec.padR = cardSpec.padB = cardSpec.padL = 16; cardSpec.gap = 12; uint16_t card = tree.add(layer, cardSpec); addText(tree, card, "forget network?"); Spec buttonsSpec; buttonsSpec.gap = 8; buttonsSpec.justify = END; uint16_t buttons = tree.add(card, buttonsSpec, BOX, ROW); addButton(tree, buttons, "cancel"); uint16_t confirm = addButton(tree, buttons, "forget"); checkTrue("dialog laid out", run(tree, root)); check("layer", rect(tree, layer), "0,0 320x480"); const Node& cardNode = tree.nodes[card]; checkTrue("card is 0.85 of the panel", cardNode.w == 272); checkTrue("card is centred horizontally", cardNode.x == 24); checkTrue("card is centred vertically", cardNode.y == (PANEL_H - cardNode.h) / 2); checkTrue("the dialog was tapped through", tree.hit(root, 60, 24) == layer); checkTrue("the dialog's own buttons still work", tree.hit(root, tree.nodes[confirm].x + 4, tree.nodes[confirm].y + 4) == confirm); (void)buried; } // Labels share one arena. Overwriting in place matters because the clock repaints every // second at a fixed width, and an append per tick would grow without bound. static void labelsShareOneArena() { Tree tree; uint16_t root = addRoot(tree); uint16_t clock = addText(tree, root, "12:00:00"); uint16_t other = addText(tree, root, "wifi"); size_t before = tree.footprint(); for (int i = 0; i < 100; i++) tree.setLabel(clock, "12:00:01"); checkTrue("a same-width label must not grow the arena", tree.footprint() == before); check("clock label", tree.label(clock), "12:00:01"); check("neighbour label", tree.label(other), "wifi"); tree.setLabel(clock, "a much longer label"); check("grown label", tree.label(clock), "a much longer label"); check("neighbour survived the growth", tree.label(other), "wifi"); checkTrue("a node with no label reports none", tree.label(root) == nullptr); } // Styling is inheritance rather than copies: the root carries the palette and a node // that names nothing costs nothing, which is what keeps the arena at 16 bytes a node. static void styleInheritsFromTheNearestAncestor() { Tree tree; uint16_t root = addRoot(tree); uint16_t middle = tree.add(root, boxSpec()); uint16_t leaf = tree.add(middle, boxSpec()); Style& palette = tree.styleFor(root); palette.fg = 0x0001; palette.bg = 0x0002; palette.set = S_FG | S_BG; Style& override_ = tree.styleFor(leaf); override_.fg = 0x0003; override_.set = S_FG; checkTrue("a leaf inherits what it does not state", tree.inherited(leaf, S_BG).bg == 0x0002); checkTrue("a leaf keeps what it does state", tree.inherited(leaf, S_FG).fg == 0x0003); checkTrue("an unstyled node inherits both", tree.inherited(middle, S_FG).fg == 0x0001); checkTrue("an unset role falls back rather than guessing", tree.inherited(leaf, S_BORDER).border == Style().border); checkTrue("only styled nodes cost anything", tree.styleOf(middle) == nullptr); } // The point of the split arena: what a screen costs once its layout pass is over. static void reportFootprint() { Tree tree; uint16_t root = addRoot(tree); for (int i = 0; i < 200; i++) addButton(tree, root, "item"); size_t built = tree.nodes.size(); checkTrue("footprint tree laid out", run(tree, root)); size_t scratch = built * sizeof(Spec); tree.dropScratch(); size_t persistent = tree.footprint(); printf("\n sizeof(Node)=%zu sizeof(Spec)=%zu", sizeof(Node), sizeof(Spec)); printf("\n %zu nodes: %zu B steady, %zu B peak (+%zu B scratch)", built, persistent, persistent + scratch, scratch); checkTrue("a Node must stay at 16 bytes", sizeof(Node) == 16); } int main() { flowStacksChildren(); fractionsResolveAgainstContent(); rowCentresOnCrossAxis(); justifyBetweenSpreads(); justifyEndShiftsRun(); betweenWithOneChild(); alignCentresOnTheRoot(); fractionInsideAutoParentFails(); hitPrefersTheDeepestNode(); dialogCoversAndCaptures(); labelsShareOneArena(); styleInheritsFromTheNearestAncestor(); reportFootprint(); if (failures) { printf("\n%d failure(s)\n", failures); return 1; } printf("\n ok\n"); return 0; }