fluor 0.0.2

First-principles GUI compositor library: center-origin RU coordinates, harmonic-mean span scaling, CPU softbuffer rendering, ARM-first.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
//! Focus glow rasterizers. Four directional ray passes (right / left / top / bottom) that scan the canvas's α channel to find the silhouette of the pill to glow around, then emit decaying rays of partial-α `glow_colour` pixels outward. Plus a 4-way blur variant used by photon's textbox glow shape, and the shared decay-table helper.
//!
//! The `factor_256` curve: `alpha[k] = (alpha[k-1] × factor_256) >> 8`. Geometric decay starting from `seed` (typically `0x80` for horizontal, `0x40` for vertical). [`ray_reach_px`] reports how many steps until α decays to zero — callers use it to pre-size damage rects so the host clears + finalizes exactly the area the rasterizer will paint.

use super::HitId;
use crate::canvas::Canvas;
use crate::paint::Clip;
use crate::pixel::{Blend, BlendMode};

// Suppress the unused-import warning when this file gets re-included via `pub use glow::*;` — HitId isn't referenced here, but the import keeps the symbol available for future glow variants that might want to stamp a hit map.
#[allow(dead_code)]
fn _unused_hit_id_ref(_: HitId) {}

/// Precompute the per-step α decay table for a ray. `alphas[0] = seed`; `alphas[k] = (alphas[k-1] * factor_256) >> 8`. Returns the populated prefix length (where `alphas[len-1] > 0`). Replaces the per-step mul on the hot path with an array lookup. 1024 entries cover the worst case (seed 0x40, factor 254 → ~528 non-zero steps).
#[inline]
fn compute_alphas(seed: u32, factor_256: u32) -> ([u32; 1024], usize) {
    let mut alphas = [0u32; 1024];
    alphas[0] = seed;
    let mut a = seed;
    let mut len = 1;
    while len < 1024 {
        a = (a * factor_256) >> 8;
        if a == 0 {
            break;
        }
        alphas[len] = a;
        len += 1;
    }
    (alphas, len)
}

/// Same decay iteration as [`compute_alphas`] but returns only the length (`alpha_len`). Callers that need the per-ray pixel reach to pre-size damage rects (textbox focus glow padding, chrome shadow extent) use this so the geometry the host clears + finalizes matches *exactly* what the rasterizer will paint — no early cutoff at the bbox edge, no over-clearing past where rays actually decay to zero.
pub fn ray_reach_px(seed: u32, factor_256: u32) -> usize {
    if seed == 0 || factor_256 == 0 || factor_256 >= 256 {
        return 0;
    }
    let mut a = seed;
    let mut len = 1usize;
    while len < 1024 {
        a = (a * factor_256) >> 8;
        if a == 0 {
            break;
        }
        len += 1;
    }
    len
}

/// Right-edge glow ray pass. Per row in the pill's vertical span, scans the buffer's α byte from the right end of the bbox leftward to find that row's first fully-opaque pixel (α=0xFF — the pill's interior, set by the squircle's writes), then walks rightward writing a glow pixel each step whose α decays exponentially from `seed` via `factor_256` — same curve as [`crate::paint::paint_shadow`], just emitting white at 0°/180° instead of black at 45°. Each write composes UNDER the existing target via [`Blend::under`] (Normal).
///
/// `factor_256 ∈ [1, 255]`: 240 ≈ ×0.9375 per step (~60-pixel reach at seed=0x80), 250 ≈ ×0.9766 (~150-pixel). Caller derives this from `effective_span` (or font_size) so glow reach is RU-invariant. Uses [`compute_alphas`] to precompute the decay table — hot loop is a table lookup.
pub fn apply_textbox_glow_right(
    canvas: &mut Canvas,
    pill_x: isize,
    pill_y: isize,
    pill_w: isize,
    pill_h: isize,
    glow_colour: u32,
    seed: u32,
    factor_256: u32,
    clip: Option<Clip>,
) {
    let buf_w = canvas.width;
    let buf_h = canvas.height;
    if pill_w <= 0 || pill_h <= 0 || seed == 0 || factor_256 == 0 || factor_256 >= 256 {
        return;
    }
    let buf_w_i = buf_w as isize;
    let buf_h_i = buf_h as isize;
    let clip_rect = Clip::resolve(clip, buf_w, buf_h);
    let y0 = (pill_y.max(0) as usize).max(clip_rect.y_start);
    let y1 = ((pill_y + pill_h).min(buf_h_i).max(0) as usize).min(clip_rect.y_end);
    if y0 >= y1 {
        return;
    }
    let scan_left = (pill_x.max(0) as usize).max(clip_rect.x_start);
    let scan_right = ((pill_x + pill_w).min(buf_w_i).max(0) as usize).min(clip_rect.x_end);
    if scan_left >= scan_right {
        return;
    }

    let (alphas, alpha_len) = compute_alphas(seed, factor_256);
    {
        let dx0 = scan_left;
        let dx1 = (scan_right + alpha_len).min(buf_w).min(clip_rect.x_end);
        canvas.damage.add_bounds(dx0, y0, dx1, y1);
    }

    let glow_rgb = glow_colour & 0x00FF_FFFF;
    let pixels: &mut [u32] = canvas.pixels;
    let ray_x_end = clip_rect.x_end.min(buf_w);

    for y in y0..y1 {
        let row_base = y * buf_w;
        let mut opaque_x: Option<usize> = None;
        for x in (scan_left..scan_right).rev() {
            if (pixels[row_base + x] >> 24) == 0xFF {
                opaque_x = Some(x);
                break;
            }
        }
        let Some(ox) = opaque_x else {
            continue;
        };
        let ray_start = ox + 1;
        for k in 0..alpha_len {
            let bx = ray_start + k;
            if bx >= ray_x_end {
                break;
            }
            let new_pixel = (alphas[k] << 24) | glow_rgb;
            let idx = row_base + bx;
            pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
        }
    }
}

/// Left-edge glow ray pass. Mirror of [`apply_textbox_glow_right`]: per row in the pill's vertical span, scans the buffer's α byte from the LEFT end of the bbox rightward to find the first fully-opaque pixel, then walks `reach_px` steps LEFT writing a linearly-tapered glow underneath. Each write composes UNDER the existing target via [`Blend::under`] (Normal).
pub fn apply_textbox_glow_left(
    canvas: &mut Canvas,
    pill_x: isize,
    pill_y: isize,
    pill_w: isize,
    pill_h: isize,
    glow_colour: u32,
    seed: u32,
    factor_256: u32,
    clip: Option<Clip>,
) {
    let buf_w = canvas.width;
    let buf_h = canvas.height;
    if pill_w <= 0 || pill_h <= 0 || seed == 0 || factor_256 == 0 || factor_256 >= 256 {
        return;
    }
    let buf_w_i = buf_w as isize;
    let buf_h_i = buf_h as isize;
    let clip_rect = Clip::resolve(clip, buf_w, buf_h);
    let y0 = (pill_y.max(0) as usize).max(clip_rect.y_start);
    let y1 = ((pill_y + pill_h).min(buf_h_i).max(0) as usize).min(clip_rect.y_end);
    if y0 >= y1 {
        return;
    }
    let scan_left = (pill_x.max(0) as usize).max(clip_rect.x_start);
    let scan_right = ((pill_x + pill_w).min(buf_w_i).max(0) as usize).min(clip_rect.x_end);
    if scan_left >= scan_right {
        return;
    }

    let (alphas, alpha_len) = compute_alphas(seed, factor_256);
    {
        let dx0 = scan_left.saturating_sub(alpha_len).max(clip_rect.x_start);
        let dx1 = scan_right;
        canvas.damage.add_bounds(dx0, y0, dx1, y1);
    }

    let glow_rgb = glow_colour & 0x00FF_FFFF;
    let pixels: &mut [u32] = canvas.pixels;
    let ray_x_start = clip_rect.x_start;

    for y in y0..y1 {
        let row_base = y * buf_w;
        let mut opaque_x: Option<usize> = None;
        for x in scan_left..scan_right {
            if (pixels[row_base + x] >> 24) == 0xFF {
                opaque_x = Some(x);
                break;
            }
        }
        let Some(ox) = opaque_x else {
            continue;
        };
        if ox == 0 {
            continue;
        }
        let ray_start = ox - 1;
        for k in 0..alpha_len {
            if ray_start < k {
                break;
            }
            let bx = ray_start - k;
            if bx < ray_x_start {
                break;
            }
            let new_pixel = (alphas[k] << 24) | glow_rgb;
            let idx = row_base + bx;
            pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
        }
    }
}

/// Top-edge glow ray pass. Per column in the pill's horizontal span, scans the buffer's α byte from the TOP of the bbox downward to find the first fully-opaque pixel (α=0xFF — the squircle's interior), then walks `reach_px` steps UP writing a glow pixel each step whose α tapers linearly from `seed` down toward 0. Each write composes UNDER existing target via [`Blend::under`] (Normal).
///
/// Why the scan only catches squircle pixels: the horizontal glow passes (left/right) write α ≤ `seed_horizontal` (typically 0x80) which is well below 0xFF, so they don't trigger the "fully opaque" scan condition — the vertical pass cleanly walks past horizontal glow pixels until it reaches the silhouette's actual α=0xFF interior.
pub fn apply_textbox_glow_top(
    canvas: &mut Canvas,
    pill_x: isize,
    pill_y: isize,
    pill_w: isize,
    pill_h: isize,
    glow_colour: u32,
    seed: u32,
    factor_256: u32,
    clip: Option<Clip>,
) {
    let buf_w = canvas.width;
    let buf_h = canvas.height;
    if pill_w <= 0 || pill_h <= 0 || seed == 0 || factor_256 == 0 || factor_256 >= 256 {
        return;
    }
    let buf_w_i = buf_w as isize;
    let buf_h_i = buf_h as isize;
    let clip_rect = Clip::resolve(clip, buf_w, buf_h);
    let x0 = (pill_x.max(0) as usize).max(clip_rect.x_start);
    let x1 = ((pill_x + pill_w).min(buf_w_i).max(0) as usize).min(clip_rect.x_end);
    if x0 >= x1 {
        return;
    }
    let scan_top = (pill_y.max(0) as usize).max(clip_rect.y_start);
    let scan_bot = ((pill_y + pill_h).min(buf_h_i).max(0) as usize).min(clip_rect.y_end);
    if scan_top >= scan_bot {
        return;
    }

    let (alphas, alpha_len) = compute_alphas(seed, factor_256);
    {
        let dy0 = scan_top.saturating_sub(alpha_len).max(clip_rect.y_start);
        let dy1 = scan_bot;
        canvas.damage.add_bounds(x0, dy0, x1, dy1);
    }

    let glow_rgb = glow_colour & 0x00FF_FFFF;
    let pixels: &mut [u32] = canvas.pixels;
    let ray_y_start = clip_rect.y_start;

    for x in x0..x1 {
        let mut opaque_y: Option<usize> = None;
        for y in scan_top..scan_bot {
            if (pixels[y * buf_w + x] >> 24) == 0xFF {
                opaque_y = Some(y);
                break;
            }
        }
        let Some(oy) = opaque_y else {
            continue;
        };
        if oy == 0 {
            continue;
        }
        let ray_start = oy - 1;
        for k in 0..alpha_len {
            if ray_start < k {
                break;
            }
            let by = ray_start - k;
            if by < ray_y_start {
                break;
            }
            let new_pixel = (alphas[k] << 24) | glow_rgb;
            let idx = by * buf_w + x;
            pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
        }
    }
}

/// Bottom-edge glow ray pass. Mirror of [`apply_textbox_glow_top`]: per column in the pill's horizontal span, scans the buffer's α byte from the BOTTOM of the bbox upward to find the first fully-opaque pixel, then walks `reach_px` steps DOWN writing a linearly-tapered glow underneath.
pub fn apply_textbox_glow_bottom(
    canvas: &mut Canvas,
    pill_x: isize,
    pill_y: isize,
    pill_w: isize,
    pill_h: isize,
    glow_colour: u32,
    seed: u32,
    factor_256: u32,
    clip: Option<Clip>,
) {
    let buf_w = canvas.width;
    let buf_h = canvas.height;
    if pill_w <= 0 || pill_h <= 0 || seed == 0 || factor_256 == 0 || factor_256 >= 256 {
        return;
    }
    let buf_w_i = buf_w as isize;
    let buf_h_i = buf_h as isize;
    let clip_rect = Clip::resolve(clip, buf_w, buf_h);
    let x0 = (pill_x.max(0) as usize).max(clip_rect.x_start);
    let x1 = ((pill_x + pill_w).min(buf_w_i).max(0) as usize).min(clip_rect.x_end);
    if x0 >= x1 {
        return;
    }
    let scan_top = (pill_y.max(0) as usize).max(clip_rect.y_start);
    let scan_bot = ((pill_y + pill_h).min(buf_h_i).max(0) as usize).min(clip_rect.y_end);
    if scan_top >= scan_bot {
        return;
    }

    let (alphas, alpha_len) = compute_alphas(seed, factor_256);
    {
        let dy0 = scan_top;
        let dy1 = (scan_bot + alpha_len).min(buf_h).min(clip_rect.y_end);
        canvas.damage.add_bounds(x0, dy0, x1, dy1);
    }

    let glow_rgb = glow_colour & 0x00FF_FFFF;
    let pixels: &mut [u32] = canvas.pixels;
    let ray_y_end = clip_rect.y_end.min(buf_h);

    for x in x0..x1 {
        let mut opaque_y: Option<usize> = None;
        for y in (scan_top..scan_bot).rev() {
            if (pixels[y * buf_w + x] >> 24) == 0xFF {
                opaque_y = Some(y);
                break;
            }
        }
        let Some(oy) = opaque_y else {
            continue;
        };
        let ray_start = oy + 1;
        for k in 0..alpha_len {
            let by = ray_start + k;
            if by >= ray_y_end {
                break;
            }
            let new_pixel = (alphas[k] << 24) | glow_rgb;
            let idx = by * buf_w + x;
            pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
        }
    }
}

/// Paint a 4-directional blur glow around a textbox pill silhouette.
///
/// Loop structure + adder/intensity math ported from photon's `apply_textbox_glow` (`compositing.rs:4479`). Pixel writes compose UNDER whatever's already in the buffer via [`Blend::under`] (Normal) — same kernel as every other fluor primitive. Caller paints squircle FIRST (topmost), then this; the squircle's opaque interior takes the under() early-out so glow stays outside the silhouette, AA-edge pixels (α<255) let glow bleed through the soft transition.
pub fn apply_textbox_glow(
    canvas: &mut Canvas,
    mask: &[u8],
    center_x: isize,
    center_y: isize,
    box_width: usize,
    box_height: usize,
    glow_colour: u32,
) {
    let buf_w = canvas.width;
    let buf_h = canvas.height;
    let blur_h = 32usize;
    let blur_v = 16usize;

    let half_h = (box_height / 2) as isize;
    if center_y < half_h || (center_y + half_h) as usize >= buf_h {
        return;
    }
    if center_x < 0 || (center_x as usize) >= buf_w {
        return;
    }
    let cy = center_y as usize;
    let cx = center_x as usize;

    let y_top = cy - box_height / 2;
    let y_bot = cy + box_height / 2;
    // Damage = glow halo bbox (box + lateral blur padding, vertical box + vertical blur).
    {
        let x0 = cx.saturating_sub(box_width / 2 + blur_h);
        let x1 = (cx + box_width / 2 + blur_h).min(buf_w);
        let y0 = y_top.saturating_sub(blur_v);
        let y1 = (y_bot + blur_v).min(buf_h);
        canvas.damage.add_bounds(x0, y0, x1, y1);
    }
    let pixels: &mut [u32] = canvas.pixels;

    // Find horizontal bounds by scanning mask at center row, starting at the pill center.
    let mut x_left = cx;
    let mut x_right = cx;
    let scan = cy * buf_w;
    for lx in (0..cx).rev() {
        if mask[scan + lx] > 0 {
            x_left = lx;
        } else {
            break;
        }
    }
    for rx in cx..buf_w {
        if mask[scan + rx] > 0 {
            x_right = rx;
        } else {
            break;
        }
    }

    let corner_r = 2 * box_width * box_height / (box_width + box_height);
    let xvs = x_left + corner_r;
    let xve = x_right - corner_r;
    let yhs = y_top + corner_r;
    let yhe = y_bot - corner_r;

    let glow_rgb = glow_colour & 0x00FF_FFFF;

    // Right blur.
    for y in y_top..y_bot {
        let mut adder = 0u32;
        let start = x_right
            - (yhs as isize - y as isize).max(0) as usize
            - (y as isize - yhe as isize).max(0) as usize;
        for bx in start..x_right + blur_h {
            if bx >= buf_w {
                break;
            }
            let idx = y * buf_w + bx;
            if bx > 0 && mask[idx] < mask[idx - 1] {
                adder += (mask[idx - 1] - mask[idx]) as u32;
            }
            adder = (adder * 15 >> 4).min(71);
            let intensity = (adder * (255 - mask[idx]) as u32) >> 8;
            if intensity > 0 {
                let new_pixel = (intensity << 24) | glow_rgb;
                pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
            }
        }
    }
    // Left blur.
    for y in y_top..y_bot {
        let mut adder = 0u32;
        let end = x_left
            + (yhs as isize - y as isize).max(0) as usize
            + (y as isize - yhe as isize).max(0) as usize;
        for bx in (x_left.saturating_sub(blur_h)..=end).rev() {
            let idx = y * buf_w + bx;
            if bx + 1 < buf_w && mask[idx] < mask[idx + 1] {
                adder += (mask[idx + 1] - mask[idx]) as u32;
            }
            adder = (adder * 15 >> 4).min(71);
            let intensity = (adder * (255 - mask[idx]) as u32) >> 8;
            if intensity > 0 {
                let new_pixel = (intensity << 24) | glow_rgb;
                pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
            }
        }
    }
    // Down blur.
    for bx in x_left..x_right {
        let mut adder = 0u32;
        let start = y_bot
            - (xvs as isize - bx as isize).max(0) as usize
            - (bx as isize - xve as isize).max(0) as usize;
        for by in start..y_bot + blur_v {
            if by >= buf_h {
                break;
            }
            let idx = by * buf_w + bx;
            if by > 0 {
                let ia = (by - 1) * buf_w + bx;
                if mask[idx] < mask[ia] {
                    adder += (mask[ia] - mask[idx]) as u32;
                }
            }
            adder = (adder * 3 >> 2).min(70);
            let intensity = (adder * (255 - mask[idx]) as u32) >> 8;
            if intensity > 0 {
                let new_pixel = (intensity << 24) | glow_rgb;
                pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
            }
        }
    }
    // Up blur.
    for bx in x_left..x_right {
        let mut adder = 0u32;
        let end = y_top
            + (xvs as isize - bx as isize).max(0) as usize
            + (bx as isize - xve as isize).max(0) as usize;
        for by in (0..=end).rev() {
            if by + blur_v < y_top {
                break;
            }
            if by >= buf_h {
                continue;
            }
            let idx = by * buf_w + bx;
            if by + 1 < buf_h {
                let ib = (by + 1) * buf_w + bx;
                if mask[idx] < mask[ib] {
                    adder += (mask[ib] - mask[idx]) as u32;
                }
            }
            adder = (adder * 3 >> 2).min(70);
            let intensity = (adder * (255 - mask[idx]) as u32) >> 8;
            if intensity > 0 {
                let new_pixel = (intensity << 24) | glow_rgb;
                pixels[idx] = pixels[idx].under(new_pixel, BlendMode::Normal);
            }
        }
    }
}