diff --git a/src/tabs.mach b/src/tabs.mach index 5316ac5..d2fc753 100644 --- a/src/tabs.mach +++ b/src/tabs.mach @@ -151,6 +151,7 @@ pub rec Event { rec Geom { rh: f32; pad: f32; + sep: f32; lh: f32; x0: f32; x1: f32; @@ -366,6 +367,12 @@ fun width(ctx: *context.Context, g: *Geom, t: *Tab) f32 { ret lo + (natural(ctx, g, t) - lo) * g.fit; } +# the room a tab takes on the strip this frame: its body and the separator +# after it, which the body is drawn short of, so the body keeps its whole width +fun room(ctx: *context.Context, g: *Geom, t: *Tab) f32 { + ret width(ctx, g, t) + g.sep; +} + # the furthest the strip scrolls fun most(g: *Geom) f32 { ret maxf(g.total - (g.x1 - g.x0), 0.0::f32); @@ -376,14 +383,15 @@ fun measure(ctx: *context.Context, tabs: *Tab, n: usize) Geom { var g: Geom; val ts: *theme.Style = context.look_of(ctx, theme.TAB); g.pad = context.px(ctx, ts.inset.l); + g.sep = context.px(ctx, ts.margin.r); g.lh = context.line_height(ctx); g.rh = maxf(context.px(ctx, ts.min_h), g.lh + context.px(ctx, ts.inset.t + ts.inset.b)); var nat: f32 = 0.0::f32; var lo: f32 = 0.0::f32; var i: usize = 0; for (i < n) { - nat = nat + natural(ctx, ?g, ?tabs[i]); - lo = lo + least(ctx, ?g, ?tabs[i]); + nat = nat + natural(ctx, ?g, ?tabs[i]) + g.sep; + lo = lo + least(ctx, ?g, ?tabs[i]) + g.sep; i = i + 1; } val r: context.Area = widget.place(ctx, layout.fill(1.0::f32), layout.fit(), nat, g.rh); @@ -404,7 +412,7 @@ fun measure(ctx: *context.Context, tabs: *Tab, n: usize) Geom { g.total = 0.0::f32; i = 0; for (i < n) { - g.total = g.total + width(ctx, ?g, ?tabs[i]); + g.total = g.total + room(ctx, ?g, ?tabs[i]); i = i + 1; } ret g; @@ -415,7 +423,7 @@ fun reveal(ctx: *context.Context, g: *Geom, s: *State, tabs: *Tab, n: usize) { var x: f32 = 0.0::f32; var i: usize = 0; for (i < n) { - val w: f32 = width(ctx, g, ?tabs[i]); + val w: f32 = room(ctx, g, ?tabs[i]); if (i == s.index) { val view: f32 = g.x1 - g.x0; if (x < s.scroll) { s.scroll = x; } @@ -435,7 +443,7 @@ fun step(ctx: *context.Context, g: *Geom, s: *State, tabs: *Tab, n: usize, right var i: usize = 0; val eps: f32 = 0.5::f32; for (i < n) { - val w: f32 = width(ctx, g, ?tabs[i]); + val w: f32 = room(ctx, g, ?tabs[i]); if (right && x + w > s.scroll + view + eps) { ret x + w - view; } if (!right && x < s.scroll - eps) { to = x; } x = x + w; @@ -585,7 +593,7 @@ pub fun bar(ctx: *context.Context, key: str, tabs: *Tab, n: usize) Event { var over: interact.Hit; var slot: usize = n; var mark: f32 = g.x0 + g.total - s.scroll; - val seps: f32 = context.px(ctx, context.look_of(ctx, theme.TAB).margin.r); + val seps: f32 = g.sep; var x: f32 = g.x0 - s.scroll; var i: usize = 0; val grp: interact.Group = interact.begin_group(ctx, id, interact.ROW); @@ -594,7 +602,7 @@ pub fun bar(ctx: *context.Context, key: str, tabs: *Tab, n: usize) Event { for (i < n) { val t: *Tab = ?tabs[i]; val x0: f32 = x; - val x1: f32 = x + width(ctx, ?g, t); + val x1: f32 = x + room(ctx, ?g, t); x = x1; val tid: u64 = tab_id(id, t.key); val h: interact.Hit = interact.hit(ctx, tid, x0, g.y0, x1 - seps, g.y1, input.BUTTON_LEFT | input.BUTTON_MIDDLE | interact.FOCUS); @@ -754,6 +762,39 @@ fun keys(a: *App) u64 { ret k; } +test bar__a_tab_with_room_is_drawn_its_whole_width { + var al: allo.Allocator; + val made: err[allo.Error] = page.make(?al); + if (sel made.err) { ret 1; } + var a: App; + var d: driver.Driver; + if (!app(?d, ?al, ?a, 6, 1600.0::f32)) { ret 1; } + a.tabs[1].closable = false; + a.tabs[4].closable = false; + driver.step(?d); + driver.step(?d); + val ts: *theme.Style = context.look_of(?d.ctx, theme.TAB); + var g: Geom; + g.pad = context.px(?d.ctx, ts.inset.l); + g.sep = context.px(?d.ctx, ts.margin.r); + g.lh = context.line_height(?d.ctx); + g.fit = 1.0::f32; + if (g.sep <= 0.0::f32) { ret 2; } + # each tab's drawn rect, short of its separator, is its whole natural width, + # so its label box is exactly its label + val bid: u64 = context.id_of(?d.ctx, "tabs"); + var i: usize = 0; + for (i < a.n) { + val r: opt[context.Area] = driver.rect(?d, tab_id(bid, a.tabs[i].key)); + if (sel r.none) { ret 3; } + if (r.some.x1 - r.some.x0 != natural(?d.ctx, ?g, ?a.tabs[i])) { ret 4; } + i = i + 1; + } + val f: err[allo.Error] = driver.free(?d); + if (sel f.err) { ret 1; } + ret 0; +} + test bar__unshrunk_tabs_are_their_natural_width { var al: allo.Allocator; val made: err[allo.Error] = page.make(?al); diff --git a/src/text.mach b/src/text.mach index 4c0f66b..71cf631 100644 --- a/src/text.mach +++ b/src/text.mach @@ -198,6 +198,11 @@ fun cut_at(ctx: *context.Context, s: str, n: usize, x: f32, y: f32, w: f32, mode ret px - x; } +# how far a label may overrun its box and still count as fitting: a box sized to +# its label is built from absolute coordinates, whose rounding far from the left +# edge leaves it a hair short, and a sixty-fourth of a pixel never shows +val FIT_SLACK: f32 = 0.015625; + # draw the first n bytes of a line of text with its line box's top-left at # (x, y), as it is when it fits the box's span, else as the kind's overflow # says: clipped to the box, or cut with an ellipsis at its end to fit the @@ -218,7 +223,7 @@ fun cut_at(ctx: *context.Context, s: str, n: usize, x: f32, y: f32, w: f32, mode pub fun fit_box(ctx: *context.Context, kind: u32, s: str, n: usize, x: f32, y: f32, b: context.Area, c: draw.Color) { if (s == nil || n == 0) { ret; } val w: f32 = maxf(b.x1 - b.x0, 0.0::f32); - if (context.text_width_n(ctx, s, n) <= w) { + if (context.text_width_n(ctx, s, n) <= w + FIT_SLACK) { context.text_span(ctx, s, n, x, y, c); ret; } @@ -338,6 +343,45 @@ pub fun spans_at(ctx: *context.Context, sp: *Span, n: usize, x: f32, y: f32) f32 ret px - x; } +test text__a_label_sized_to_its_box_is_whole_anywhere { + var a: allo.Allocator; + val made: err[allo.Error] = page.make(?a); + if (sel made.err) { ret 1; } + var ctx: context.Context; + val ie: err[allo.Error] = context.init(?ctx, ?a); + if (sel ie.err) { ret 1; } + var in: input.Input; + context.begin(?ctx, in, 6000.0::f32, 100.0::f32); + val s: str = "Behavior"; + val n: usize = str_len(s); + val lw: f32 = context.text_width(?ctx, s); + val white: draw.Color = draw.rgba(1.0::f32, 1.0::f32, 1.0::f32, 1.0::f32); + # boxes sized to the label as a tab or button builds them, from absolute + # coordinates far from the left edge with paddings off the pixel grid; each + # must draw the same glyphs as the label drawn whole + var k: u32 = 0; + for (k < 600) { + val x: f32 = 900.0::f32 + k::f32 * 7.31::f32; + val pad: f32 = 1.0::f32 + (k % 37)::f32 * 0.173::f32; + val b: context.Area = context.Area{x0: x + pad, y0: 0.0::f32, x1: (x + (pad * 2.0::f32 + lw)) - pad, y1: 20.0::f32}; + val ref: usize = context.draw_count(?ctx); + context.text_span(?ctx, s, n, x + pad, 0.0::f32, white); + val at: usize = context.draw_count(?ctx); + fit_box(?ctx, 0, s, n, x + pad, 0.0::f32, b, white); + if (context.draw_count(?ctx) - at != at - ref) { ret 2; } + val p: *draw.Vert = context.draw_verts(?ctx); + var i: usize = 0; + for (i < at - ref) { + if (p[at + i].u != p[ref + i].u || p[at + i].v != p[ref + i].v) { ret 3; } + i = i + 1; + } + k = k + 1; + } + val freed: err[allo.Error] = context.free(?ctx); + if (sel freed.err) { ret 1; } + ret 0; +} + test text__cuts_and_spans { var a: allo.Allocator; val made: err[allo.Error] = page.make(?a);