From 453eaa03fc9288f2756fd346286e664e865ff1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 29 May 2026 15:48:23 -0300 Subject: [PATCH] Several flex fixes. --- .../assets/html/flex_mediaqueries.html | 774 ++++++++++++++++++ .../about.ZLlEu_QV.css | 732 +++++++++++++++++ .../assets/html/flex_mediaquery.html | 71 ++ src/eepp/ui/css/mediaquery.cpp | 5 + src/eepp/ui/flexlayouter.cpp | 26 +- src/eepp/ui/uilayoutermanager.cpp | 3 + src/eepp/ui/uitextspan.cpp | 15 +- src/tests/unit_tests/uihtml_tests.cpp | 141 ++++ 8 files changed, 1761 insertions(+), 6 deletions(-) create mode 100644 bin/unit_tests/assets/html/flex_mediaqueries.html create mode 100644 bin/unit_tests/assets/html/flex_mediaqueries_files/about.ZLlEu_QV.css create mode 100644 bin/unit_tests/assets/html/flex_mediaquery.html diff --git a/bin/unit_tests/assets/html/flex_mediaqueries.html b/bin/unit_tests/assets/html/flex_mediaqueries.html new file mode 100644 index 000000000..aab320cac --- /dev/null +++ b/bin/unit_tests/assets/html/flex_mediaqueries.html @@ -0,0 +1,774 @@ + + + + + + + + What Async Promised and What it Delivered — Causality + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
April 2026
+

What Async Promised and What it Delivered

+

+ Each wave fixed the last wave's worst problem and introduced a new + one. +

+
+
+

+ OS threads carry overhead: an operating system thread reserves + virtual address space for its stack and takes on the order of tens + of microseconds to create on modern Linux. + (edit Apr 27: The original “roughly a millisecond”, and “megabyte + of stack space” conflated virtual reservation with physical memory + commitment. Thanks to ibraheemdev, jemfinch, eklitzke on HN) + Context switches happen in kernel space and burn CPU cycles, and + O(n) readiness polling (select, poll) add up at scale. A server + handling thousands of concurrent connections and dedicating one + thread per connection means thousands of threads each consuming + memory and competing for scheduling. The system spends time managing + threads that could be better spent doing useful work. +

+

+ This is the C10K problem, named by Dan Kegel in 1999. If you were + building a web server, a chat system, or anything with a large + number of simultaneous connections, you needed a way to handle + concurrency without a thread per connection. +

+

+ The answer came in waves, each solving the previous wave’s worst + problem while introducing new ones. Previously we’ve looked at + channels in Go + and + actors in Erlang. Now we turn to async, which is everywhere these days. +

+

Callbacks

+

+ The first wave was straightforward: don’t block the thread. Instead + of waiting for an i/o operation to complete, register a function to + be called when it finishes and move on to the next piece of work. + Event loops (select, poll, epoll, kqueue) multiplexed thousands of + connections onto a handful of threads, and callbacks were the + programmer’s interface to this machinery. +

+

+ Node.js built an entire ecosystem on this model, handling thousands + of concurrent connections on a single thread. Nginx’s event-driven + architecture was a major reason it displaced Apache for + high-concurrency workloads. +

+

+ This nicely solved the performance problem, but at a cost: callbacks + invert control flow. Instead of writing “do A, then B, then C” as + three sequential statements, you write “do A, and when it’s done + call this function, which does B, and when that’s done call + this other function, which does C.” The programmer’s intent becomes + scattered across nested closures. JavaScript developers named this + “callback hell” and built + an entire website to + commiserate. +

+

+ Callbacks have deeper problems than aesthetics, such as fracturing + error handling. Each callback needs its own error path. Errors can’t + propagate naturally up the call stack because there is no call stack + (callbacks run in a different context from where they are + registered). Handling partial failure in a chain of callbacks means + threading error state through every function in the chain. +

+

+ Plus, callbacks have no notion of cancellation. If you start an + asynchronous operation and then decide you don’t need the result, + there’s no general way to stop it. The callback will fire + eventually, and your code needs to handle the case where it no + longer cares about the result. +

+

+ Callbacks solved the resource problem (too many threads) by creating + an ergonomics problem (code that’s hard to write, read, and get + right). +

+

Promises and Futures

+

+ The next wave started with a good idea: what if, instead of passing + a callback for later invocation, an asynchronous operation + immediately returned an object representing its eventual result? +

+

+ This is a promise (JavaScript) or future (Java, Rust, etc). The + concept dates to Baker and Hewitt in 1977, but it took the C10K + pressure of the 2010s to push it into mainstream programming. + JavaScript standardized native Promises in ES2015 following the + community-driven Promises/A+ spec, and Java 8 introduced + CompletableFuture. +

+

+ Promises are more ergonomic than callbacks. First, promises are + composable: promise.then(f).then(g) reads as a pipeline + instead of a nested pyramid. Error handling also consolidates: a + .catch() at the end of a chain handles failures from + any step. And promises are values that you can store, pass around, + and return from functions. A first-class handle to an eventual value + moves the conversation away from raw threads and toward data + dependencies. The idea that “this value depends on a computation + that hasn’t finished yet” is a useful thing to be able to express. +

+

+ Here’s JavaScript reading a user profile and then fetching their + recent orders, first with callbacks, then with promises: +

+
// Callbacks: nested, error handling at every level
+getUser(userId, (err, user) => {
+    if (err) return handleError(err);
+    getOrders(user.id, (err, orders) => {
+        if (err) return handleError(err);
+        render(user, orders);
+    });
+});
+
+// Promises: chained, error handling consolidated
+getUser(userId)
+    .then(user => getOrders(user.id).then(orders => [user, orders]))
+    .then(([user, orders]) => render(user, orders))
+    .catch(handleError);
+

+ The promise-based version is not a huge improvement on this small + example, but the difference grows with complexity: five steps deep + in callbacks is nearly unreadable, while five + .then() calls chained together are at least linear. +

+

But promises introduced their own problems:

+

+ Promises are one-shot. A promise resolves exactly + once. This makes them unsuitable for modeling streams, events, + repeated messages, or any ongoing communication. A WebSocket that + receives a stream of messages doesn’t map onto “a value that will + exist later.” This forces a split: promises for request-response + patterns, and something else (event emitters, observables, callbacks + again) for everything else. +

+

+ Composition is clunky. The example above hints at + it: getting both user and orders into the + final .then() requires nesting or awkward gymnastics + with Promise.all. Two independent async operations are + easy (Promise.all([a, b])), but anything more complex + (conditional branching, loops over async operations, early exit) + requires increasingly elaborate combinator patterns. These patterns + work but they’re a functional programming idiom grafted onto an + imperative language and they don’t feel natural. +

+

+ Errors vanish silently. JavaScript promises that + reject without a .catch() handler originally just + swallowed the error. The value was lost causing failures to be + invisible. This was bad enough that Node.js eventually changed + unhandled rejections from a warning to a process crash, and browsers + added unhandledrejection events. A feature designed to + improve error handling managed to create an entirely new class of + silent failures that didn’t exist with callbacks. +

+

+ The type split. Every function now returns either a + value or a promise of a value. So callers need to know which one + they’re getting and libraries need to decide which one to provide. A + function that was synchronous becomes asynchronous when you add a + database call to it, and now every caller needs to handle a promise + instead of a value. This is a mild form of the coloring problem that + the next wave would make even worse. +

+

Async/Await

+

+ Promise chains still looked nothing like the sequential code + developers wrote for everything else. Async/await, pioneered by C# + in 2012 and adopted by JavaScript (ES2017), Python (3.5), Rust + (1.39), Kotlin, Swift, and Dart, delivered exactly that: +

+
// Promise chains
+function loadDashboard(userId) {
+    return getUser(userId)
+        .then(user => getOrders(user.id)
+            .then(orders => [user, orders]))
+        .then(([user, orders]) => render(user, orders));
+}
+
+// Async/await
+async function loadDashboard(userId) {
+    const user = await getUser(userId);
+    const orders = await getOrders(user.id);
+    return render(user, orders);
+}
+

+ The async/await version reads like sequential code. Variables bind + naturally. You can use try/catch instead of + .catch(). Loops work with await inside + them. It’s an ergonomic win for linear sequences of asynchronous + operations. +

+

+ The industry adopted it fast, with JavaScript frameworks going + all-in, Python’s asyncio becoming the standard approach for + concurrent i/o, and Rust stabilizing async/await as the path to + high-performance networking. Within a few years, async/await was the + default way to write concurrent i/o code in most mainstream + languages. +

+

+ Paying the Function Coloring Tax +

+

+ In 2015, right as async/await was gaining steam, Bob Nystrom + published + “What Color is Your Function?”, a thought experiment about a language where every function is + either “red” or “blue.” Red functions can call blue functions, but + blue functions can’t call red functions without special ceremony. + Every function must choose a color, and if you call a red function + from a blue one, the blue one must become red, spreading virally + throughout the codebase. +

+

+ This was an analogy to async/await: async functions are red, sync + functions are blue. An async function can call a sync function + without issue, but calling an async function from a sync function + requires blocking the thread or restructuring the code. Every + function in your program must choose a color, and that choice + propagates through every caller. +

+

+ Nystrom’s post stuck because it put a name to something developers + had been experiencing without a vocabulary for it. Function coloring + reshapes entire codebases and ecosystems. +

+

+ The Rust async ecosystem fragmented around competing runtimes + (Tokio, async-std, smol) that provide incompatible implementations + of fundamental types like TCP streams and timers. A library written + for Tokio can’t easily be used with async-std. The popular HTTP + client reqwest simply requires Tokio, and if your + project uses a different runtime, that’s your problem. Now library + authors either pick Tokio (locking out alternatives) or attempt + runtime-agnostic abstractions (adding complexity and sometimes + performance overhead). +

+

+ Tokio’s dominance is function coloring at ecosystem scale. The tax + shows up at other scales too: +

+

+ At the function level, adding a single i/o call to + a previously synchronous function changes its signature, its return + type, and its calling convention. Every caller must be updated, and + their callers must be updated. The change ripples through the call + graph until it hits a framework entry point or a main function. A + one-line database lookup can require modifying dozens of files. +

+

+ At the library level, authors face a choice of + writing a sync library and exclude async users, or writing an async + library and force sync users to add runtime dependencies (or + maintain both). Many choose “both,” doubling the API surface, the + test matrix, and the maintenance burden. In Python, the + requests library (sync) and + aiohttp (async) are separate projects by separate + authors doing the same thing. httpx eventually appeared + to offer both interfaces from one package, which is an improvement + only needed because of the split. +

+

+ At the ecosystem level, the Rust example above is + the norm, not the exception. Every library that touches i/o must + choose a color, and that choice limits which other libraries it can + work with. The Rust async book itself notes that “sync and async + code also tend to promote different design patterns, which can make + it difficult to compose code intended for the different + environments.” +

+

+ And the costs aren’t just logistical: async/await introduced + entirely new categories of bugs that threads don’t have. O’Connor + documents a class of async Rust deadlocks he calls “futurelocks”: a + future acquires a lock, then stops being polled while another future + tries to acquire the same lock. With threads, a thread holding a + lock always makes progress toward releasing it (unless you do + something everyone knows is dangerous, like + SuspendThread). With async Rust, the standard tools + like select!, buffered streams, and + FuturesUnordered routinely stop polling futures that + hold resources. The original futurelock at Oxide required core dumps + and a disassembler to diagnose. +

+

A Sequential Trap

+

+ A subtler cost that gets less attention is that async/await’s + greatest strength, making asynchronous code look sequential, is also + a cognitive trap. +

+
async function loadDashboard(userId) {
+    const user = await getUser(userId);
+    const orders = await getOrders(user.id);
+    const recommendations = await getRecommendations(user.id);
+    return render(user, orders, recommendations);
+}
+

+ This fetches orders and recommendations sequentially: + getRecommendations doesn’t start until + getOrders finishes. But these two operations are + independent, because recommendations don’t depend on orders. So they + could run in parallel, but don’t. The code looks clean and correct + while leaving performance on the table. +

+

+ The parallel version requires the programmer to explicitly break out + of sequential style: +

+
async function loadDashboard(userId) {
+    const user = await getUser(userId);
+    const [orders, recommendations] = await Promise.all([
+        getOrders(user.id),
+        getRecommendations(user.id)
+    ]);
+    return render(user, orders, recommendations);
+}
+

+ The pattern scales poorly beyond small examples. In a real + application with dozens of async calls, determining which operations + are independent and can be parallelized requires the programmer to + manually analyze dependencies and restructure the code accordingly. + The sequential syntax actively obscures the dependency structure, + i.e. the one piece of information that would tell you what can run + in parallel. +

+

+ Async/await was introduced to make asynchronous code easier to + write. It made “what can run concurrently” something the programmer + must determine manually and express through combinator patterns that + break the sequential flow that was the whole point. +

+

What Async Got Right

+

To be fair, async abstractions did improve things.

+

+ Async/await’s ergonomics for linear sequences are better than + callbacks or promise chains. For code that’s inherently sequential + but happens to include i/o, async/await removes real syntactic + noise. It’s easier to read and debug than callback-based code. +

+

+ Some language designers chose different paths. For example, Go + deliberately chose goroutines, accepting a heavier runtime in + exchange for no function coloring at all. + (Edit note Apr 24: Go actually introduced a form of coloring + through context.Context, which propagates through + calls for cancellation. Edit Apr 27: previous language implied Go + made the decision in reaction to async/await) + Java’s Project Loom (virtual threads in Java 21) also made a + different bet: lightweight threads that look and behave like regular + threads, so no code needs to change color. The Loom team explicitly + cited function coloring as a problem they wanted to avoid. +

+

+ Zig went further: it removed its compiler-level async/await entirely + and rebuilt around an Io interface parameter that i/o operations + accept. The runtime (threaded, event-loop, whatever the user + supplies) fulfills the interface. Function signatures don’t change + based on how they’re scheduled, and async/await become library + functions rather than language keywords. Though some argue that the + Io parameter itself is a form of coloring. +

+

+ Language designers who studied the async/await experience in other + ecosystems concluded that the costs of function coloring outweigh + the benefits and chose different paths. +

+

Accumulating Costs

+

+ Each solution solved a problem but introduced new costs. And those + costs are structural, affecting the shape of every program, library, + and API in the codebase. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
WaveSolvedIntroduced
CallbacksThread-per-connection resource exhaustion + Inverted control flow, fragmented error handling, callback + hell +
PromisesNesting, error consolidation, values over callbacks + One-shot limitation, silent error swallowing, mild type split +
Async/AwaitErgonomics for linear async sequences + Function coloring, ecosystem fragmentation, new deadlock + classes, sequential trap +
+

+ Each wave made the local experience of writing async code more + pleasant while making the global experience more complex. The + developer writing a single async function has never had it better, + while the team maintaining a large codebase with mixed sync/async + code, managing dependency compatibility across runtimes, and trying + to find parallelism opportunities hidden behind sequential-looking + await chains are carrying a burden that didn’t exist + before these abstractions were introduced. +

+

+ This isn’t a case of bad engineering. The people who designed + callbacks, promises, and async/await were solving real problems, and + each step was a reasonable response to the previous step’s failures. + But fifteen years and several iterations in, the accumulated tax is + sizable, and a pattern is visible: each fix treats symptoms while + leaving the structure intact. +

+

+ The callbacks-to-promises-to-async/await arc may be the clearest + illustration yet of a theme running through this series: approaches + that start by asking “how do we manage concurrent execution?” keep + generating new problems at every level of abstraction. You can watch + this one play out in real time, across a single ecosystem, within a + single decade. +

+

References

+ +
+ + + +
+
+ + + + diff --git a/bin/unit_tests/assets/html/flex_mediaqueries_files/about.ZLlEu_QV.css b/bin/unit_tests/assets/html/flex_mediaqueries_files/about.ZLlEu_QV.css new file mode 100644 index 000000000..88968f919 --- /dev/null +++ b/bin/unit_tests/assets/html/flex_mediaqueries_files/about.ZLlEu_QV.css @@ -0,0 +1,732 @@ +:root { + --bg: #1C1917; + --surface: #292524; + --surface-hover: #302c2a; + --text: #F0EEEC; + --heading: #FAFAF7; + --heading2: #ffc25f; + --accent: #D97706; + --accent-hover: #F59E0B; + --muted: #A8A29E; + --code-text: #FDE68A; + --code-bg: #1a1714; + --border: #897c75; + --amber-dim: #92400E; + --astro-code-background: #1a1714; + --astro-code-foreground: #F0EEEC; + --astro-code-token-constant: #93C5FD; + --astro-code-token-string: #6EE7B7; + --astro-code-token-comment: #A8A29E; + --astro-code-token-keyword: #F59E0B; + --astro-code-token-function: #FDE68A; + --astro-code-token-punctuation: #F0EEEC +} + +*, +*:before, +*:after { + margin: 0; + padding: 0; + box-sizing: border-box +} + +html { + font-size: 18px; + scroll-behavior: smooth +} + +body { + background: var(--bg); + color: var(--text); + font-family: "Source Serif 4", Georgia, serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.site-header { + padding: 2rem 1.5rem 1.5rem; + max-width: 700px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center +} + +.header-logo { + display: flex; + align-items: center; + gap: .6rem; + text-decoration: none +} + +.header-wordmark { + font-family: "Source Sans 3", sans-serif; + font-weight: 700; + font-size: 1.25rem; + color: var(--heading); + letter-spacing: -.01em +} + +.site-nav { + display: flex; + gap: 1.5rem +} + +.site-nav a { + font-family: "Source Sans 3", sans-serif; + font-size: .9rem; + font-weight: 500; + color: var(--muted); + text-decoration: none; + transition: color .2s +} + +.site-nav a:hover { + text-decoration: underline; + color: var(--accent) +} + +.content { + max-width: 700px; + margin: 0 auto; + padding: 0 1.5rem 4rem +} + +.site-intro { + margin-top: 2rem; + margin-bottom: 2rem +} + +.series-title { + font-family: "Source Serif 4", Georgia, serif; + font-weight: 700; + font-size: 1.8rem; + color: var(--heading); + line-height: 1.25; + margin-bottom: .6rem +} + +.series-subtitle { + font-family: "Source Serif 4", Georgia, serif; + font-style: italic; + font-size: 1.05rem; + color: var(--muted); + line-height: 1.55 +} + +.series-roadmap { + margin-bottom: 2.5rem +} + +.roadmap-list { + list-style: none; + padding: 0; + margin: 0 +} + +.roadmap-item { + display: flex; + align-items: baseline; + gap: .75rem; + padding: .6rem 0; + border-bottom: 1px solid var(--border) +} + +.roadmap-item:first-child { + border-top: 1px solid var(--border) +} + +.roadmap-number { + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + font-size: .85rem; + color: var(--muted); + min-width: 1.5rem; + flex-shrink: 0 +} + +.roadmap-item.published .roadmap-number { + color: var(--accent) +} + +.roadmap-title { + font-family: "Source Serif 4", Georgia, serif; + font-size: 1.05rem; + line-height: 1.4 +} + +a.roadmap-title { + color: var(--heading); + text-decoration: none; + transition: color .2s +} + +a.roadmap-title:hover { + color: var(--accent) +} + +.upcoming-title { + color: var(--text); + opacity: .7 +} + +.placeholder-title { + color: var(--muted); + font-style: italic; + opacity: .6 +} + +.roadmap-badge { + font-family: "Source Sans 3", sans-serif; + font-size: .7rem; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: .05em; + margin-left: auto; + flex-shrink: 0 +} + +.roadmap-note { + margin-top: 1rem; + font-family: "Source Sans 3", sans-serif; + font-size: .85rem +} + +.roadmap-note a { + color: var(--accent); + text-decoration: none; + transition: color .2s +} + +.roadmap-note a:hover { + color: var(--accent-hover) +} + +.latest-essay { + margin-bottom: 1rem +} + +.section-heading { + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + font-size: .85rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: .08em; + margin-bottom: .25rem +} + +.essay-subtitle-preview { + font-family: "Source Serif 4", Georgia, serif; + font-style: italic; + font-size: .95rem; + color: var(--muted); + line-height: 1.5; + margin-bottom: .6rem +} + +.series-page-header { + margin-top: 2rem; + margin-bottom: 2.5rem +} + +.series-page-header h1 { + font-family: "Source Serif 4", Georgia, serif; + font-weight: 700; + font-size: 1.8rem; + color: var(--heading); + line-height: 1.25; + margin-bottom: .6rem +} + +.series-page-subtitle { + font-family: "Source Serif 4", Georgia, serif; + font-style: italic; + font-size: 1.05rem; + color: var(--muted); + line-height: 1.55; + margin-bottom: 1.25rem +} + +.series-page-intro { + font-size: .95rem; + line-height: 1.65; + color: var(--text); + opacity: .85 +} + +.roadmap-content { + flex: 1 +} + +.roadmap-date { + font-family: "Source Sans 3", sans-serif; + font-size: .75rem; + color: var(--muted); + margin-left: .75rem +} + +.roadmap-teaser { + font-size: .88rem; + color: var(--text); + opacity: .7; + line-height: 1.55; + margin-top: .25rem +} + +.essay-list { + margin-top: 2rem +} + +.essay-item { + padding: 2rem 0; + border-bottom: 1px solid var(--border) +} + +.essay-item:first-child { + border-top: 1px solid var(--border) +} + +.essay-date { + font-family: "Source Sans 3", sans-serif; + font-size: .8rem; + color: var(--muted); + margin-bottom: .4rem +} + +.essay-title { + font-family: "Source Serif 4", Georgia, serif; + font-weight: 700; + font-size: 1.35rem; + line-height: 1.35; + margin-bottom: .6rem +} + +.essay-title a { + color: var(--heading); + text-decoration: none; + transition: color .2s +} + +.essay-title a:hover { + color: var(--accent) +} + +.essay-excerpt { + color: var(--text); + font-size: .95rem; + line-height: 1.7; + opacity: .85 +} + +.essay-header { + margin-top: 2rem; + margin-bottom: 2.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border) +} + +.essay-header .essay-date { + margin-bottom: .75rem +} + +.essay-header h1 { + font-family: "Source Serif 4", Georgia, serif; + font-weight: 700; + font-size: 2rem; + line-height: 1.25; + color: var(--heading); + margin-bottom: .75rem +} + +.essay-subtitle { + font-family: "Source Serif 4", Georgia, serif; + font-style: italic; + font-size: 1.1rem; + color: var(--muted); + line-height: 1.5 +} + +.essay-body h2 { + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + font-size: 1.2rem; + color: var(--heading); + margin-top: 2.5rem; + margin-bottom: .75rem; + letter-spacing: -.01em +} + +.essay-body h3 { + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + font-size: 1.05rem; + color: var(--heading); + margin-top: 2rem; + margin-bottom: .5rem +} + +.essay-body p { + margin-bottom: 1.25rem; + font-size: 1rem; + line-height: 1.65 +} + +.essay-body a { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: var(--amber-dim); + transition: text-decoration-color .2s +} + +.essay-body a:hover { + text-decoration-color: var(--accent-hover) +} + +.essay-body blockquote { + border-left: 3px solid var(--accent); + padding: .75rem 1.25rem; + margin: 1.5rem 0; + background: var(--surface); + border-radius: 0 4px 4px 0; + font-style: italic; + color: var(--text); + opacity: .9 +} + +.essay-body blockquote p { + margin-bottom: 0 +} + +.essay-body code { + font-family: Source Code Pro, monospace; + font-size: .85em; + background: var(--surface); + color: var(--code-text); + padding: .15em .4em; + border-radius: 3px +} + +.essay-body pre { + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1.25rem 1.5rem; + margin: 1.5rem 0; + overflow-x: auto; + line-height: 1.5 +} + +.essay-body pre code { + background: none; + padding: 0; + font-size: .85rem; + color: var(--code-text) +} + +.essay-body ul, +.essay-body ol { + margin-bottom: 1.25rem; + padding-left: 1.5rem +} + +.essay-body li { + margin-bottom: .4rem; + line-height: 1.65 +} + +.essay-body strong { + color: var(--heading2); + font-weight: 600 +} + +.essay-body em { + font-style: italic +} + +.essay-body hr { + border: none; + border-top: 1px solid var(--border); + margin: 2.5rem 0 +} + +.essay-body img { + max-width: 100%; + border-radius: 6px +} + +.essay-body table { + width: 100%; + border-collapse: collapse; + margin: 1.5rem 0; + font-size: .92rem; + line-height: 1.5 +} + +.essay-body thead th { + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + color: var(--heading); + text-align: left; + padding: .6rem 1rem; + border-bottom: 2px solid var(--border) +} + +.essay-body tbody td { + padding: .6rem 1rem; + border-bottom: 1px solid var(--border); + vertical-align: top +} + +.essay-body tbody tr:last-child td { + border-bottom: none +} + +.essay-body tbody td code { + font-size: .82em +} + +.discuss-links { + margin-top: 3rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border); + font-family: "Source Sans 3", sans-serif; + font-size: .85rem; + color: var(--muted); + display: flex; + gap: 1.5rem +} + +.discuss-links a { + color: var(--accent); + text-decoration: none; + transition: color .2s +} + +.discuss-links a:hover { + color: var(--accent-hover); + text-decoration: underline +} + +.newsletter-box { + margin-top: 3rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.75rem 2rem +} + +.newsletter-box h3 { + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + font-size: 1rem; + color: var(--heading); + margin-bottom: .4rem +} + +.newsletter-box p { + font-size: .88rem; + color: var(--muted); + line-height: 1.5; + margin-bottom: 1rem +} + +.newsletter-form { + display: flex; + gap: .5rem +} + +.newsletter-form input[type=email] { + flex: 1; + padding: .6rem .9rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + font-family: "Source Sans 3", sans-serif; + font-size: .88rem; + outline: none; + transition: border-color .2s +} + +.newsletter-form input[type=email]:focus { + border-color: var(--accent) +} + +.newsletter-form input[type=email]::placeholder { + color: var(--muted); + opacity: .6 +} + +.newsletter-form button { + padding: .6rem 1.25rem; + background: var(--accent); + border: none; + border-radius: 6px; + color: #1c1917; + font-family: "Source Sans 3", sans-serif; + font-weight: 600; + font-size: .85rem; + cursor: pointer; + transition: background .2s +} + +.newsletter-form button:hover { + background: var(--accent-hover) +} + +.essay-nav { + margin-top: 3rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 2rem +} + +.essay-nav a { + display: flex; + flex-direction: column; + text-decoration: none; + gap: .25rem +} + +.essay-nav-label { + font-family: "Source Sans 3", sans-serif; + font-size: .8rem; + color: var(--muted); + line-height: 1.4 +} + +.essay-nav-title { + font-family: "Source Sans 3", sans-serif; + font-size: .9rem; + color: var(--accent); + transition: color .2s; + line-height: 1.4 +} + +.essay-nav a:hover .essay-nav-title { + color: var(--accent-hover); + text-decoration: underline +} + +.essay-nav-next { + margin-left: auto; + text-align: right +} + +.about-content { + margin-top: 2rem +} + +.about-content h1 { + font-family: "Source Serif 4", Georgia, serif; + font-weight: 700; + font-size: 1.8rem; + color: var(--heading); + margin-bottom: 1.5rem +} + +.about-content p { + margin-bottom: 1.25rem; + font-size: 1rem; + line-height: 1.65 +} + +.about-content a { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: var(--amber-dim); + transition: text-decoration-color .2s +} + +.about-content a:hover { + text-decoration-color: var(--accent-hover) +} + +.about-links { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border); + display: flex; + gap: 1.5rem; + font-family: "Source Sans 3", sans-serif; + font-size: .9rem +} + +.about-links a { + color: var(--accent); + text-decoration: none; + transition: color .2s +} + +.about-links a:hover { + color: var(--accent-hover); + text-decoration: underline +} + +.site-footer { + max-width: 700px; + margin: 0 auto; + padding: 1.5rem; + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-end; + font-family: "Source Sans 3", sans-serif; + font-size: .9rem; + color: var(--muted) +} + +.site-footer a { + color: var(--muted); + text-decoration: none; + transition: color .2s +} + +.site-footer a:hover { + text-decoration: underline; + color: var(--accent) +} + +@media(max-width:600px) { + html { + font-size: 16px + } + + .site-header { + padding: 1.5rem 1rem 1rem + } + + .content { + padding: 0 1rem 3rem + } + + .essay-header h1 { + font-size: 1.6rem + } + + .newsletter-form { + flex-direction: column + } + + .essay-nav { + flex-direction: column; + gap: 1.25rem + } + + .essay-nav-next { + text-align: left + } + + .site-footer { + flex-direction: column; + gap: .5rem; + padding: 1.5rem 1rem + } +} \ No newline at end of file diff --git a/bin/unit_tests/assets/html/flex_mediaquery.html b/bin/unit_tests/assets/html/flex_mediaquery.html new file mode 100644 index 000000000..ac710ab19 --- /dev/null +++ b/bin/unit_tests/assets/html/flex_mediaquery.html @@ -0,0 +1,71 @@ + + + + + + + + + diff --git a/src/eepp/ui/css/mediaquery.cpp b/src/eepp/ui/css/mediaquery.cpp index 974a7e92e..7931e8d1f 100644 --- a/src/eepp/ui/css/mediaquery.cpp +++ b/src/eepp/ui/css/mediaquery.cpp @@ -141,6 +141,11 @@ MediaQueryList::ptr MediaQueryList::parse( const std::string& str ) { String::trimInPlace( tok ); String::toLowerInPlace( tok ); + if ( String::startsWith( tok, "@media" ) ) { + tok = tok.substr( 6 ); + String::trimInPlace( tok ); + } + MediaQuery::ptr query = MediaQuery::parse( tok ); if ( query ) { diff --git a/src/eepp/ui/flexlayouter.cpp b/src/eepp/ui/flexlayouter.cpp index d910e1b57..8b998f9d7 100644 --- a/src/eepp/ui/flexlayouter.cpp +++ b/src/eepp/ui/flexlayouter.cpp @@ -183,6 +183,15 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis item.widget->getLayoutHeightPolicy() != SizePolicy::Fixed ) item.widget->setInternalPixelsHeight( tentativeCrossSize ); + SizePolicy oldMainPolicy = mainAxis.horizontal ? item.widget->getLayoutWidthPolicy() + : item.widget->getLayoutHeightPolicy(); + if ( oldMainPolicy == SizePolicy::MatchParent ) { + if ( mainAxis.horizontal ) + item.widget->setLayoutWidthPolicy( SizePolicy::WrapContent ); + else + item.widget->setLayoutHeightPolicy( SizePolicy::WrapContent ); + } + if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) ) { auto* childHtml = item.widget->asType(); auto* layouter = childHtml->getLayouter(); @@ -191,6 +200,13 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis } } + if ( oldMainPolicy == SizePolicy::MatchParent ) { + if ( mainAxis.horizontal ) + item.widget->setLayoutWidthPolicy( oldMainPolicy ); + else + item.widget->setLayoutHeightPolicy( oldMainPolicy ); + } + item.targetMainSize = resolveFlexBasis( item.widget, mDirection, item.flexBasisValue, item.flexBasisAuto, mainAxis ); @@ -366,19 +382,19 @@ void FlexLayouter::alignMainAxis( FlexLine& line, const Float containerMainSize, break; case CSSJustifyContent::SpaceBetween: if ( itemCount > 1 ) - spacing = freeSpaceMain / (Float)( itemCount - 1 ); + spacing = columnGap + freeSpaceMain / (Float)( itemCount - 1 ); else mainOffset = 0.f; break; case CSSJustifyContent::SpaceAround: if ( itemCount > 0 ) { - spacing = freeSpaceMain / (Float)itemCount; + spacing = columnGap + freeSpaceMain / (Float)itemCount; mainOffset = spacing * 0.5f; } break; case CSSJustifyContent::SpaceEvenly: if ( itemCount > 0 ) { - Float slot = freeSpaceMain / (Float)( itemCount + 1 ); + Float slot = columnGap + freeSpaceMain / (Float)( itemCount + 1 ); mainOffset = slot; spacing = slot; } @@ -572,12 +588,12 @@ void FlexLayouter::applyLayout( const std::vector& lines, const Axis& Float totH = containerHeight; if ( widthPolicy == SizePolicy::WrapContent ) - totW = contentW + containerPadding.Right; + totW = containerPadding.Left + contentW + containerPadding.Right; else if ( widthPolicy != SizePolicy::Fixed ) totW = mContainer->getParent() ? mContainer->getParent()->getPixelsSize().getWidth() : totW; if ( heightPolicy == SizePolicy::WrapContent ) - totH = contentH + containerPadding.Bottom; + totH = containerPadding.Top + contentH + containerPadding.Bottom; mContainer->setInternalPixelsWidth( totW ); mContainer->setInternalPixelsHeight( totH ); diff --git a/src/eepp/ui/uilayoutermanager.cpp b/src/eepp/ui/uilayoutermanager.cpp index 66df3eb9b..46d32abd1 100644 --- a/src/eepp/ui/uilayoutermanager.cpp +++ b/src/eepp/ui/uilayoutermanager.cpp @@ -24,6 +24,9 @@ static bool parentIsFlexContainer( UIWidget* widget ) { UILayouter* UILayouterManager::create( CSSDisplay display, UIWidget* container ) { // Blockification per CSS Flexbox §4: children of flex containers are block-level if ( parentIsFlexContainer( container ) ) { + // But a child that is itself a flex container still needs FlexLayouter + if ( display == CSSDisplay::Flex || display == CSSDisplay::InlineFlex ) + return eeNew( FlexLayouter, ( container ) ); return eeNew( BlockLayouter, ( container ) ); } diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index 5f0c29b05..a9a66d3ef 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -71,8 +71,21 @@ bool UITextSpan::isInlineBlock() const { } void UITextSpan::draw() { - if ( !isInline() ) + if ( !isInline() ) { UIRichText::draw(); + } else { + // When a UITextSpan is a flex item it is laid out independently by the + // flex container (blockification per CSS Flexbox §4). In that case the + // parent flex container does NOT render its text via rebuildRichText(), + // so the span must draw itself. + Node* parentNode = getParent(); + if ( parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) ) { + CSSDisplay parentDisplay = parentNode->asType()->getDisplay(); + if ( parentDisplay == CSSDisplay::Flex || parentDisplay == CSSDisplay::InlineFlex ) { + UIRichText::draw(); + } + } + } } bool UITextSpan::applyProperty( const StyleSheetProperty& attribute ) { diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 1c1f3a3f2..42582ce64 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -3257,3 +3257,144 @@ UTEST( UIHTML, FlexFormLayout ) { Engine::destroySingleton(); } + +UTEST( UIHTML, FlexMediaQueriesLayout ) { + Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Media Queries Layout Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + + std::string html; + FileSystem::fileGet( "assets/html/flex_mediaqueries.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + + sceneNode->update( Seconds( 1 ) ); + sceneNode->updateDirtyLayouts(); + + auto body = sceneNode->getRoot()->findByTag( "body" ); + ASSERT_TRUE( body != nullptr ); + auto bodyWidget = body->asType(); + + // Header + auto header = bodyWidget->findByClass( "site-header" ); + ASSERT_TRUE( header != nullptr ); + auto headerWidget = header->asType(); + + // Header logo (contains SVG + "Causality" text) + auto logo = headerWidget->findByClass( "header-logo" ); + ASSERT_TRUE( logo != nullptr ); + auto logoWidget = logo->asType(); + + // Header wordmark (the "Causality" span) + auto wordmark = headerWidget->findByClass( "header-wordmark" ); + ASSERT_TRUE( wordmark != nullptr ); + auto wordmarkWidget = wordmark->asType(); + + // Site nav + auto nav = headerWidget->findByClass( "site-nav" ); + ASSERT_TRUE( nav != nullptr ); + auto navWidget = nav->asType(); + + // Header should have visible height (padding + content + padding) + EXPECT_GT( headerWidget->getPixelsSize().getHeight(), 80.f ); + + // Header should not exceed content width significantly + EXPECT_LE( headerWidget->getPixelsSize().getWidth(), 1024.f ); + + // The wordmark "Causality" should have positive width and height (visible text) + EXPECT_GT( wordmarkWidget->getPixelsSize().getWidth(), 10.f ); + EXPECT_GT( wordmarkWidget->getPixelsSize().getHeight(), 5.f ); + + // The logo (flex item in header) should not extend outside the header + EXPECT_LE( logoWidget->getPixelsPosition().x + logoWidget->getPixelsSize().getWidth(), + headerWidget->getPixelsPosition().x + headerWidget->getPixelsSize().getWidth() + + 1.f ); + + // Site nav should not overflow the header + EXPECT_LE( navWidget->getPixelsPosition().x + navWidget->getPixelsSize().getWidth(), + headerWidget->getPixelsPosition().x + headerWidget->getPixelsSize().getWidth() + + 1.f ); + + // Essay nav + auto essayNav = bodyWidget->findByClass( "essay-nav" ); + ASSERT_TRUE( essayNav != nullptr ); + auto essayNavWidget = essayNav->asType(); + + // The essay-nav link () + auto essayNavLink = essayNavWidget->findByClass( "essay-nav-prev" ); + ASSERT_TRUE( essayNavLink != nullptr ); + auto essayNavLinkWidget = essayNavLink->asType(); + + // essay-nav should contain its link (link should not overflow in local coords) + Float linkLocalX = essayNavLinkWidget->getPixelsPosition().x; + Float linkLocalY = essayNavLinkWidget->getPixelsPosition().y; + Float linkLocalW = essayNavLinkWidget->getPixelsSize().getWidth(); + Float linkLocalH = essayNavLinkWidget->getPixelsSize().getHeight(); + Float navW = essayNavWidget->getPixelsSize().getWidth(); + Float navH = essayNavWidget->getPixelsSize().getHeight(); + + EXPECT_GE( linkLocalX, -1.f ); + EXPECT_LE( linkLocalX + linkLocalW, navW + 1.f ); + EXPECT_GE( linkLocalY, -1.f ); + EXPECT_LE( linkLocalY + linkLocalH, navH + 1.f ); + + // essay-nav should have visible size + EXPECT_GT( essayNavWidget->getPixelsSize().getHeight(), 10.f ); + EXPECT_GT( essayNavWidget->getPixelsSize().getWidth(), 10.f ); + + // The essay-nav link contains two spans: label and title + // They should stack vertically (flex-direction: column on the ) + // so the link height should be at least the sum of both span heights + auto essayLabel = essayNavLinkWidget->findByClass( "essay-nav-label" ); + auto essayTitle = essayNavLinkWidget->findByClass( "essay-nav-title" ); + if ( essayLabel && essayTitle ) { + auto labelWidget = essayLabel->asType(); + auto titleWidget = essayTitle->asType(); + EXPECT_GT( essayNavLinkWidget->getPixelsSize().getHeight(), + labelWidget->getPixelsSize().getHeight() + + titleWidget->getPixelsSize().getHeight() - 1.f ); + } + + Engine::destroySingleton(); +} + +UTEST( UIHTML, FlexAnchorInFlexNavVisible ) { + Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Anchor in Flex Nav Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + + std::string html; + FileSystem::fileGet( "assets/html/flex_mediaquery.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + + sceneNode->update( Seconds( 1 ) ); + sceneNode->updateDirtyLayouts(); + + auto body = sceneNode->getRoot()->findByTag( "body" ); + ASSERT_TRUE( body != nullptr ); + auto bodyWidget = body->asType(); + + auto nav = bodyWidget->findByClass( "site-nav" ); + ASSERT_TRUE( nav != nullptr ); + auto navWidget = nav->asType(); + + // The nav is a flex container; its children are blockified flex items. + // Each should have non-zero width and height (text must be visible). + for ( Uint32 i = 0; i < navWidget->getChildCount(); ++i ) { + auto c = navWidget->getChildAt( i ); + auto cw = c->asType(); + if ( !cw || cw->getElementTag() != "a" ) + continue; + EXPECT_GT( cw->getPixelsSize().getWidth(), 5.f ); + EXPECT_GT( cw->getPixelsSize().getHeight(), 5.f ); + } + + Engine::destroySingleton(); +}