Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions src/tabs.mach
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ pub rec Event {
rec Geom {
rh: f32;
pad: f32;
sep: f32;
lh: f32;
x0: f32;
x1: f32;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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; }
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 45 additions & 1 deletion src/text.mach
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
Loading