makepad/widgets/sheets/themes/segment-display/widgets.splash
Admin 53e0e85b92 draw and widgets: Slug text everywhere, no blank glyphs, slimmer widgets
All outline text draws with Slug curves on every platform (SDF/MSDF stays
selectable per size); a frame never presents a blank glyph and retained draw
lists keep theirs. One coverage model for vector feathering, widget families
split out of the core crate, fewer steady-state allocations, video keeps its
aspect (Contain/Cover), render and SSAO pass unification.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

2619 lines
128 KiB
Text

use mod.prelude.widgets_internal.*
// A segment display on a pale glass: the face of a digital wristwatch and
// the screen of a handheld game console with a dot-matrix screen.
// The glass is flat and holds four shades and no more: ink, a dark and a
// light middle, and the pale ground; anything between them is a dither,
// never a veil. Every mark on it is ink, the unlit segments stay faintly
// there as ghosts, and the ink floats a hair above the reflector, so it
// throws a faint shadow down and to the left. Keys are pixel-framed tiles
// or inverse legends; groups are glass windows set in a dark frame with a
// light lip under it. A state is a flag, a filled segment or an inverse
// legend. Nothing glows.
mod.widgets.ButtonFlat.padding = mod.turtle.Inset{left: 10.0 right: 10.0 top: 2.5 bottom: 2.5}
mod.widgets.ButtonFlat.min_height = 24.0
mod.widgets.ButtonFlat.spacing = 6.0
mod.widgets.Button.draw_bg.border_radius = 1.5
mod.widgets.Button.draw_bg.border_size = 0.34
// A ROW OF FIELDS IS ONE HEIGHT: the box metrics are stated once here and
// handed to each field below. Buttons stand at the same 24.
let field_height = 24.0
let field_pad_v = 2.5
let field_radius = 1.5
let field_border = 0.34
// The room a Fit field takes: where a field is Fit, the padding IS its
// height.
let field_room = fn(w) {
w.min_height = field_height
w.padding.top = field_pad_v
w.padding.bottom = field_pad_v
}
// The box a field draws. Given the draw object and not the widget, because
// not every field draws its own box.
let field_face = fn(d) {
d.border_radius = field_radius
d.border_size = field_border
}
// A field that states a frame and lays its own parts out inside it takes
// the height alone: a padding would displace its line.
let field_frame = fn(w) {
w.min_height = field_height
}
field_room(mod.widgets.TextInput)
field_face(mod.widgets.TextInput.draw_bg)
field_room(mod.widgets.ComboBox)
field_face(mod.widgets.ComboBox.draw_bg)
field_room(mod.widgets.DropDown)
field_face(mod.widgets.DropDown.draw_bg)
field_room(mod.widgets.DropDown2)
field_face(mod.widgets.DropDown2.draw_bg)
field_room(mod.widgets.FieldWell)
field_face(mod.widgets.FieldWell.draw_bg)
field_room(mod.widgets.TagField)
field_face(mod.widgets.TagField.draw_bg)
field_frame(mod.widgets.NumberField)
mod.widgets.NumberField.draw_bg.border_radius = field_radius
field_frame(mod.widgets.ValueInput)
field_frame(mod.widgets.DateField)
field_frame(mod.widgets.TimeField)
field_frame(mod.widgets.DatePicker)
field_frame(mod.widgets.DateRangePicker)
// The chrome-less input a well holds must impose no height of its own.
mod.widgets.WellInput.min_height = 0.0
mod.widgets.Window.pass.clear_color = #9ca388
// Plain views stop clipping, so a frame, a lip, an ink shadow or a focus
// ring may fall into the gap beside a control. Scroll views keep theirs.
mod.widgets.View.clip_x = false
mod.widgets.View.clip_y = false
// ---------------------------------------------------------------------------
// SHARED PARTS. Distances in points, negative inside, y down; `px` is one
// device pixel in points; `o` the quad's origin in the window.
// The four shades of the glass, the ghost between the two palest, the lit
// glass of an inverted legend, and the dark frame of a window.
let lcd_ink = fn() -> vec3 { return vec3(0.161, 0.169, 0.125) }
let lcd_dark = fn() -> vec3 { return vec3(0.337, 0.353, 0.275) }
let lcd_mid = fn() -> vec3 { return vec3(0.478, 0.502, 0.396) }
let lcd_ghost = fn() -> vec3 { return vec3(0.553, 0.576, 0.482) }
let lcd_glass = fn() -> vec3 { return vec3(0.635, 0.663, 0.557) }
// The page is the glass a step down; a window's glass a step up from it.
let lcd_ground = fn() -> vec3 { return vec3(0.612, 0.639, 0.533) }
let lcd_wglass = fn() -> vec3 { return vec3(0.682, 0.710, 0.604) }
let lcd_lit = fn() -> vec3 { return vec3(0.714, 0.741, 0.635) }
let lcd_frame = fn() -> vec3 { return vec3(0.110, 0.114, 0.129) }
let lcd_red = fn() -> vec3 { return vec3(0.639, 0.149, 0.110) }
let lcd_g3 = fn(v: float) -> vec3 { return vec3(v, v, v) }
// A box of true radius r.
let lcd_rbox = fn(p: vec2, c: vec2, h: vec2, r: float) -> float {
let q = abs(p - c) - h + vec2(r, r)
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0, 0.0))) - r
}
// Premultiplied source-over.
let lcd_over = fn(dst: vec4, src: vec4) -> vec4 {
return src + dst * (1.0 - src.a)
}
// A box snapped to whole device pixels in window space, in the quad's
// local points: .xy the low corner, .zw the high one.
let lcd_snapbox = fn(o: vec2, lo: vec2, hi: vec2, px: float) -> vec4 {
let a = vec2(Finish.snap(o.x + lo.x, px), Finish.snap(o.y + lo.y, px)) - o
let b = vec2(Finish.snap(o.x + hi.x, px), Finish.snap(o.y + hi.y, px)) - o
return vec4(a.x, a.y, b.x, b.y)
}
let lcd_grow = fn(rp: vec2, rs: vec2, m: float) -> vec4 {
return vec4(rp.x - m, rp.y - m, rs.x + 2.0 * m, rs.y + 2.0 * m)
}
// The centre of the device pixel p lies in, in local points: shapes tested
// there come out as hard pixel art, stairs and all.
let lcd_pix = fn(p: vec2, o: vec2, px: float) -> vec2 {
return (floor((p + o) / px + vec2(0.001, 0.001)) + vec2(0.5, 0.5)) * px - o
}
// Dithers, counted in window pixels: a checker (half), and one pixel in
// four (quarter).
let lcd_checker = fn(p: vec2, o: vec2, px: float) -> float {
let q = floor((p + o) / px + vec2(0.001, 0.001))
return step(0.5, fract((q.x + q.y) * 0.5))
}
let lcd_quarter = fn(p: vec2, o: vec2, px: float) -> float {
let q = floor((p + o) / px + vec2(0.001, 0.001))
return step(0.5, fract(q.x * 0.5)) * step(0.5, fract(q.y * 0.5))
}
// A straight segment from a to b: the distance to its axis.
let lcd_seg = fn(p: vec2, a: vec2, b: vec2) -> float {
let pa = p - a
let ba = b - a
let t = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0)
return length(pa - ba * t)
}
// The shadow ink throws on the reflector, given the mark's distance at p
// moved back by the throw (-0.75, +1.0): a blur of a point and a half,
// about a seventh of the ink at the mark, gone within five device pixels.
let lcd_inkshade = fn(ds: float, amt: float) -> float {
return (1.0 - smoothstep(-1.0, 2.5, ds)) * amt
}
let lcd_throw = fn() -> vec2 { return vec2(-0.75, 1.0) }
// A dashed ring one device pixel wide, `gap` points outside a box edge:
// two device pixels on, two off, counted in window pixels along the side.
// It is the focus mark of every control.
let lcd_dash_ring = fn(d: float, p: vec2, c: vec2, h: vec2, o: vec2, gap: float, px: float) -> float {
let ring = Finish.band(-(d - gap - px), 0.0, px, px)
let side = step(abs(p.y - c.y) - h.y, abs(p.x - c.x) - h.x)
let t = mix(p.x + o.x, p.y + o.y, side)
let on = step(fract(floor(t / px + 0.01) * 0.25), 0.3)
return ring * on
}
// The glass in window points: the base, a sheen lighter toward the upper
// right, a one-device-pixel grain that never repeats, and the pixel matrix
// of the panel, a dot every second device pixel each way.
let lcd_glass_at = fn(base: vec3, w: vec2, px: float, sheen: float) -> vec3 {
var col = base * (1.0 + sheen)
col = col + lcd_g3(Finish.grain(w / px, 0.022))
let cell = floor(w / px + vec2(0.01, 0.01))
let dot = step(fract(cell.x / 2.0), 0.1) * step(fract(cell.y / 2.0), 0.1)
col = col - lcd_g3(0.03 * dot)
return col
}
// A 5 by 7 dot-matrix letter: O, N or F (0, 1, 2), one bit per row in each
// of five columns, the top row the lowest bit. `q` counts device pixels
// from the letter's top left.
let lcd_letter = fn(ch: float, q: vec2) -> float {
let col = q.x
var c0 = 62.0
var c1 = 65.0
var c2 = 65.0
var c3 = 65.0
var c4 = 62.0
if ch > 0.5 {
c0 = 127.0
c1 = 4.0
c2 = 8.0
c3 = 16.0
c4 = 127.0
}
if ch > 1.5 {
c0 = 127.0
c1 = 9.0
c2 = 9.0
c3 = 9.0
c4 = 1.0
}
let v = c0 * step(col, 0.5) + c1 * step(0.5, col) * step(col, 1.5) + c2 * step(1.5, col) * step(col, 2.5) + c3 * step(2.5, col) * step(col, 3.5) + c4 * step(3.5, col) * step(col, 4.5)
let bit = step(0.5, fract(floor(v / pow(2.0, q.y) + 0.001) * 0.5))
return bit * step(0.0, q.x) * step(q.x, 4.5) * step(0.0, q.y) * step(q.y, 6.5)
}
// A word of dot-matrix letters, "OFF" (0) or "ON" (1), set with its top
// left at `at` (a window-snapped point); coverage at p, in whole pixels.
let lcd_word = fn(p: vec2, o: vec2, at: vec2, px: float, word: float) -> float {
let q = floor((p + o) / px + vec2(0.001, 0.001)) - floor((at + o) / px + vec2(0.5, 0.5))
let k = floor(q.x / 6.0)
let lq = vec2(q.x - k * 6.0, q.y)
let n = mix(3.0, 2.0, word)
// OFF: O F F; ON: O N
let ch = mix(mix(0.0, 2.0, step(0.5, k)), mix(0.0, 1.0, step(0.5, k)), word)
return lcd_letter(ch, lq) * step(0.0, k) * step(k, n - 0.5)
}
// A small seven-segment numeral row: the scale numbers 0 25 50 75 100 (k
// = 0..4), each set on its tick, the first flush left and the last flush
// right. Returns coverage, and .y the distance to the nearest number's
// extent (for the rule that joins them).
let lcd_scale_numbers = fn(p: vec2, x0: float, x1: float, y0: float, dh: float, px: float) -> vec2 {
let span = max(x1 - x0, 1.0)
let k = clamp(floor((p.x - x0) / (span * 0.25) + 0.5), 0.0, 4.0)
let ax = x0 + k * span * 0.25
let n = mix(mix(1.0, 2.0, step(0.5, k)), 3.0, step(3.5, k))
let adv = dh * 0.68
let cw = dh * 0.5
let w = (n - 1.0) * adv + cw
let align = mix(mix(0.0, 0.5, step(0.5, k)), 1.0, step(3.5, k))
let left = ax - w * align
let j = floor((p.x - left) / adv)
let u = (p.x - left - j * adv) / cw
// the digits of 0, 25, 50, 75, 100
var dg = 0.0
if k > 0.5 && k < 1.5 {
dg = mix(2.0, 5.0, step(0.5, j))
}
if k > 1.5 && k < 2.5 {
dg = mix(5.0, 0.0, step(0.5, j))
}
if k > 2.5 && k < 3.5 {
dg = mix(7.0, 5.0, step(0.5, j))
}
if k > 3.5 {
dg = mix(1.0, 0.0, step(0.5, j))
}
let uv = vec2(u, (p.y - y0) / dh)
let inside = step(0.0, j) * step(j, n - 0.5) * step(0.0, u) * step(u, 1.0)
let a = Finish.seg7(uv, dg, px / dh) * inside
let ext = max(left - p.x, p.x - (left + w))
return vec2(a, ext)
}
// ---------------------------------------------------------------------------
// BUTTONS. One face for the family; the template picks the kind:
// 0 a cell drawn in ink on the glass, framed by `frame` (0 none, 1 a
// one-pixel outline, 2 a dashed outline, 3 an ink rule under the word);
// 1 a pixel-framed tile, the frame two device pixels of ink with stepped
// corners; 2 the inverse tile, filled with ink, lit glass letters;
// 3 the warning tile, its frame and word printed in red on the glass,
// filled red while held; 4 a tile framed twice.
// Under the pointer a glass face lays the checker dither; pressed, it
// inverts; an inverse face turns back to glass.
// The quad is grown 5 points for the ink shadow and the focus ring.
let lcd_button_face = fn(p: vec2, sb: vec4, o: vec2, px: float, kind: float, frame: float, hover: float, down: float, focus: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let held = down * live
let pq = lcd_pix(p, o, px)
let tile = step(0.5, kind)
// the stepped outline of a tile, tested on pixel centres; a cell is
// square
let k = mix(0.01, 3.0 * px, tile)
let dq = Finish.sd_chamfer(pq, c, h, k)
let d = mix(lcd_rbox(p, c, h, 0.01), dq, tile)
let inside = mix(Finish.cover(d, px), step(dq, 0.0), tile)
let inverse = step(1.5, kind) * step(kind, 2.5)
let red = step(2.5, kind) * step(kind, 3.5)
let under = step(2.5, frame) * (1.0 - tile)
// what is filled: the inverse tile at rest, any glass face while held;
// the inverse tile turns to glass while held
let solid = mix(held, 1.0 - held, inverse)
// the rule under the word of the tonal cell: the label's width and two
// points either side, three points under its foot
let uy = Finish.snap(o.y + c.y + 6.5, px) - o.y
let ux0 = sb.x + 8.0
let ux1 = sb.z - 8.0
let uc = vec2((ux0 + ux1) * 0.5, uy + px * 0.5)
let uh = vec2(max((ux1 - ux0) * 0.5, 1.0), px * 0.5)
let ul = step(ux0, p.x) * step(p.x, ux1) * step(uy, p.y) * step(p.y, uy + px) * under * (1.0 - solid)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the ink's shadow on the reflector: of the fill, of the frame, of the
// rule; a held key lies flat
let th = lcd_throw()
let ds = Finish.sd_chamfer(p - th, c, h, k)
let framed = max(tile, step(0.5, frame) * step(frame, 2.5))
let shf = lcd_inkshade(ds, 0.14) * solid * (1.0 - inside) * (1.0 - held)
let shl = lcd_inkshade(abs(ds + px) - px, 0.14) * framed * (1.0 - solid) * (1.0 - inside)
let shu = lcd_inkshade(lcd_rbox(p - th, uc, uh, 0.01), 0.14) * under * (1.0 - solid) * (1.0 - ul)
let sh = max(max(shf, shl), shu) * live
out = vec4(lcd_ink() * sh, sh)
// the band under the pointer: a checker of the light middle
let chk = lcd_checker(p, o, px)
let ba = inside * chk * hover * live * (1.0 - solid)
out = lcd_over(out, vec4(lcd_mid() * ba, ba))
// the fill: ink, or on the warning tile the printed red
var fcol = mix(lcd_ink(), lcd_dark(), 0.35 * hover * live * inverse)
fcol = mix(fcol, lcd_red(), red)
let fa = inside * solid
let fill = mix(fcol, lcd_mid(), disabled * inverse)
out = lcd_over(out, vec4(fill * fa, fa))
// the frame
var line = 0.0
// a tile: two device pixels of stepped ink
line = max(line, step(dq, 0.0) * step(-2.0 * px, dq) * tile * (1.0 - inverse))
// twice: an inner line two pixels further in
line = max(line, step(dq, -3.0 * px) * step(-4.0 * px, dq) * step(3.5, kind))
// a cell: one device pixel, solid or dashed
var cl = Finish.band(d, 0.0, px, px) * step(0.5, frame) * step(frame, 2.5) * (1.0 - tile)
let side = step(abs(p.y - c.y) - h.y + 1.0, abs(p.x - c.x) - h.x + 1.0)
let t = mix(p.x + o.x, p.y + o.y, side)
let dash = step(fract(floor(t / px + 0.01) * 0.25), 0.3)
cl = cl * mix(1.0, dash, step(1.5, frame))
line = max(line, cl)
line = max(line, ul)
// the inverse tile's frame shows while it is held (turned to glass)
line = max(line, step(dq, 0.0) * step(-2.0 * px, dq) * inverse * held)
let lcol = mix(mix(lcd_ink(), lcd_red(), red), lcd_mid(), disabled)
let la = line * (1.0 - solid * (1.0 - inverse))
out = lcd_over(out, vec4(lcol * la, la))
// the inverse tile under the pointer: a glass keyline two pixels in
let kl = step(dq, -2.0 * px) * step(-3.0 * px, dq) * inverse * hover * live * (1.0 - held)
out = lcd_over(out, vec4(lcd_lit() * kl, kl))
// focus: a dashed ink ring two points out
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
// The wait spinner the stock face drew, which this one must draw too: eight
// segments round the middle of the face, the lit one walking on the
// button's own animation clock, the rest at ghost.
let lcd_wait = fn(p: vec2, c: vec2, px: float, anim: float, ink: vec3, ghost: vec3) -> vec4 {
let v = p - c
var th = atan2(v.x, -v.y)
if th < 0.0 {
th = th + 2.0 * PI
}
let k = floor(th / (PI * 0.25) + 0.5)
let ang = k * PI * 0.25
let dir = vec2(sin(ang), -cos(ang))
let along = clamp(dot(v, dir), 2.5, 5.5)
let dd = length(v - dir * along) - 0.75
let a = Finish.cover(dd, px)
let head = floor(fract(anim) * 8.0)
let age = fract((head - k) / 8.0 + 1.0)
let lit = max(1.0 - age * 3.0, 0.0)
let col = mix(ghost, ink, lit)
return vec4(col * a, a)
}
let lcd_button = fn(pos: vec2, size: vec2, o: vec2, px: float, kind: float, frame: float, hover: float, down: float, focus: float, disabled: float, loading: float, anim: float, m: float) -> vec4 {
let p = pos * (size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let sb = lcd_snapbox(o, vec2(0.0, 0.0), size, px)
var out = lcd_button_face(p, sb, o, px, kind, frame, hover, down, focus, disabled)
if loading > 0.001 {
let c = (sb.xy + sb.zw) * 0.5
let dark = step(1.5, kind) * step(kind, 2.5)
let ink = mix(lcd_ink(), lcd_lit(), dark)
let ghost = mix(lcd_ghost(), lcd_dark(), dark)
let w = lcd_wait(p, c, px, anim, ink, ghost)
out = lcd_over(out, w * loading)
}
return out
}
mod.widgets.ButtonFlat.draw_bg.kind = uniform(0.0)
mod.widgets.ButtonFlat.draw_bg.frame = uniform(0.0)
mod.widgets.ButtonFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ButtonFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return self.host_over(lcd_button(self.pos, self.rect_size, self.rect_pos, px, self.kind, self.frame, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, 5.0), 5.0)
}
mod.widgets.ButtonFlat.clip_x = false
mod.widgets.ButtonFlat.clip_y = false
// Inks. On the glass: ink, turned to the lit glass when the face inverts.
mod.widgets.ButtonFlat.draw_text.color = #292b20
mod.widgets.ButtonFlat.draw_text.color_hover = #292b20
mod.widgets.ButtonFlat.draw_text.color_down = #b6bda2
mod.widgets.ButtonFlat.draw_text.color_focus = #292b20
mod.widgets.ButtonFlat.draw_text.color_disabled = #6c725c
mod.widgets.ButtonFlat.draw_text.text_style = mod.theme.font_label{}
mod.widgets.ButtonFlat.draw_icon.color = #292b20
mod.widgets.ButtonFlat.icon_walk.width = 14.0
// The tiles: the standard key and everything cut from it. They are handed
// a draw object of their own, since most of them share the flat button's.
let lcd_tile_bg = mod.widgets.ButtonFlat.draw_bg{
kind: 1.0
frame: 0.0
}
let lcd_tiled = fn(w) {
w.draw_bg = lcd_tile_bg
}
lcd_tiled(mod.widgets.Button)
lcd_tiled(mod.widgets.ButtonGradientX)
lcd_tiled(mod.widgets.ButtonGradientY)
lcd_tiled(mod.widgets.ButtonIcon)
lcd_tiled(mod.widgets.ButtonGradientXIcon)
lcd_tiled(mod.widgets.ButtonGradientYIcon)
lcd_tiled(mod.widgets.ButtonXs)
lcd_tiled(mod.widgets.ButtonSm)
lcd_tiled(mod.widgets.ButtonLg)
lcd_tiled(mod.widgets.ButtonXl)
lcd_tiled(mod.widgets.ButtonCompound)
// The primary: the inverse tile, the one filled key of a row.
mod.widgets.ButtonPrimary.draw_bg.kind = 2.0
mod.widgets.ButtonPrimary.draw_text.color = #b6bda2
mod.widgets.ButtonPrimary.draw_text.color_hover = #c6cdb2
mod.widgets.ButtonPrimary.draw_text.color_down = #292b20
mod.widgets.ButtonPrimary.draw_text.color_focus = #b6bda2
mod.widgets.ButtonPrimary.draw_text.color_disabled = #3a3d2e
mod.widgets.ButtonPrimary.draw_text.text_style = mod.theme.font_bold{}
mod.widgets.ButtonPrimary.draw_icon.color = #b6bda2
// The second action: the tile framed twice.
mod.widgets.ButtonSecondary.draw_bg.kind = 4.0
mod.widgets.ButtonSecondary.draw_text.color = #292b20
mod.widgets.ButtonSecondary.draw_text.color_hover = #292b20
mod.widgets.ButtonSecondary.draw_text.color_down = #b6bda2
mod.widgets.ButtonSecondary.draw_text.color_focus = #292b20
mod.widgets.ButtonSecondary.draw_text.color_disabled = #6c725c
mod.widgets.ButtonSecondary.draw_text.text_style = mod.theme.font_label{}
mod.widgets.ButtonSecondary.draw_icon.color = #292b20
// The tonal action: a bare word with an ink rule under it; the pointer
// lays the dither behind it, a press inverts it.
mod.widgets.ButtonTertiary.draw_bg.kind = 0.0
mod.widgets.ButtonTertiary.draw_bg.frame = 3.0
mod.widgets.ButtonTertiary.draw_text.color = #292b20
mod.widgets.ButtonTertiary.draw_text.color_hover = #292b20
mod.widgets.ButtonTertiary.draw_text.color_down = #b6bda2
mod.widgets.ButtonTertiary.draw_text.color_focus = #292b20
mod.widgets.ButtonTertiary.draw_text.color_disabled = #6c725c
mod.widgets.ButtonTertiary.draw_text.text_style = mod.theme.font_label{}
mod.widgets.ButtonTertiary.draw_icon.color = #292b20
// The destructive action: the pixel tile with its frame and its word
// printed in red on the glass, the one colour of the sheet and only where
// it means a warning; held, the tile fills with the red.
mod.widgets.ButtonDanger.draw_bg.kind = 3.0
mod.widgets.ButtonDanger.draw_bg.frame = 0.0
mod.widgets.ButtonDanger.draw_text.color = #a3261c
mod.widgets.ButtonDanger.draw_text.color_hover = #a3261c
mod.widgets.ButtonDanger.draw_text.color_down = #b6bda2
mod.widgets.ButtonDanger.draw_text.color_focus = #a3261c
mod.widgets.ButtonDanger.draw_text.color_disabled = #6c725c
mod.widgets.ButtonDanger.draw_text.text_style = mod.theme.font_label{}
mod.widgets.ButtonDanger.draw_icon.color = #a3261c
// The outline: the unit box of the panel, one device pixel of ink round a
// glass cell; dashed, the same line broken.
mod.widgets.ButtonOutline.draw_bg.kind = 0.0
mod.widgets.ButtonOutline.draw_bg.frame = 1.0
mod.widgets.ButtonOutline.draw_text.color = #292b20
mod.widgets.ButtonOutline.draw_text.color_hover = #292b20
mod.widgets.ButtonOutline.draw_text.color_down = #b6bda2
mod.widgets.ButtonOutline.draw_text.color_focus = #292b20
mod.widgets.ButtonOutline.draw_text.color_disabled = #6c725c
mod.widgets.ButtonOutline.draw_text.text_style = mod.theme.font_label{}
mod.widgets.ButtonOutline.draw_icon.color = #292b20
mod.widgets.ButtonDashed.draw_bg.frame = 2.0
mod.widgets.ButtonDashed.draw_bg.border_dash = 0.0
// The flatter faces: bare ink until the pointer comes.
mod.widgets.ButtonFlatter.draw_bg.kind = 0.0
mod.widgets.ButtonFlatter.draw_bg.frame = 0.0
// The size rungs keep the family's padding; a small key is 20 tall, a
// large one 28.
mod.widgets.ButtonSm.padding = mod.turtle.Inset{left: 8.0 right: 8.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonSm.min_height = 20.0
mod.widgets.ButtonXs.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonXs.min_height = 17.0
mod.widgets.ButtonLg.padding = mod.turtle.Inset{left: 12.0 right: 12.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonLg.min_height = 28.0
mod.widgets.ButtonXl.padding = mod.turtle.Inset{left: 16.0 right: 16.0 top: 0.0 bottom: 0.0}
mod.widgets.ButtonXl.min_height = 32.0
mod.widgets.ButtonIcon.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 3.0 bottom: 3.0}
// HELD STATES. The stock tracks write the pointer's hover and the press as
// snapped values, and a snapped value never arrives when a state is held
// from the start (a still page, a restored screen). The same states with
// plain values and a snapped entry look and move the same under a live
// pointer, and hold.
let lcd_held_states = fn(w) {
w.animator.hover.on.from = {all: Snap}
w.animator.hover.on.apply = {draw_bg: {down: 0.0, hover: 1.0} draw_text: {down: 0.0, hover: 1.0}}
w.animator.hover.down.from = {all: Snap}
w.animator.hover.down.apply = {draw_bg: {down: 1.0, hover: 1.0} draw_text: {down: 1.0, hover: 1.0}}
}
lcd_held_states(mod.widgets.ButtonFlat)
lcd_held_states(mod.widgets.CheckBoxFlat)
lcd_held_states(mod.widgets.ToggleFlat)
lcd_held_states(mod.widgets.RadioButtonFlat)
lcd_held_states(mod.widgets.RadioButtonTabFlat)
lcd_held_states(mod.widgets.TextInputFlat)
lcd_held_states(mod.widgets.DropDownFlat)
// ---------------------------------------------------------------------------
// CHECK BOXES. A 12 point box of one device pixel of ink; inside it the
// mark, a square segment 2.5 points in, at ghost when off and ink when on;
// mixed, a bar; an error hatches the box and doubles its line. Held, the
// whole box inverts.
mod.widgets.CheckBoxFlat.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 5.0 bottom: 5.0}
mod.widgets.CheckBoxFlat.label_walk.margin = mod.turtle.Inset{left: 19.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.CheckBoxFlat.draw_bg.size = uniform(12.0)
mod.widgets.CheckBoxFlat.draw_bg.border_size = uniform(0.34)
mod.widgets.CheckBoxFlat.draw_bg.border_radius = uniform(0.0)
mod.widgets.CheckBoxFlat.draw_text.color_error = #292b20
// The mark box of a check box, radio or toggle: where it stands in the quad.
let lcd_markbox = fn(size: vec2, o: vec2, w: float, hgt: float, at_end: float, px: float) -> vec4 {
let x = (size.x - w) * at_end
let y = size.y * 0.5 - hgt * 0.5
return lcd_snapbox(o, vec2(x, y), vec2(x + w, y + hgt), px)
}
mod.widgets.CheckBoxFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.CheckBoxFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let held = self.down * live
let sb = lcd_markbox(self.rect_size, o, self.size, self.size, self.mark_at_end, px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 0.01)
let inside = Finish.cover(d, px)
let on = self.active * (1.0 - self.mixed)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the ink's shadow: the line's, or the held box's
let ds = lcd_rbox(p - lcd_throw(), c, h, 0.01)
let sh = mix(lcd_inkshade(abs(ds) - px * 0.5, 0.14), lcd_inkshade(ds, 0.14), held) * (1.0 - inside) * live
out = vec4(lcd_ink() * sh, sh)
// the pointer lays the light dither inside
let chk = lcd_checker(p, o, px)
let ba = inside * chk * self.hover * live * (1.0 - held)
out = lcd_over(out, vec4(lcd_mid() * ba, ba))
// an error hatches the box: one pixel on the diagonal every three
let hatch = Finish.stripes(p + o, 0.785398, 3.0 * px, 0.34, px) * inside * self.error * (1.0 - held)
out = lcd_over(out, vec4(lcd_dark() * hatch, hatch))
// held, the box inverts
let ia = inside * held
out = lcd_over(out, vec4(lcd_ink() * ia, ia))
// the line, doubled by an error
var line = Finish.band(d, 0.0, px, px)
line = max(line, Finish.ring_out(d, px, 2.0 * px, px) * self.error)
let lcol = mix(lcd_ink(), lcd_mid(), self.disabled)
let la = line
out = lcd_over(out, vec4(lcol * la, la))
// the mark: a square segment, or the bar of a mixed box; unlit, a ghost
let inner = h - vec2(2.5, 2.5)
let mk = Finish.cover(lcd_rbox(p, c, inner, 0.01), px) * (1.0 - self.mixed)
let bar = Finish.cover(lcd_rbox(p, c, vec2(inner.x, 1.0), 0.01), px) * self.mixed
let lit = max(mk * on, bar)
let ghost = mk * (1.0 - on) * (1.0 - self.mixed)
let gcol = mix(lcd_ghost(), vec3(0.604, 0.631, 0.529), self.disabled)
let ga = ghost * (1.0 - held)
out = lcd_over(out, vec4(gcol * ga, ga))
let mcol = mix(mix(lcd_ink(), lcd_mid(), self.disabled), lcd_lit(), held)
out = lcd_over(out, vec4(mcol * lit, lit))
// focus: the dashed ring two points out
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * self.focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.CheckBoxFlat.clip_x = false
mod.widgets.CheckBoxFlat.clip_y = false
// TOGGLES. A flag pair: two cells in one ink box, OFF and ON in dot-matrix
// capitals; the word that holds is an inverse legend, the other stands at
// ghost. Pressed, the other cell darkens toward its turn.
mod.widgets.ToggleFlat.label_walk.margin = mod.turtle.Inset{left: 41.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.ToggleFlat.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 4.0 bottom: 4.0}
mod.widgets.ToggleFlat.draw_bg.size = uniform(16.0)
mod.widgets.ToggleFlat.draw_bg.pill_aspect = uniform(2.125)
mod.widgets.ToggleFlat.draw_bg.knob_inset = uniform(1.0)
mod.widgets.ToggleFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ToggleFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let held = self.down * live
let tw = self.size * self.pill_aspect
let sb = lcd_markbox(self.rect_size, o, tw, self.size, self.mark_at_end, px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 0.01)
let inside = Finish.cover(d, px)
// which word holds: follows the pointer while dragged
let t = step(0.5, mix(self.active, self.drag_pos, self.drag))
let mid = Finish.snap(o.x + c.x - px, px) - o.x
let in_on = step(mid, p.x)
let cur = mix(1.0 - in_on, in_on, t)
var out = vec4(0.0, 0.0, 0.0, 0.0)
let ds = lcd_rbox(p - lcd_throw(), c, h, 0.01)
let sh = lcd_inkshade(abs(ds) - px * 0.5, 0.14) * (1.0 - inside) * live
out = vec4(lcd_ink() * sh, sh)
// the other cell: dithered under the pointer, darker while pressed
let chk = lcd_checker(p, o, px)
let other = inside * (1.0 - cur)
let oa = other * chk * max(self.hover * live, held)
let ocol = mix(lcd_mid(), lcd_dark(), held)
out = lcd_over(out, vec4(ocol * oa, oa))
// the cell that holds: an ink fill (a light middle while disabled)
let fa = inside * cur
let fcol = mix(lcd_ink(), lcd_mid(), self.disabled)
out = lcd_over(out, vec4(fcol * fa, fa))
// the box and the rule between the cells
let rule = step(mid, p.x) * step(p.x, mid + px) * inside
let la = max(Finish.band(d, 0.0, px, px), rule)
let lcol = mix(lcd_ink(), lcd_mid(), self.disabled)
out = lcd_over(out, vec4(lcol * la, la))
// an error doubles the line
let el = Finish.ring_out(d, px, 2.0 * px, px) * self.error
out = lcd_over(out, vec4(lcd_ink() * el, el))
// the words, 7 pixels tall, centred in their cells
let yw = Finish.snap(o.y + c.y - 3.5 * px, px) - o.y
let xoff = Finish.snap(o.x + (sb.x + mid) * 0.5 - 8.5 * px, px) - o.x
let xon = Finish.snap(o.x + (mid + sb.z) * 0.5 - 5.5 * px, px) - o.x
let w_off = lcd_word(p, o, vec2(xoff, yw), px, 0.0)
let w_on = lcd_word(p, o, vec2(xon, yw), px, 1.0)
let lit_col = mix(lcd_lit(), lcd_glass(), self.disabled)
let ghost_col = mix(mix(lcd_mid(), lcd_dark(), 0.25), lcd_ghost(), 0.6 * self.disabled)
let won = w_on * t + w_off * (1.0 - t)
let wgh = w_on * (1.0 - t) + w_off * t
out = lcd_over(out, vec4(ghost_col * wgh, wgh))
out = lcd_over(out, vec4(lit_col * won, won))
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * self.focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.ToggleFlat.clip_x = false
mod.widgets.ToggleFlat.clip_y = false
// RADIOS. A 12 point diamond in stepped pixels; inside, a diamond segment
// at ghost, filled with ink when chosen.
mod.widgets.RadioButtonFlat.padding = mod.turtle.Inset{left: 0.0 right: 8.0 top: 5.0 bottom: 5.0}
mod.widgets.RadioButtonFlat.label_walk.margin = mod.turtle.Inset{left: 19.0 right: 0.0 top: 0.0 bottom: 0.0}
mod.widgets.RadioButtonFlat.draw_bg.size = uniform(12.0)
mod.widgets.RadioButtonFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RadioButtonFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let held = self.down * live
let sb = lcd_markbox(self.rect_size, o, self.size + px, self.size + px, 0.0, px)
let c = (sb.xy + sb.zw) * 0.5
let r = (sb.z - sb.x) * 0.5
// a diamond tested on pixel centres: hard stairs
let q = abs(lcd_pix(p, o, px) - c)
let dq = q.x + q.y - r
let inside = step(dq, 0.0)
var out = vec4(0.0, 0.0, 0.0, 0.0)
let qs = abs(p - lcd_throw() - c)
let ds = (qs.x + qs.y - r) * 0.70711
let sh = lcd_inkshade(abs(ds) - px * 0.5, 0.14) * (1.0 - inside) * live
out = vec4(lcd_ink() * sh, sh)
let chk = lcd_checker(p, o, px)
let ba = inside * chk * self.hover * live * (1.0 - held)
out = lcd_over(out, vec4(lcd_mid() * ba, ba))
let ia = inside * held
out = lcd_over(out, vec4(lcd_ink() * ia, ia))
// the stepped line, one pixel wide along the stairs
let line = inside * step(-1.5 * px, dq)
let lcol = mix(lcd_ink(), lcd_mid(), self.disabled)
out = lcd_over(out, vec4(lcol * line, line))
// the inner diamond segment
let mk = step(q.x + q.y - r * 0.5, 0.0)
let gcol = mix(lcd_ghost(), vec3(0.604, 0.631, 0.529), self.disabled)
let ga = mk * (1.0 - self.active) * (1.0 - held)
out = lcd_over(out, vec4(gcol * ga, ga))
let mcol = mix(mix(lcd_ink(), lcd_mid(), self.disabled), lcd_lit(), held)
let ma = mk * self.active
out = lcd_over(out, vec4(mcol * ma, ma))
let dd = (abs(p - c).x + abs(p - c).y - r) * 0.70711
let kf = Finish.band(-(dd - 2.0 - px), 0.0, px, px) * self.focus * live * lcd_checker(p, o, px)
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.RadioButtonFlat.clip_x = false
mod.widgets.RadioButtonFlat.clip_y = false
// RADIO TABS. Square caption cells: a one-pixel rule above and below and a
// divider either side; the chosen one is the inverse legend, dark with lit
// glass letters. The pointer lays the dither, a press inverts.
let lcd_caption = fn(p: vec2, sb: vec4, o: vec2, px: float, fill: float, dither: float, focus: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 0.01)
let inx = step(sb.x, p.x) * step(p.x, sb.z)
let iny = step(sb.y, p.y) * step(p.y, sb.w)
let inside = inx * iny
var out = vec4(0.0, 0.0, 0.0, 0.0)
let sh = lcd_inkshade(lcd_rbox(p - lcd_throw(), c, h, 0.01), 0.14) * fill * (1.0 - inside) * live
out = vec4(lcd_ink() * sh, sh)
let chk = lcd_checker(p, o, px)
let da = inside * chk * dither * (1.0 - fill)
out = lcd_over(out, vec4(lcd_mid() * da, da))
let fa = inside * fill
let fcol = mix(lcd_ink(), lcd_mid(), disabled)
out = lcd_over(out, vec4(fcol * fa, fa))
let rules = max(step(p.y, sb.y + px), step(sb.w - px, p.y)) * inx
let divs = max(step(p.x, sb.x + px), step(sb.z - px, p.x)) * iny
let la = max(rules, divs) * inside
let lcol = mix(lcd_ink(), lcd_mid(), disabled)
out = lcd_over(out, vec4(lcol * la, la))
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.RadioButtonTabFlat.padding = mod.turtle.Inset{left: 0.0 right: 0.0 top: 5.0 bottom: 5.0}
mod.widgets.RadioButtonTabFlat.label_walk.margin = mod.turtle.Inset{left: 9.0 right: 9.0 top: 0.0 bottom: 0.0}
mod.widgets.RadioButtonTabFlat.draw_text.color = #292b20
mod.widgets.RadioButtonTabFlat.draw_text.color_hover = #292b20
mod.widgets.RadioButtonTabFlat.draw_text.color_down = #b6bda2
mod.widgets.RadioButtonTabFlat.draw_text.color_focus = #292b20
mod.widgets.RadioButtonTabFlat.draw_text.color_active = #b6bda2
mod.widgets.RadioButtonTabFlat.draw_text.color_disabled = #6c725c
mod.widgets.RadioButtonTabFlat.draw_text.text_style = mod.theme.font_label{}
mod.widgets.RadioButtonTabFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RadioButtonTabFlat.draw_bg.pixel = fn() {
let m = 5.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size, px)
// chosen: the inverse legend; the press inverts too
let live = 1.0 - self.disabled
let inv = max(self.active, self.down * live)
return lcd_caption(p, sb, self.rect_pos, px, inv, self.hover * live, self.focus, self.disabled)
}
mod.widgets.RadioButtonTabFlat.clip_x = false
mod.widgets.RadioButtonTabFlat.clip_y = false
mod.widgets.RadioButtonTab.clip_x = false
mod.widgets.RadioButtonTab.clip_y = false
// ---------------------------------------------------------------------------
// SLIDERS. The bar scale of the wristwatch: a row of short dashes, the
// value's run filled with ink and the rest at ghost; over it the scale's
// numbers 0 25 50 75 100 in small segment digits, joined by a one-pixel
// rule; under it the thumb, a small ink pointer. The pointer is outlined
// under the hand, and grows while dragged.
let lcd_scale = fn(p: vec2, o: vec2, size: vec2, top: float, px: float, slide: float, lo: float, hi: float, numbers: float, hover: float, focus: float, drag: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let x0 = 3.0
let x1 = size.x - 3.0
let span = max(x1 - x0, 1.0)
// rows, from the top of the track band
let dh = 5.5
let y_num = top
let y_dash = Finish.snap(o.y + top + mix(0.5, 7.5, numbers), px) - o.y
let dash_h = 4.0
let y_ptr = y_dash + dash_h + 1.0
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the dashes: whole segments, a gap of two points, about 8.5 apart
let n = max(floor((span + 2.0) / 8.5), 4.0)
let pitch = (span + 2.0) / n
let k = floor((p.x - x0) / pitch)
let within = p.x - x0 - k * pitch
let in_dash = step(0.0, within) * step(within, pitch - 2.0) * step(0.0, k) * step(k, n - 1.0)
let in_row = step(y_dash, p.y) * step(p.y, y_dash + dash_h)
let sc = (k + 0.5) / n
let lit = step(lo, sc + 0.5 / n - 0.0001) * step(sc - 0.5 / n + 0.0001, hi) * step(0.0001, hi - lo + 0.0001)
let full = step(lo, sc) * step(sc, hi)
let on = max(full, lit * step(0.0, hi - lo - 0.0001) * step(sc, hi + 0.0001) * step(lo - 0.0001, sc))
let da = in_dash * in_row
let lcol = mix(lcd_ink(), lcd_mid(), disabled)
let dcol = mix(lcd_ghost(), lcol, on)
// the lit dashes throw the ink's shadow
let sp = p - lcd_throw()
let sk = clamp(floor((sp.x - x0 + 1.0) / pitch), 0.0, n - 1.0)
let sbc = vec2(x0 + sk * pitch + (pitch - 2.0) * 0.5, y_dash + dash_h * 0.5)
let sbd = lcd_rbox(sp, sbc, vec2((pitch - 2.0) * 0.5, dash_h * 0.5), 0.01)
let ssc = (sk + 0.5) / n
let sh = lcd_inkshade(sbd, 0.14) * step(lo, ssc) * step(ssc, hi) * (1.0 - da) * live
out = vec4(lcd_ink() * sh, sh)
out = lcd_over(out, vec4(dcol * da, da))
// the numbers and the rule joining them
if numbers > 0.5 {
let nm = lcd_scale_numbers(p, x0, x1, y_num, dh, px)
let ncol = mix(lcd_ink(), lcd_mid(), disabled)
let na = nm.x
out = lcd_over(out, vec4(ncol * na, na))
let yr = Finish.snap(o.y + y_num + dh * 0.5, px) - o.y
let rule = step(yr, p.y) * step(p.y, yr + px) * step(1.5, nm.y) * step(x0, p.x) * step(p.x, x1)
let ra = rule * 0.8
out = lcd_over(out, vec4(ncol * ra, ra))
}
// the pointer: a small triangle, tip up against the dashes
let hx = Finish.snap(o.x + x0 + slide * span, px) - o.x + px * 0.5
let grow = 1.0 + 0.35 * drag * live
let ph = 3.5 * grow
let pw = 3.5 * grow
let q = p - vec2(hx, y_ptr)
let td = max(-q.y, abs(q.x) * (ph / pw) + q.y - ph) * 0.7
let pa = Finish.cover(td, px)
let psd = max(-(q.y - 1.0), abs(q.x + 0.75) * (ph / pw) + (q.y - 1.0) - ph) * 0.7
let psh = lcd_inkshade(psd, 0.14) * (1.0 - pa) * live
out = lcd_over(out, vec4(lcd_ink() * psh, psh))
out = lcd_over(out, vec4(lcol * pa, pa))
// under the pointer: a one-pixel box round the thumb; focus: dashed
let bc = vec2(hx, y_ptr + ph * 0.5)
let bh = vec2(pw + 1.5, ph * 0.5 + 1.5)
let bd = lcd_rbox(p, bc, bh, 0.01)
let hb = Finish.band(-bd, 0.0, px, px) * hover * live * (1.0 - focus)
out = lcd_over(out, vec4(lcd_ink() * hb, hb))
let kf = lcd_dash_ring(bd, p, bc, bh, o, 0.0, px) * focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.SliderFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderFlat.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let lo = min(self.origin_pos, self.slide_pos) * self.arc_origin
let hi = mix(self.slide_pos, max(self.origin_pos, self.slide_pos), self.arc_origin)
return lcd_scale(p, self.rect_pos, self.rect_size, 19.5, px, self.slide_pos, lo, hi, 1.0, self.hover, self.focus, self.drag, self.disabled)
}
mod.widgets.SliderFlat.clip_x = false
mod.widgets.SliderFlat.clip_y = false
// The minimal slider: the dashes and the pointer, no numbers.
mod.widgets.SliderMinimal.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let lo = min(self.origin_pos, self.slide_pos) * self.arc_origin
let hi = mix(self.slide_pos, max(self.origin_pos, self.slide_pos), self.arc_origin)
return lcd_scale(p, self.rect_pos, self.rect_size, self.offset_y, px, self.slide_pos, lo, hi, 0.0, self.hover, self.focus, self.drag, self.disabled)
}
// The value read out beside a slider: tabular digits in the ink.
let lcd_value_style = mod.theme.font_regular{font_size: 7.5 font_family: mod.theme.font_regular.font_family{ibm_plex_text := FontMember{res: crate_resource("makepad_widgets:resources/jetbrains_mono_variable.ttf") weight: 600.0 asc: 0.0 desc: 0.0}}}
mod.widgets.SliderMinimal.text_input.draw_text.text_style = lcd_value_style
mod.widgets.SliderFlat.text_input.draw_text.text_style = lcd_value_style
mod.widgets.SliderRoundFlat.text_input.draw_text.text_style = lcd_value_style
// The round slider becomes a segment bar: a glass window framed in one
// pixel of ink, its dashes filled up to the value, the number at its right.
mod.widgets.SliderRoundFlat.height = 20.0
mod.widgets.SliderRoundFlat.text_input.min_height = 0.0
mod.widgets.SliderRoundFlat.text_input.padding = mod.turtle.Inset{left: 0.0 right: 6.0 top: 0.0 bottom: 0.0}
mod.widgets.SliderRoundFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.SliderRoundFlat.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let live = 1.0 - self.disabled
let sb = lcd_snapbox(o, vec2(self.label_size, 0.0), self.rect_size, px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 1.5)
let inside = Finish.cover(d, px)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// a window let into the glass: a shade under its top edge
let topd = clamp((p.y - sb.y) / 3.0, 0.0, 1.0)
let wcol = mix(lcd_glass() * 0.82, lcd_glass() * 0.97, topd)
out = vec4(wcol * inside, inside)
// dashes inside, leaving room for the number at the right
let lo = min(self.origin_pos, self.slide_pos) * self.arc_origin
let hi = mix(self.slide_pos, max(self.origin_pos, self.slide_pos), self.arc_origin)
let x0 = sb.x + 3.0
let x1 = sb.z - 36.0
let span = max(x1 - x0, 1.0)
let n = max(floor((span + 1.5) / 6.0), 4.0)
let pitch = (span + 1.5) / n
let k = floor((p.x - x0) / pitch)
let within = p.x - x0 - k * pitch
let in_dash = step(0.0, within) * step(within, pitch - 1.5) * step(0.0, k) * step(k, n - 1.0)
let in_row = step(c.y - 3.0, p.y) * step(p.y, c.y + 3.0)
let sc = (k + 0.5) / n
let on = step(lo, sc) * step(sc, hi)
let lcol = mix(lcd_ink(), lcd_mid(), self.disabled)
let dcol = mix(vec3(0.537, 0.561, 0.467), lcol, on)
let da = in_dash * in_row
out = lcd_over(out, vec4(dcol * da, da))
let chk = lcd_checker(p, o, px)
let hv = inside * chk * 0.0
let la = Finish.band(d, 0.0, px, px)
let fl = Finish.band(d, px, 2.0 * px, px) * max(self.hover, self.drag) * live
out = lcd_over(out, vec4(lcol * max(la, fl), max(la, fl)))
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * self.focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.SliderRoundFlat.clip_x = false
mod.widgets.SliderRoundFlat.clip_y = false
// FADERS. A slot four points wide let into the glass: one device pixel of
// ink round a core of one-to-one ink hatching, the family's mid-tone. A
// printed ladder on ONE side of it, two points clear: ten steps, a long
// tick at 0, 50 and 100. The cap is an inverse tile 18 across and 10 along
// with a lit line across its middle, the line the value is read off; it
// throws the ink's shadow and moves over the slot.
mod.widgets.SliderFader.cap_size = 10.0
mod.widgets.SliderFader.track_inset = 6.0
mod.widgets.SliderFaderY.width = 26.0
mod.widgets.SliderFader.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let live = 1.0 - self.disabled
let along = mix(self.rect_size.x, self.rect_size.y, self.is_vertical)
let across = mix(self.rect_size.y, self.rect_size.x, self.is_vertical)
let a = mix(p.x, self.rect_size.y - p.y, self.is_vertical)
let b = mix(p.y, p.x, self.is_vertical)
let oa = mix(self.rect_pos.x, self.rect_pos.y, self.is_vertical)
let ob = mix(self.rect_pos.y, self.rect_pos.x, self.is_vertical)
let travel = max(along - self.inset_px * 2.0 - self.cap_px, 1.0)
let foot = self.inset_px + self.cap_px * 0.5
let mid = Finish.snap(ob + across * 0.5, px) - ob
let icol = mix(lcd_ink(), lcd_mid(), self.disabled)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the ladder, on the near side only (left of a column, above a row)
let pc = lcd_pix(p, self.rect_pos, px)
let ac = mix(pc.x, self.rect_size.y - pc.y, self.is_vertical)
let bc = mix(pc.y, pc.x, self.is_vertical)
let da = ac - foot
let step10 = travel / 10.0
let kk = floor(da / step10 + 0.5)
let near = abs(da - kk * step10)
let tick = step(near, px * 0.5) * step(-0.5, kk) * step(kk, 10.5)
let off = mid - bc
let major = step(fract(kk / 5.0 + 0.01), 0.1)
let tl = mix(3.0, 5.0, major)
let tk = tick * step(4.0, off) * step(off, 4.0 + tl)
out = lcd_over(out, vec4(icol * tk, tk))
// the slot: six device pixels across, square ends of half a point
let sa0 = Finish.snap(oa + self.inset_px, px) - oa
let sa1 = Finish.snap(oa + along - self.inset_px, px) - oa
let sb0 = mid - 3.0 * px
let sc = vec2((sa0 + sa1) * 0.5, sb0 + 3.0 * px)
let sh2 = vec2((sa1 - sa0) * 0.5, 3.0 * px)
let q = vec2(a, b)
let sd = lcd_rbox(q, sc, sh2, 0.5)
let sfill = Finish.cover(sd, px)
let hatch = lcd_checker(p, self.rect_pos, px)
var scol = mix(lcd_ground() * 0.92, icol, hatch * 0.85)
scol = mix(scol, icol, Finish.band(sd, 0.0, px, px))
out = lcd_over(out, vec4(scol * sfill, sfill))
// the cap
let at = foot + self.slide_pos * travel
let ca_h = Finish.snap(oa + at, px) - oa
let ca_v = along - (Finish.snap(oa + along - at, px) - oa)
let ca = mix(ca_h, ca_v, self.is_vertical)
let cc = vec2(ca, mid)
let chh = vec2(self.cap_px * 0.5, 9.0)
let cd = lcd_rbox(q, cc, chh, 1.0)
// the ink's shadow, down and a little left in screen space
let sofs = mix(vec2(-0.75, 1.0), vec2(-1.0, -0.75), self.is_vertical)
let csd = lcd_rbox(q - sofs, cc, chh, 1.0)
let ca2 = Finish.cover(cd, px)
let sh = lcd_inkshade(csd, 0.16) * (1.0 - ca2) * live
out = lcd_over(out, vec4(lcd_ink() * sh, sh))
let face = mix(mix(lcd_ink(), lcd_dark(), 0.4 * self.hover * live), lcd_mid(), self.disabled)
out = lcd_over(out, vec4(face * ca2, ca2))
// the lit line across the cap, one pixel, and a keyline while dragged
let cut = step(ca, a) * step(a, ca + px) * step(abs(b - cc.y), chh.y - 2.0 * px) * ca2
out = lcd_over(out, vec4(lcd_lit() * cut, cut))
let kl = Finish.band(cd, -3.0 * px, -2.0 * px, px) * self.drag * live
out = lcd_over(out, vec4(lcd_lit() * kl, kl))
let kf = lcd_dash_ring(cd, q, cc, chh, vec2(oa, ob), 2.0, px) * self.focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
// ---------------------------------------------------------------------------
// KNOBS. The wristwatch's bezel: a dark ring printed with minute marks in
// the glass colour, lit along its upper edge, standing on the glass with a
// short shade under it. Inside, a glass dial with its own shadow under the
// rim, a ring of 24 segments lit up to the value and ghosted beyond, and a
// plain centre with an ink pointer.
let lcd_knob = fn(p: vec2, c: vec2, rad: float, o: vec2, px: float, slide: float, origin: float, arc_origin: float, gap: float, hover: float, focus: float, drag: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let v = p - c
let r = length(v)
let rin = rad * 0.78
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the bezel's shade on the glass
let sr = length(v - vec2(-0.4, 1.0))
let sh = (1.0 - smoothstep(rad - 0.8, rad + 1.8, sr)) * 0.30 * (1.0 - Finish.cover(r - rad, px)) * mix(1.0, 0.4, disabled)
out = vec4(0.0, 0.0, 0.0, sh)
// a light lip where the glass meets the bezel's foot
let lip = Finish.band(-(r - rad - px), 0.0, px, px) * smoothstep(0.2, 0.8, v.y / max(r, 0.001)) * 0.7
out = lcd_over(out, vec4(lcd_g3(0.84) * lip, lip))
// the bezel ring
let ring = Finish.cover(max(r - rad, rin - r), px)
let up = clamp(-v.y / max(rad, 1.0), -1.0, 1.0)
var bcol = lcd_frame() * (1.0 + 0.25 * up) + lcd_g3(0.02 * hover * live)
// the minute marks: sixty, the fifth ones longer
var th = atan2(v.x, -v.y)
if th < 0.0 {
th = th + 2.0 * PI
}
let mi = floor(th / (PI / 30.0) + 0.5)
let mang = mi * PI / 30.0
let mdir = vec2(sin(mang), -cos(mang))
let mperp = abs(dot(v, vec2(mdir.y, -mdir.x)))
let five = step(fract(mi / 5.0 + 0.01), 0.1)
let mlen = mix(0.09, 0.16, five) * rad
let mark = Finish.band(mperp - px * mix(0.5, 0.75, five), -px, 0.0, px) * step(rad - px * 2.0 - mlen, r) * step(r, rad - px * 2.0)
let mcol = mix(vec3(0.600, 0.627, 0.529), lcd_lit(), hover * live)
bcol = mix(bcol, mcol * mix(1.0, 0.5, disabled), mark * 0.9)
// the lit line on the bezel's upper outer edge, the dark lip inside
let edge = Finish.band(-(r - rad), 0.0, px, px) * clamp(up * 1.6, 0.0, 1.0)
bcol = mix(bcol, vec3(0.380, 0.392, 0.416), edge)
let inner = Finish.band(r - rin, 0.0, px, px)
bcol = mix(bcol, lcd_g3(0.01), inner)
out = lcd_over(out, vec4(bcol * ring, ring))
// the dial glass, shaded under the rim at the top
let dial = Finish.cover(r - rin, px)
let shade = (1.0 - smoothstep(0.0, rin * 0.28, dot(v, vec2(0.0, 1.0)) + rin)) * 0.45
var gcol = lcd_wglass() * (1.0 - shade)
gcol = gcol + lcd_g3(Finish.grain((p + o) / px, 0.018))
// the ring of segments
let start = gap * 0.5 * PI / 180.0
let sweep = 2.0 * PI - gap * PI / 180.0
var ta = atan2(-v.x, v.y)
if ta < 0.0 {
ta = ta + 2.0 * PI
}
let t = (ta - start) / sweep
let s0 = rin * 0.70
let s1 = rin * 0.90
let smid = (s0 + s1) * 0.5
// segments about three and a half points long, 12 to 24 of them
let n = clamp(floor(sweep * smid / 3.6), 12.0, 24.0)
let seg = floor(t * n)
let seglen = sweep * smid / n
let f = fract(t * n) * seglen
let in_seg = step(f, seglen - 1.0) * step(0.0, t) * step(t, 1.0)
let band = Finish.cover(max(s0 - r, r - s1), px) * in_seg
let lo = min(origin, slide) * arc_origin
let hi = mix(slide, max(origin, slide), arc_origin)
let sc = (seg + 0.5) / n
let lit = step(lo, sc) * step(sc, hi + 0.5 / n - 0.0001)
let icol = mix(lcd_ink(), lcd_mid(), disabled)
let segc = mix(lcd_wglass() * 0.86, icol, lit)
gcol = mix(gcol, segc, band)
// the pointer: a short ink bar in the plain centre, a tenth to four
// tenths of the radius out, well inside the ring of segments
let av = start + sweep * slide
let dir = vec2(-sin(av), cos(av))
let al = clamp(dot(v, dir), rad * 0.10, rad * 0.40)
let pw = rad * 0.06 * mix(1.0, 1.25, drag * live)
let pd = length(v - dir * al) - pw
let pa = Finish.cover(pd, px)
let psd = length(v - lcd_throw() - dir * clamp(dot(v - lcd_throw(), dir), rad * 0.10, rad * 0.40)) - pw
gcol = mix(gcol, gcol * 0.86, lcd_inkshade(psd, 1.0) * (1.0 - pa))
gcol = mix(gcol, icol, pa)
out = lcd_over(out, vec4(gcol * dial, dial))
// focus: a dashed ring two points out
let kr = Finish.band(abs(r - rad - 2.0 - px * 0.5) - px * 0.5, -px, 0.0, px)
let kd = step(fract(floor(th * rad / px / 2.0 + 0.01) * 0.5), 0.3)
let kf = kr * kd * focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.RotaryKnob.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RotaryKnob.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let dia = min(self.rect_size.x, self.rect_size.y)
let rad = dia * 0.5 - 1.5
let c = vec2(Finish.snap(self.rect_pos.x + self.rect_size.x * 0.5, px), Finish.snap(self.rect_pos.y + self.rect_size.y - dia * 0.5, px)) - self.rect_pos
return lcd_knob(p, c, rad, self.rect_pos, px, self.slide_pos, self.origin_pos, self.arc_origin, self.gap, self.hover, self.focus, self.drag, self.disabled)
}
mod.widgets.RotaryKnob.clip_x = false
mod.widgets.RotaryKnob.clip_y = false
mod.widgets.RotaryFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.RotaryFlat.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let dia = min(self.rect_size.x, self.rect_size.y - 24.0)
let rad = dia * 0.5 - 2.0
let c = vec2(Finish.snap(self.rect_pos.x + self.rect_size.x * 0.5, px), Finish.snap(self.rect_pos.y + 24.0 + dia * 0.5, px)) - self.rect_pos
return lcd_knob(p, c, rad, self.rect_pos, px, self.slide_pos, self.origin_pos, self.arc_origin, self.gap, self.hover, self.focus, self.drag, self.disabled)
}
mod.widgets.RotaryFlat.clip_x = false
mod.widgets.RotaryFlat.clip_y = false
mod.widgets.RotaryFlat.text_input.padding = mod.turtle.Inset{left: 4.0 right: 4.0 top: 1.0 bottom: 1.0}
mod.widgets.RotaryFlat.text_input.min_height = 0.0
mod.widgets.RotaryFlat.text_input.draw_text.text_style = lcd_value_style
// ---------------------------------------------------------------------------
// FIELDS. A window let into the glass: one device pixel of ink round a
// glass a shade darker, a shadow under its top edge, and a light lip under
// its foot where the cut catches the light. The pointer lays a sparse
// dither; focus doubles the line. A field a host has made invisible (the
// value inside a slider) stays invisible.
let lcd_well = fn(p: vec2, sb: vec4, o: vec2, px: float, hover: float, focus: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 1.5)
let inside = Finish.cover(d, px)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the lip under the foot
let lip = Finish.ring_out(d, 0.0, px, px) * step(c.y + h.y - 1.0, p.y) * 0.75
out = vec4(lcd_g3(0.80) * lip, lip)
// the glass: shaded under the top edge over four points, ease-out
let t = clamp((p.y - sb.y) / 4.0, 0.0, 1.0)
let fall = 1.0 - (1.0 - t) * (1.0 - t)
var col = mix(lcd_ground() * 0.66, lcd_ground() * 0.955, fall)
col = mix(col, lcd_ground() * 0.985, disabled)
col = col + lcd_g3(Finish.grain((p + o) / px, 0.016))
out = lcd_over(out, vec4(col * inside, inside))
// under the pointer: one pixel in four of the light middle
let qa = lcd_quarter(p, o, px) * inside * hover * live * 0.8
out = lcd_over(out, vec4(lcd_mid() * qa, qa))
// the line; focus doubles it inward
let lcol = mix(lcd_ink(), lcd_mid(), disabled)
let la = max(Finish.band(d, 0.0, px, px), Finish.band(d, px, 2.0 * px, px) * focus * live)
out = lcd_over(out, vec4(lcol * la, la))
return out
}
mod.widgets.TextInputFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size - vec2(0.0, px), px)
let vis = step(0.001, self.border_size)
return lcd_well(p, sb, self.rect_pos, px, self.hover, self.focus, self.disabled) * vis
}
mod.widgets.TextInputFlat.draw_text.color_empty = #5d624d
mod.widgets.TextInputFlat.draw_text.color_empty_hover = #535843
mod.widgets.TextInputFlat.draw_text.color_empty_focus = #5d624d
// The caret is an ink block a point and a half wide.
mod.widgets.TextInputFlat.draw_cursor.pixel = fn() {
let a = (1.0 - self.blink) * self.focus * (1.0 - self.disabled)
return vec4(vec3(0.161, 0.169, 0.125) * a, a)
}
mod.widgets.TextInputFlat.draw_selection.border_radius = 0.0
// The number field: the same window, its stepper cut off at the right by
// a one-pixel rule, two cells with pixel arrows; a cell lays the dither
// under the pointer and inverts while pressed.
mod.widgets.NumberField.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size - vec2(0.0, px), px)
return lcd_well(p, sb, self.rect_pos, px, self.hover, self.focus, self.disabled)
}
mod.widgets.NumberField.pad_left = 8.0
mod.widgets.NumberSpin.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let w = self.rect_size.x
let hgt = self.rect_size.y
let mid = Finish.snap(o.y + hgt * 0.5, px) - o.y
let live = 1.0 - self.disabled
let upper = step(p.y, mid)
let hot = mix(max(0.0, -self.hot), max(0.0, self.hot), upper) * live
let dn = mix(max(0.0, -self.down), max(0.0, self.down), upper) * live
let inx = step(px, p.x) * step(p.x, w - px)
let iny = step(1.5 * px, p.y) * step(p.y, hgt - 1.5 * px)
let cell = inx * iny
var out = vec4(0.0, 0.0, 0.0, 0.0)
let chk = lcd_checker(p, o, px)
let ha = cell * chk * hot * (1.0 - dn)
out = lcd_over(out, vec4(lcd_mid() * ha, ha))
let ia = cell * dn
out = lcd_over(out, vec4(lcd_ink() * ia, ia))
// the rules: down the left edge and across the middle
let rl = step(p.x, px) * step(px, p.y) * step(p.y, hgt - px)
let rm = step(mid - px * 0.5, p.y) * step(p.y, mid + px * 0.5) * step(px, p.x)
let icol = mix(lcd_ink(), lcd_mid(), self.disabled)
let ra = max(rl, rm)
out = lcd_over(out, vec4(icol * ra, ra))
// the arrows: stepped triangles, 5 pixels across
let cx = Finish.snap(o.x + w * 0.5, px) - o.x
let q = floor((p + o) / px + vec2(0.001, 0.001)) - floor((vec2(cx, 0.0) + o) / px + vec2(0.5, 0.5))
let yu = floor((o.y + mid * 0.5) / px) - floor(o.y / px + 0.5)
let yd = floor((o.y + mid + (hgt - mid) * 0.5) / px) - floor(o.y / px + 0.5)
let qy = floor((p.y + o.y) / px + 0.001) - floor(o.y / px + 0.5)
let ru = qy - (yu - 1.0)
let rd = (yd + 1.0) - qy
let au = step(0.0, ru) * step(ru, 2.5) * step(abs(q.x + 0.5), ru + 0.6)
let ad = step(0.0, rd) * step(rd, 2.5) * step(abs(q.x + 0.5), rd + 0.6)
let ucol = mix(icol, lcd_lit(), max(0.0, self.down) * live)
let dcol = mix(icol, lcd_lit(), max(0.0, -self.down) * live)
out = lcd_over(out, vec4(ucol * au, au))
out = lcd_over(out, vec4(dcol * ad, ad))
return out
}
// The printed arrow glyphs are hidden; the face draws the arrows.
mod.widgets.NumberSpin.draw_text.color = #292b2000
mod.widgets.NumberSpin.draw_text.color_disabled = #292b2000
// DROP-DOWNS. The window, with a cell 18 points wide at its right split
// off by a one-pixel rule and a stepped ink triangle in it; open or held,
// the cell inverts.
let lcd_choice = fn(p: vec2, sb: vec4, o: vec2, px: float, hover: float, open: float, focus: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
var out = lcd_well(p, sb, o, px, 0.0, focus, disabled)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let rx = Finish.snap(o.x + sb.z - 18.0, px) - o.x
let incell = step(rx + px, p.x) * step(p.x, sb.z - px) * step(sb.y + px, p.y) * step(p.y, sb.w - px)
let chk = lcd_checker(p, o, px)
let ha = incell * chk * hover * live * (1.0 - open)
out = lcd_over(out, vec4(lcd_mid() * ha, ha))
let ia = incell * open * live
out = lcd_over(out, vec4(lcd_ink() * ia, ia))
let icol = mix(lcd_ink(), lcd_mid(), disabled)
let rule = step(rx, p.x) * step(p.x, rx + px) * step(sb.y, p.y) * step(p.y, sb.w)
out = lcd_over(out, vec4(icol * rule, rule))
// the triangle: 7 pixels across, 4 down, stepped
let tx = Finish.snap(o.x + rx + 9.0, px) - o.x
let q = floor((p + o) / px + vec2(0.001, 0.001)) - floor((vec2(tx, c.y - 1.5) + o) / px + vec2(0.5, 0.5))
let tr = 3.0 - q.y
let ta = step(0.0, q.y) * step(q.y, 3.5) * step(abs(q.x + 0.5), tr + 0.6)
let tcol = mix(icol, lcd_lit(), open * live)
out = lcd_over(out, vec4(tcol * ta, ta))
// focus also shows as the dashed ring outside
let d = lcd_rbox(p, c, h, 1.5)
return out
}
mod.widgets.DropDownFlat.padding = mod.turtle.Inset{left: 8.0 right: 26.0 top: 2.5 bottom: 2.5}
mod.widgets.DropDownFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size - vec2(0.0, px), px)
return lcd_choice(p, sb, self.rect_pos, px, self.hover, max(self.active, self.down * self.hover), self.focus, self.disabled)
}
mod.widgets.DropDownFlat.draw_text.color = #292b20
mod.widgets.DropDownFlat.draw_text.color_hover = #292b20
mod.widgets.DropDownFlat.draw_text.color_focus = #292b20
mod.widgets.DropDownFlat.draw_text.color_down = #292b20
mod.widgets.DropDownFlat.draw_text.color_disabled = #6c725c
// The select wears a button built before this sheet: it is given the
// drop-down's window and cell here, by its own path.
mod.widgets.Select.face.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size - vec2(0.0, px), px)
return lcd_choice(p, sb, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled)
}
mod.widgets.MultiSelect.face.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size - vec2(0.0, px), px)
return lcd_choice(p, sb, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled)
}
mod.widgets.Select.face.padding = mod.turtle.Inset{left: 8.0 right: 26.0 top: 2.5 bottom: 2.5}
mod.widgets.MultiSelect.face.padding = mod.turtle.Inset{left: 8.0 right: 26.0 top: 2.5 bottom: 2.5}
mod.widgets.Select.face.min_height = 24.0
mod.widgets.MultiSelect.face.min_height = 24.0
mod.widgets.Select.face.draw_text.color = #292b20
mod.widgets.Select.face.draw_text.color_hover = #292b20
mod.widgets.Select.face.draw_text.color_down = #292b20
mod.widgets.Select.face.draw_text.color_focus = #292b20
mod.widgets.MultiSelect.face.draw_text.color = #292b20
mod.widgets.MultiSelect.face.draw_text.color_hover = #292b20
mod.widgets.MultiSelect.face.draw_text.color_down = #292b20
mod.widgets.MultiSelect.face.draw_text.color_focus = #292b20
// The combo box: the same window and cell; its open list inverts the
// chosen row.
mod.widgets.ComboBoxFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size - vec2(0.0, px), px)
return lcd_choice(p, sb, self.rect_pos, px, self.hover, self.down * self.hover, self.focus, self.disabled)
}
mod.widgets.ComboBoxFlat.padding = mod.turtle.Inset{left: 8.0 right: 26.0 top: 2.5 bottom: 2.5}
mod.widgets.ComboBoxFlat.draw_popup_bg.color = #a8af94
mod.widgets.ComboBoxFlat.draw_popup_bg.border_color = #292b20
mod.widgets.ComboBoxFlat.draw_popup_bg.border_radius = 0.0
mod.widgets.ComboBoxFlat.draw_item.color_hover = #7a806533
mod.widgets.ComboBoxFlat.draw_item.color_active = #292b20
mod.widgets.ComboBoxFlat.draw_item_text.color_active = #b6bda2
mod.widgets.ComboBoxFlat.item_height = 20.0
// the input inside the box takes no row of its own
mod.widgets.ComboBoxFlat.input.min_height = 0.0
// ---------------------------------------------------------------------------
// TABS. Caption cells between two one-pixel rules; the tab in use is the
// inverse caption tab, dark with lit glass letters; the pointer lays the
// light dither.
mod.widgets.Tabs.height = 22.0
mod.widgets.Tabs.draw_bg.color = #00000000
mod.widgets.Tabs.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let yt = Finish.snap(self.rect_pos.y, px) - self.rect_pos.y
let yb = Finish.snap(self.rect_pos.y + self.rect_size.y, px) - self.rect_pos.y
let la = max(step(yb - px, p.y) * step(p.y, yb), step(yt, p.y) * step(p.y, yt + px))
return vec4(lcd_ink() * la, la)
}
let lcd_tab_face = fn(p: vec2, size: vec2, o: vec2, px: float, fill: float, dither: float) -> vec4 {
let sb = lcd_snapbox(o, vec2(0.0, 0.0), size + vec2(px, 0.0), px)
let inx = step(sb.x, p.x) * step(p.x, sb.z)
let iny = step(sb.y, p.y) * step(p.y, sb.w)
let inside = inx * iny
var out = vec4(0.0, 0.0, 0.0, 0.0)
let chk = lcd_checker(p, o, px)
let da = inside * chk * dither
out = lcd_over(out, vec4(lcd_mid() * da, da))
let fa = inside * fill
out = lcd_over(out, vec4(lcd_ink() * fa, fa))
// the cell's dividers; the strip draws the rules above and below
let side = max(step(p.x, sb.x + px), step(sb.z - px, p.x)) * iny
let la = side * inside
out = lcd_over(out, vec4(lcd_ink() * la, la))
return out
}
mod.widgets.Tabs.draw_tab.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return lcd_tab_face(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, 0.0, 0.0)
}
mod.widgets.Tabs.draw_tab_hover.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return lcd_tab_face(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, 0.0, 1.0)
}
mod.widgets.Tabs.draw_tab_active.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
return lcd_tab_face(self.pos * self.rect_size, self.rect_size, self.rect_pos, px, 1.0, 0.0)
}
mod.widgets.Tabs.draw_sep.color = #00000000
mod.widgets.Tabs.draw_mark.color = #7a8065
// The close mark is a stepped x, the add mark a one-pixel plus.
mod.widgets.Tabs.draw_mark.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let is_plus = step(0.5, self.plus)
let cx0 = mix(self.rect_size.x - 5.5, self.rect_size.x * 0.5, is_plus)
let c = vec2(Finish.snap(o.x + cx0, px), Finish.snap(o.y + self.rect_size.y * 0.5, px)) - o
let q = floor((p - c) / px + vec2(0.001, 0.001))
let r = mix(3.0, 5.0, is_plus)
let inbox = step(abs(q.x + 0.5), r + 0.5) * step(abs(q.y + 0.5), r + 0.5)
let cross = max(step(abs(q.x - q.y), 0.5), step(abs(q.x + q.y + 1.0), 0.5)) * inbox
let plus = max(step(abs(q.x), 0.5), step(abs(q.y), 0.5)) * inbox
let a = mix(cross, plus, is_plus) * self.color.a
return vec4(self.color.rgb * a, a)
}
mod.widgets.Tabs.draw_text.color = #292b20
mod.widgets.Tabs.draw_text_active.color = #b6bda2
mod.widgets.Tabs.draw_text.ink_centered = true
mod.widgets.Tabs.draw_text_active.ink_centered = true
let lcd_tab_type = mod.theme.font_label{font_size: 7.5 line_spacing: 1.0}
mod.widgets.Tabs.draw_text.text_style = lcd_tab_type
mod.widgets.Tabs.draw_text_active.text_style = lcd_tab_type
// The segmented group: one frame round the row, the chosen cell inverse.
mod.widgets.SegmentedControlFlat.segment_height = 22.0
mod.widgets.SegmentedControlFlat.segment_padding = 9.0
mod.widgets.SegmentedControlFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size, px)
let d = lcd_rbox(p, (sb.xy + sb.zw) * 0.5, (sb.zw - sb.xy) * 0.5, 0.01)
let la = Finish.band(d, 0.0, px, px)
return vec4(lcd_ink() * la, la)
}
mod.widgets.SegmentedControlFlat.draw_pill.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size, px)
let d = lcd_rbox(p, (sb.xy + sb.zw) * 0.5, (sb.zw - sb.xy) * 0.5, 0.01)
let a = Finish.cover(d, px)
return vec4(lcd_ink() * a, a)
}
mod.widgets.SegmentedControlFlat.draw_text.color = #292b20
mod.widgets.SegmentedControlFlat.draw_text_selected.color = #b6bda2
mod.widgets.SegmentedControlFlat.draw_text.text_style = mod.theme.font_label{}
mod.widgets.SegmentedControlFlat.draw_text_selected.text_style = mod.theme.font_label{}
mod.widgets.SegmentedControlFlat.palette.on_neutral_container = #b6bda2
// The dock's tabs: the tab in use is a caption cell with a two-pixel ink
// bar along its top; the others lay the dither under the pointer.
mod.widgets.Tab.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let sb = lcd_snapbox(o, vec2(0.0, 0.0), self.rect_size, px)
let chk = lcd_checker(p, o, px)
var out = vec4(0.0, 0.0, 0.0, 0.0)
let da = chk * self.hover * (1.0 - self.active)
out = lcd_over(out, vec4(lcd_mid() * da, da))
let bar = step(p.y, sb.y + 2.0 * px) * self.active
let side = max(step(p.x, sb.x + px), step(sb.z - px, p.x)) * self.active
let la = max(bar, side)
out = lcd_over(out, vec4(lcd_ink() * la, la))
return out
}
mod.widgets.TabBar.draw_fill.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let yb = Finish.snap(self.rect_pos.y + self.rect_size.y, px) - self.rect_pos.y
let la = step(yb - px, p.y) * step(p.y, yb)
return vec4(lcd_ink() * la, la)
}
// ---------------------------------------------------------------------------
// PROGRESS. The gauge bar of the wristwatch: a pill outlined in one device
// pixel of ink and, a device pixel of glass in from the line, the value's
// run as a solid ink pill of its own, from the start and never shorter
// than it is tall. Only the piece that carries the track draws the line,
// so stacked sections stand as pills inside one. The amount that is not
// known cannot move without a clock, so it stands still as a checker
// dither of ink over the whole inside. The quad is grown 4 points for the
// ink shadow.
mod.widgets.ProgressBarFlat.height = 8.0
mod.widgets.ProgressBarFlat.draw_bg.thickness = uniform(6.0)
mod.widgets.ProgressBarFlat.draw_bg.border_radius = uniform(0.0)
mod.widgets.ProgressBarFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ProgressBarFlat.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
// the pill, start to end, the thickness tall and centred in the walk;
// its line one device pixel of ink
let hgt = min(self.thickness, self.rect_size.y)
let y0 = (self.rect_size.y - hgt) * 0.5
let sb = lcd_snapbox(o, vec2(self.rect_size.x * self.start, y0), vec2(self.rect_size.x * self.end, y0 + hgt), px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let r = min(h.x, h.y)
let d = lcd_rbox(p, c, h, r)
let inside = Finish.cover(d, px)
let la = Finish.band(d, 0.0, px, px) * self.track
// the inside, two device pixels in: past the line, a pixel of glass
let hf = max(h.y - 2.0 * px, min(px, h.y))
let xi0 = sb.x + 2.0 * px
let xi1 = max(sb.z - 2.0 * px, xi0)
let th = lcd_throw()
var fa = 0.0
var shf = 0.0
if self.value >= 0.0 {
// the value's run, its ends the line's inset with it; never
// shorter than it is tall, so the least value is a dot
let v = clamp((self.value - self.start) / max(self.end - self.start, 0.0001), 0.0, 1.0)
let xv = Finish.snap(o.x + mix(xi0, xi1, v), px) - o.x
let xe = min(max(xv, xi0 + 2.0 * hf), xi1)
let fh = vec2((xe - xi0) * 0.5, hf)
let fc = vec2(xi0 + fh.x, c.y)
let rf = min(fh.x, hf)
let on = step(0.0001, v) * step(px, xe - xi0)
fa = Finish.cover(lcd_rbox(p, fc, fh, rf), px) * on
shf = lcd_inkshade(lcd_rbox(p - th, fc, fh, rf), 0.14) * on * (1.0 - fa)
} else {
// not known: the checker dither over the whole inside
let kh = vec2((xi1 - xi0) * 0.5, hf)
let kd = lcd_rbox(p, vec2(xi0 + kh.x, c.y), kh, min(kh.x, hf))
fa = Finish.cover(kd, px) * lcd_checker(p, o, px)
}
// the ink's shadow on the reflector: the line's falls outside the
// pill, the bar's on the glass round it too
let ds = lcd_rbox(p - th, c, h, r)
let shl = lcd_inkshade(abs(ds + px * 0.5) - px * 0.5, 0.14) * self.track * (1.0 - inside)
let sh = max(shl, shf)
var out = vec4(lcd_ink() * sh, sh)
out = lcd_over(out, vec4(lcd_ink() * la, la))
out = lcd_over(out, vec4(lcd_ink() * fa, fa))
return out * self.opacity
}
mod.widgets.ProgressBarFlat.clip_x = false
mod.widgets.ProgressBarFlat.clip_y = false
// LEVEL METERS. Rows of segments, lit ink to the level and ghosts beyond,
// the full height of the meter less a device pixel either side; the pitch
// is the meter's own `segment` (zero: one solid bar, ink to the level and
// ghost beyond) and `segment_gap` the share of it left clear. The held peak
// is one segment framed in ink; the over flag a small square at the end,
// filled when the level has gone over, at ghost when not.
mod.widgets.LevelMeter.height = 8.0
mod.widgets.LevelMeterColumn.width = 8.0
mod.widgets.LevelMeter.draw_bg.lamp_size = uniform(6.0)
mod.widgets.LevelMeter.draw_bg.lamp_gap = uniform(3.0)
mod.widgets.LevelMeter.draw_bg.segment = uniform(5.0)
mod.widgets.LevelMeter.draw_bg.segment_gap = uniform(0.3)
mod.widgets.LevelMeter.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let along = mix(self.rect_size.x, self.rect_size.y, self.vertical)
let across = mix(self.rect_size.y, self.rect_size.x, self.vertical)
let s = mix(self.pos.x * self.rect_size.x, (1.0 - self.pos.y) * self.rect_size.y, self.vertical)
let q = mix(self.pos.y * self.rect_size.y, self.pos.x * self.rect_size.x, self.vertical)
let lamp_room = self.lamp * (self.lamp_size + self.lamp_gap)
let bar = max(4.0, along - lamp_room)
let solid = step(self.segment, 0.49)
let pitch = max(self.segment, 1.0)
let clear = mix(clamp(pitch * self.segment_gap, px, pitch - px), 0.0, solid)
let n = mix(max(floor((bar + clear) / pitch), 1.0), 1.0, solid)
let k = mix(floor(s / pitch), 0.0, solid)
let within = s - k * pitch
let in_cell = mix(step(within, pitch - clear) * step(k, n - 1.0), step(s, bar), solid) * step(0.0, s)
// the full height, one device pixel in from either side
let cw = max(across - 2.0 * px, px)
let qa = px
let in_q = step(qa, q) * step(q, qa + cw)
let share = (k + 0.5) / max(n, 1.0)
let lit_seg = step(share, self.level + 0.5 / max(n, 1.0) - 0.0001) * step(0.0001, self.level)
let lit = mix(lit_seg, step(s, self.level * bar) * step(0.0001, self.level), solid)
let col = mix(lcd_ghost(), lcd_ink(), lit)
let a = in_cell * in_q
var out = vec4(col * a, a)
// the held peak: its segment framed, or on a solid bar a one-point rule
if self.hold > 0.002 {
let hk = floor(self.hold * n - 0.0001)
let e = step(abs(k - hk), 0.5) * in_q * step(k, n - 1.0) * (1.0 - solid)
let fr = e * (1.0 - step(px, within) * step(within, pitch - clear - px) * step(qa + px, q) * step(q, qa + cw - px)) * step(within, pitch - clear)
let hs = step(abs(s - self.hold * bar), 0.5) * in_q * solid
let ha = max(fr, hs)
out = lcd_over(out, vec4(lcd_ink() * ha, ha))
}
if self.lamp > 0.5 {
let lc = vec2(bar + self.lamp_gap + self.lamp_size * 0.5, across * 0.5)
let dq = abs(vec2(s, q) - lc) - vec2(self.lamp_size * 0.5, min(self.lamp_size * 0.5, across * 0.5 - px))
let ld = max(dq.x, dq.y)
let on = step(0.5, self.over)
let la = Finish.cover(ld, px)
let inner = Finish.cover(ld + px * 1.5, px)
let fcol = mix(lcd_ghost(), lcd_ink(), on)
out = lcd_over(out, vec4(lcd_ink() * la, la))
let ia = inner * (1.0 - on)
out = lcd_over(out, vec4(fcol * ia, ia))
}
return out * self.opacity
}
// SCROLL BARS. Eight points: the trough a one-pixel ink rule on its inner
// side; the thumb an ink block 1.5 points in, a checker dither at rest,
// solid under the pointer and while dragged.
mod.widgets.ScrollBar.bar_size = 8.0
mod.widgets.ScrollBar.bar_side_margin = 0.0
mod.widgets.ScrollBar.draw_bg.size = 8.0
mod.widgets.ScrollBar.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let len = mix(self.rect_size.x, self.rect_size.y, self.is_vertical)
let t = mix(p.x, p.y, self.is_vertical)
let b = mix(p.y, p.x, self.is_vertical)
let across = mix(self.rect_size.y, self.rect_size.x, self.is_vertical)
let rule = step(b, px)
var out = vec4(lcd_ink() * rule, rule)
let t0 = len * self.norm_scroll + 1.5
let t1 = len * (self.norm_scroll + self.norm_handle) - 1.5
let on = step(t0, t) * step(t, t1) * step(1.5 + px, b) * step(b, across - 1.5)
let chk = lcd_checker(p, o, px)
let solid = max(self.hover, self.drag)
let ta = on * mix(chk, 1.0, solid)
out = lcd_over(out, vec4(lcd_ink() * ta, ta))
return out * self.opacity
}
// ---------------------------------------------------------------------------
// WINDOWS. A group is a glass window set in the black plate the way the
// wristwatch sets each of its readouts: eight-sided, its corners cut by
// `cut` points (none rounds them instead), on a bezel `fw` points wide
// of which `fout` stand outside the box. The bezel is a grey lip one
// device pixel wide round its outer edge, lit along the top and a shade
// darker down the sides, a near-black band, and a black edge where it
// meets the glass; it stands a little proud, so a short shade falls round
// it where it sits on glass. The glass inside is a step lighter than the
// page and sits under the bezel on every side: an inner shadow all round,
// a little deeper under the top edge, then the sheen, the grain and the
// pixel matrix. An inset window is the same in a thinner bezel.
let lcd_window = fn(p: vec2, size: vec2, o: vec2, px: float, cut: float, fw: float, fout: float, sunken: float) -> vec4 {
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let hb = (hi - lo) * 0.5
let c = lo + hb
let fin = Finish.snap(fw - fout, px)
let fo = Finish.snap(fout, px)
let h = hb - vec2(fin, fin)
let hf = hb + vec2(fo, fo)
let wf = fin + fo
let rg = 2.0
let corner = step(0.01, cut)
let kg = max(cut, 0.01)
// the glass and the bezel's outer edge: cut at the corners, or rounded
let dg = mix(lcd_rbox(p, c, h, rg), Finish.sd_chamfer(p, c, h, kg), corner)
let df = mix(lcd_rbox(p, c, hf, rg + wf), Finish.sd_chamfer(p, c, hf, kg + wf * 0.4142), corner)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the bezel's shade on the page round it (lost on the black plate)
let ps = p - vec2(-0.5, 1.5)
let dsh = mix(lcd_rbox(ps, c, hf, rg + wf), Finish.sd_chamfer(ps, c, hf, kg + wf * 0.4142), corner)
let fcov = Finish.cover(df, px)
let sh = (1.0 - smoothstep(-1.5, 3.0, dsh)) * 0.26 * (1.0 - fcov)
out = vec4(0.0, 0.0, 0.0, sh)
// the bezel: a face falling from a dark grey at the top to near black
let fa = fcov * (1.0 - Finish.cover(dg, px))
let ft = clamp((p.y - (c.y - hf.y)) / max(2.0 * hf.y, 1.0), 0.0, 1.0)
var fcol = mix(vec3(0.149, 0.153, 0.173), vec3(0.110, 0.114, 0.129), smoothstep(0.0, 0.35, ft))
fcol = mix(fcol, vec3(0.122, 0.122, 0.145), smoothstep(0.7, 1.0, ft))
// the lip: one device pixel of grey round the outer edge, lit along
// the top, a shade darker down the sides and under the foot
let sy = clamp((c.y - p.y) / max(hf.y, 1.0), -1.0, 1.0)
let lipc = mix(vec3(0.204, 0.208, 0.227), vec3(0.302, 0.310, 0.333), smoothstep(-0.2, 0.6, sy))
fcol = mix(fcol, lipc, Finish.band(df, 0.0, px, px))
// the black edge where the bezel meets the glass
fcol = mix(fcol, vec3(0.012, 0.012, 0.012), Finish.band(-dg, 0.0, px, px))
out = lcd_over(out, vec4(fcol * fa, fa))
// the glass
let ga = Finish.cover(dg, px)
let gl = c - h
let u = ((p.x - gl.x) / max(2.0 * h.x, 1.0) - (p.y - gl.y) / max(2.0 * h.y, 1.0)) * 0.5
var col = lcd_glass_at(lcd_wglass(), p + o, px, 0.05 * u)
// the inner shadow all round: the glass sits well under the bezel on
// every side, ease-out from 28 per cent at the edge over five points
let ein = clamp(-dg / mix(5.0, 5.5, sunken), 0.0, 1.0)
col = col * mix(0.72, 1.0, 1.0 - (1.0 - ein) * (1.0 - ein))
// and deeper under the top edge, where the bezel keeps the light off
let reach = min(max(2.0 * h.y * 0.12, 5.0), 10.0)
let t = clamp((p.y - gl.y) / reach, 0.0, 1.0)
let fall = 1.0 - (1.0 - t) * (1.0 - t)
col = col * mix(mix(0.86, 0.82, sunken), 1.0, fall)
out = lcd_over(out, vec4(col * ga, ga))
return out
}
// THE WATCH FRAME. Every window on the sheet is the wristwatch's own LCD
// window: rounded by `r`, in a thin black bezel `fw` wide, all of it
// inside the box, printed with a red ring one device pixel wide a point
// out from the glass and a blue line along the bezel's outer edge, the
// glass a step lighter than the page sitting under it with the inner
// shadow (light on a readout, deep on a pane: `deep` from 0 to 1), the
// sheen, the grain and the pixel matrix.
let lcd_screen = fn(p: vec2, size: vec2, o: vec2, px: float, r: float, fw: float, deep: float) -> vec4 {
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let hb = (hi - lo) * 0.5
let c = lo + hb
let f = Finish.snap(fw, px)
let h = hb - vec2(f, f)
let dg = lcd_rbox(p, c, h, r)
let df = lcd_rbox(p, c, hb, r + f)
var out = vec4(0.0, 0.0, 0.0, 0.0)
// the bezel: near black, the blue line along its outer edge, the red
// ring a point out from the glass, and the black edge at the glass
let fa = Finish.cover(df, px) * (1.0 - Finish.cover(dg, px))
var fcol = vec3(0.071, 0.075, 0.086)
fcol = mix(fcol, vec3(0.188, 0.412, 0.741), Finish.band(df, 0.0, px, px))
fcol = mix(fcol, vec3(0.776, 0.157, 0.141), Finish.band(-dg, 1.0, 1.0 + px, px))
fcol = mix(fcol, vec3(0.012, 0.012, 0.012), Finish.band(-dg, 0.0, px, px))
out = lcd_over(out, vec4(fcol * fa, fa))
// the glass
let ga = Finish.cover(dg, px)
let gl = c - h
let u = ((p.x - gl.x) / max(2.0 * h.x, 1.0) - (p.y - gl.y) / max(2.0 * h.y, 1.0)) * 0.5
var col = lcd_glass_at(lcd_wglass(), p + o, px, 0.05 * u)
// the inner shadow all round, deeper under the top edge
let ein = clamp(-dg / mix(3.5, 5.0, deep), 0.0, 1.0)
col = col * mix(mix(0.82, 0.72, deep), 1.0, 1.0 - (1.0 - ein) * (1.0 - ein))
let t = clamp((p.y - gl.y) / mix(5.0, 9.0, deep), 0.0, 1.0)
col = col * mix(mix(0.9, 0.84, deep), 1.0, 1.0 - (1.0 - t) * (1.0 - t))
out = lcd_over(out, vec4(col * ga, ga))
return out
}
mod.widgets.PanelView.clip_x = false
mod.widgets.PanelView.clip_y = false
mod.widgets.PanelView.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 8.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.PanelView.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 8.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let r = min(8.0, 0.1 * min(self.rect_size.x, self.rect_size.y))
return lcd_screen(p, self.rect_size, self.rect_pos, px, r, 3.5, 1.0)
}
// SPLITTERS. The bar between two windows is the plate itself, so the
// windows stand on it as they stand everywhere else; the grip shows only
// under the pointer, as a bar of the bezel's grey, lit while it is held.
mod.widgets.Splitter.draw_bg.color_bg = #0a0b0d
mod.widgets.Splitter.draw_bg.color = #0a0b0d
mod.widgets.Splitter.draw_bg.color_hover = #34363b
mod.widgets.Splitter.draw_bg.color_drag = #4d5055
mod.widgets.Splitter.draw_bg.color_focus = #34363b
mod.widgets.Splitter.draw_bg.border_radius = 0.0
mod.widgets.Splitter.draw_bg.splitter_pad = 0.0
// The card is the wristwatch window too; its palette and
// shadow numbers from Rust are not read.
mod.widgets.Card.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 8.0)
return self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.Card.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let m = 8.0
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
var out = lcd_screen(p, self.rect_size, self.rect_pos, px, 5.0, 3.5, 0.5)
// the pointer lays a sparse dither over the glass; a press darkens it
let d = lcd_rbox(p, self.rect_size * 0.5, self.rect_size * 0.5 - vec2(3.5, 3.5), 5.0)
let la = Finish.cover(d, px) * lcd_quarter(p, self.rect_pos, px) * max(self.hover, self.down) * 0.8
let lcol = mix(lcd_mid(), lcd_dark(), self.down)
return lcd_over(out, vec4(lcol * la, la))
}
// The screen view is that window: its corner a tenth of its shorter side,
// five points at most, the bezel three and a half points, and no sheen laid
// over what it holds.
mod.widgets.ScreenView.sheen = 0.0
mod.widgets.ScreenView.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let r = min(5.0, 0.1 * min(self.rect_size.x, self.rect_size.y))
return lcd_screen(p, self.rect_size, self.rect_pos, px, r, 3.5, 0.0)
}
// The plain rounded view keeps the colours its caller gives it; the edge
// is one snapped device pixel.
mod.widgets.RoundedView.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let lo = vec2(self.border_inset.x, self.border_inset.y)
let hi = self.rect_size - vec2(self.border_inset.z, self.border_inset.w)
let sb = lcd_snapbox(self.rect_pos, lo, hi, px)
let d = lcd_rbox(p, (sb.xy + sb.zw) * 0.5, (sb.zw - sb.xy) * 0.5, max(min(self.border_radius, 6.0), 0.5))
var fill = self.color
if self.color_2.x > -0.5 {
fill = mix(self.color, self.color_2, mix(self.pos.y, self.pos.x, self.gradient_fill_horizontal))
}
let inside = Finish.cover(d, px)
var out = vec4(fill.rgb * fill.a * inside, fill.a * inside)
if self.border_size > 0.0 {
let la = Finish.band(d, 0.0, px, px) * self.border_color.a
out = lcd_over(out, vec4(self.border_color.rgb * la, la))
}
return out
}
// ---------------------------------------------------------------------------
// LISTS. Rows of 22 points, each ruled under in one device pixel of the
// ghost, as the mockup rules its lists; the rule stops ten points short of
// the right edge, where a scroll bar stands. The row's words have no chosen
// ink, so a chosen row cannot invert: it is a flat block of the glass a
// rung darker, two points in from the row's top, foot and left and from
// the rule's end, its corners cut in two-pixel steps, and it throws the
// ink's shadow. The pointer lays the checker dither, over the chosen block
// too; a disabled row lays none.
mod.widgets.ListItemOne.height = 22.0
mod.widgets.ListItem.padding = mod.turtle.Inset{left: 16.0 right: 8.0 top: 0.0 bottom: 0.0}
mod.widgets.ListItem.draw_text.color = #292b20
mod.widgets.ListItem.draw_text_2.color = #45493a
mod.widgets.ListItem.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let live = 1.0 - self.disabled
let sel = step(0.5, self.selected)
var out = vec4(0.0, 0.0, 0.0, 0.0)
let sb = lcd_snapbox(o, vec2(0.0, 1.0), self.rect_size - vec2(10.0, 1.0), px)
let inx = step(sb.x, p.x) * step(p.x, sb.z)
let iny = step(sb.y, p.y) * step(p.y, sb.w)
let inrow = inx * iny
// the rule under the row: its last device pixel, the row box wide
let yb = Finish.snap(o.y + self.rect_size.y, px) - o.y
let ra = step(yb - px, p.y) * step(p.y, yb) * inx
// the chosen row: the row box less two points at the sides and one at
// the top and foot, its corners cut two and a half pixels along; the
// cut passes between pixel centres, so the stair is two pixels deep
// wherever the row stands
let cb = lcd_snapbox(o, sb.xy + vec2(2.0, 1.0), sb.zw - vec2(2.0, 1.0), px)
let c = (cb.xy + cb.zw) * 0.5
let h = (cb.zw - cb.xy) * 0.5
let k = 2.5 * px
let dq = Finish.sd_chamfer(lcd_pix(p, o, px), c, h, k)
let fa = step(dq, 0.0) * sel
// its shadow on the reflector, laid under the rule and the block
let sh = lcd_inkshade(Finish.sd_chamfer(p - lcd_throw(), c, h, k), 0.10) * sel * (1.0 - fa)
out = lcd_over(out, vec4(lcd_ink() * sh, sh))
out = lcd_over(out, vec4(lcd_ghost() * ra, ra))
out = lcd_over(out, vec4(lcd_glass() * 0.86 * fa, fa))
// the pointer's dither, over the chosen block too
let chk = lcd_checker(p, o, px)
let ba = inrow * chk * max(self.hover, self.down) * live
out = lcd_over(out, vec4(lcd_mid() * ba, ba))
return out
}
// The open list of a drop-down: a glass sheet in one pixel of ink; the
// pointer's row lays the dither, the chosen row inverts.
mod.widgets.PopupMenuFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let sb = lcd_snapbox(self.rect_pos, vec2(0.0, 0.0), self.rect_size, px)
let d = lcd_rbox(p, (sb.xy + sb.zw) * 0.5, (sb.zw - sb.xy) * 0.5, 1.5)
let inside = Finish.cover(d, px)
let t = clamp((p.y - sb.y) / 4.0, 0.0, 1.0)
var col = lcd_glass_at(lcd_glass() * mix(0.8, 1.02, t), p + self.rect_pos, px, 0.0)
var out = vec4(col * inside, inside)
let la = Finish.band(d, 0.0, px, px)
return lcd_over(out, vec4(lcd_ink() * la, la))
}
mod.widgets.PopupMenuItem.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let live = 1.0 - self.disabled
let chk = lcd_checker(p, o, px)
let inrow = step(1.0, p.x) * step(p.x, self.rect_size.x - 1.0)
var out = vec4(0.0, 0.0, 0.0, 0.0)
let ha = chk * self.hover * live * (1.0 - self.active) * inrow
out = lcd_over(out, vec4(lcd_mid() * ha, ha))
let ia = self.active * inrow
out = lcd_over(out, vec4(lcd_ink() * ia, ia))
return out
}
mod.widgets.PopupMenuItem.draw_text.color_active = #b6bda2
// THE SPINNER. Eight segments round a plain centre, one in ink and the
// next two fading through the dark and light middles to the ghosts. A
// sheet may not read the pass clock and the spinner carries no other, so
// it stands still.
mod.widgets.SpinnerFlat.size = 16.0
mod.widgets.SpinnerFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let c = vec2(Finish.snap(o.x + self.rect_size.x * 0.5, px), Finish.snap(o.y + self.rect_size.y * 0.5, px)) - o
let rad = min(self.rect_size.x, self.rect_size.y) * 0.5
let v = p - c
var th = atan2(v.x, -v.y)
if th < 0.0 {
th = th + 2.0 * PI
}
let k = floor(th / (PI * 0.25) + 0.5)
let ang = k * PI * 0.25
let dir = vec2(sin(ang), -cos(ang))
let along = clamp(dot(v, dir), rad * 0.4, rad * 0.9)
let dd = length(v - dir * along) - rad * 0.11
let a = Finish.cover(dd, px) * self.alpha
let age = fract(k / 8.0)
var col = lcd_ghost()
col = mix(col, lcd_mid(), step(abs(k - 6.0), 0.5))
col = mix(col, lcd_dark(), step(abs(k - 7.0), 0.5))
col = mix(col, lcd_ink(), step(abs(k - 8.0), 0.5) + step(k, 0.5))
return vec4(col * a, a)
}
// Chips: a pixel-framed cell 20 points tall inside the room its size rung
// takes, one device pixel of ink; the pointer lays the dither; a chosen
// chip (light letters) is the inverse legend.
mod.widgets.ChipFlat.radius = 0.0
mod.widgets.ChipFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let inset = max((self.rect_size.y - 20.0) * 0.5, 0.0)
let sb = lcd_snapbox(o, vec2(0.0, inset), self.rect_size - vec2(0.0, inset), px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let pq = lcd_pix(p, o, px)
let dq = Finish.sd_chamfer(pq, c, h, 2.0 * px)
let inside = step(dq, 0.0)
let chosen = step(0.6, dot(self.ink.rgb, vec3(0.299, 0.587, 0.114)))
var out = vec4(0.0, 0.0, 0.0, 0.0)
let chk = lcd_checker(p, o, px)
let ha = inside * chk * max(self.hover, self.down) * (1.0 - chosen)
out = lcd_over(out, vec4(mix(lcd_mid(), lcd_dark(), self.down) * ha, ha))
let fa = inside * chosen
out = lcd_over(out, vec4(lcd_ink() * fa, fa))
let la = inside * step(-px, dq)
out = lcd_over(out, vec4(lcd_ink() * la, la))
let kf = inside * step(-3.0 * px, dq) * step(dq, -2.0 * px) * self.keyed
out = lcd_over(out, vec4(mix(lcd_ink(), lcd_lit(), chosen) * kf, kf))
return out
}
// Badges: a small inverse legend, dark with lit glass digits.
mod.widgets.BadgePalette.neutral = #292b20
mod.widgets.BadgePalette.on_neutral = #b6bda2
mod.widgets.BadgePalette.neutral_container = #92987f
mod.widgets.BadgePalette.on_neutral_container = #292b20
mod.widgets.BadgeFlat.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let bh = min(16.0, self.rect_size.y)
let sb = lcd_snapbox(o, vec2(0.0, (self.rect_size.y - bh) * 0.5), vec2(self.rect_size.x, (self.rect_size.y + bh) * 0.5), px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = Finish.sd_chamfer(lcd_pix(p, o, px), c, h, 2.0 * px)
let a = step(d, 0.0) * self.color.a
let ds = lcd_rbox(p - lcd_throw(), c, h, 1.0)
let sh = lcd_inkshade(ds, 0.14) * (1.0 - a)
var out = vec4(lcd_ink() * sh, sh)
return lcd_over(out, vec4(self.color.rgb * a, a))
}
// Rules between groups: one device pixel of ink.
mod.widgets.Hr.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let y = Finish.snap(self.rect_pos.y + self.rect_size.y * 0.5, px) - self.rect_pos.y
let a = step(y - px, p.y) * (1.0 - step(y, p.y))
return vec4(lcd_ink() * a, a)
}
mod.widgets.Vr.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let x = Finish.snap(self.rect_pos.x + self.rect_size.x * 0.5, px) - self.rect_pos.x
let a = step(x - px, p.x) * (1.0 - step(x, p.x))
return vec4(lcd_ink() * a, a)
}
// ---------------------------------------------------------------------------
// LAMPS. Nothing on the mockup's glass glows, so a lamp is a segment: lit,
// a disc of ink cut on the pixel grid, stairs and all, throwing the ink's
// shadow; half lit, the checker dither of ink over its ghost; out, the
// ghost alone. Every intent prints the same ink. The bar lamp is the same
// segment as a bar, its corners cut in two-pixel steps. The lens keeps the
// size and the place the lamp gives it; the quad is grown 4 points for the
// shadow.
let lcd_lamp = fn(p: vec2, size: vec2, o: vec2, px: float, lit: float, bar: float, reach: float) -> vec4 {
// the lens inside whatever room the quad keeps for a halo, a whole
// number of device pixels each way from a snapped corner, so a disc an
// even number of pixels wide centres on a pixel corner and an odd one
// on a pixel
let rch = clamp(reach, 0.0, 1.0)
let t = size.y / (1.0 + 2.0 * rch)
let run = max((size.x - 2.0 * t * rch) * 0.5, t * 0.5)
let h0 = vec2(mix(t * 0.5, run, bar), t * 0.5)
let c0 = size * 0.5
let nn = max(floor(2.0 * h0 / px + vec2(0.5, 0.5)), vec2(1.0, 1.0))
let lo = vec2(Finish.snap(o.x + c0.x - h0.x, px), Finish.snap(o.y + c0.y - h0.y, px)) - o
let h = nn * px * 0.5
let c = lo + h
// the segment, tested on pixel centres
let pq = lcd_pix(p, o, px)
let dq = mix(length(pq - c) - h.y, Finish.sd_chamfer(pq, c, h, 2.5 * px), bar)
let inside = step(dq, 0.0)
// the eased amount read in three steps: out, half lit, lit
let lv = floor(clamp(lit, 0.0, 1.0) * 2.0 + 0.5)
let full = step(1.5, lv)
let part = step(0.5, lv) * (1.0 - full)
let ia = inside * max(full, part * lcd_checker(p, o, px))
// the ink's shadow round the segment: all of it lit, half of it
// dithered, none from the ghost
let ps = p - lcd_throw()
let ds = mix(length(ps - c) - h.y, Finish.sd_chamfer(ps, c, h, 2.5 * px), bar)
let sh = lcd_inkshade(ds, 0.14) * (full + 0.5 * part) * (1.0 - inside)
var out = vec4(lcd_ink() * sh, sh)
// the ghost the segment always leaves, and the ink over it
out = lcd_over(out, vec4(lcd_ghost() * inside, inside))
out = lcd_over(out, vec4(lcd_ink() * ia, ia))
return out
}
mod.widgets.Lamp.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.Lamp.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
return lcd_lamp(p, self.rect_size, self.rect_pos, px, self.lit, self.bar, self.reach) * self.opacity
}
// The bar lamp is the lamp with its bar instance set: the same face.
mod.widgets.LampBar.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.LampBar.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
return lcd_lamp(p, self.rect_size, self.rect_pos, px, self.lit, self.bar, self.reach) * self.opacity
}
// ---------------------------------------------------------------------------
// SWITCHES. The rocker and the slide switch are flags, as the toggles are:
// a box of one device pixel of ink whose thrown side is an inverted cell
// and whose other side is empty glass, which the pointer lays the checker
// dither over and a press darkens toward its turn. Disabled, the ink drops
// to the light middle; an error doubles the line. The rocker's two cells
// are split by a one-pixel rule, a dot-matrix O on the off cell and an I
// one pixel wide on the on cell, lit glass on the thrown cell's ink and at
// ghost on the other. The quads are grown 5 points for the ink shadow and
// the focus ring.
let lcd_rocker = fn(p: vec2, sb: vec4, o: vec2, px: float, t: float, hover: float, down: float, focus: float, fault: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let held = down * live
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 0.01)
let inside = Finish.cover(d, px)
// the cells either side of the rule; `t` at 1 throws the on cell
let mid = Finish.snap(o.x + c.x - px, px) - o.x
let in_on = step(mid, p.x)
let cur = mix(1.0 - in_on, in_on, t)
let sh = lcd_inkshade(abs(lcd_rbox(p - lcd_throw(), c, h, 0.01)) - px * 0.5, 0.14) * (1.0 - inside) * live
var out = vec4(lcd_ink() * sh, sh)
// the other cell: dithered under the pointer, darker while pressed
let oa = inside * (1.0 - cur) * lcd_checker(p, o, px) * max(hover * live, held)
out = lcd_over(out, vec4(mix(lcd_mid(), lcd_dark(), held) * oa, oa))
// the thrown cell filled, the box, and the rule between the cells
let icol = mix(lcd_ink(), lcd_mid(), disabled)
let fa = inside * cur
out = lcd_over(out, vec4(icol * fa, fa))
let rule = step(mid, p.x) * step(p.x, mid + px) * inside
let la = max(Finish.band(d, 0.0, px, px), rule)
out = lcd_over(out, vec4(icol * la, la))
let el = Finish.ring_out(d, px, 2.0 * px, px) * fault
out = lcd_over(out, vec4(lcd_ink() * el, el))
// the legends, 7 pixels tall in the middle of their cells
let yl = Finish.snap(o.y + c.y - 3.5 * px, px) - o.y
let xo = Finish.snap(o.x + (sb.x + px + mid) * 0.5 - 2.5 * px, px) - o.x
let xi = Finish.snap(o.x + (mid + sb.z) * 0.5 - 0.5 * px, px) - o.x
let qp = floor((p + o) / px + vec2(0.001, 0.001))
let qo = qp - floor((vec2(xo, yl) + o) / px + vec2(0.5, 0.5))
let qi = qp - floor((vec2(xi, yl) + o) / px + vec2(0.5, 0.5))
let g_o = lcd_letter(0.0, qo)
let g_i = step(abs(qi.x), 0.5) * step(0.0, qi.y) * step(qi.y, 6.5)
let won = g_i * t + g_o * (1.0 - t)
let wgh = g_i * (1.0 - t) + g_o * t
let ghost_col = mix(mix(lcd_mid(), lcd_dark(), 0.25), lcd_ghost(), 0.6 * disabled)
out = lcd_over(out, vec4(ghost_col * wgh, wgh))
out = lcd_over(out, vec4(mix(lcd_lit(), lcd_glass(), disabled) * won, won))
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.ToggleRocker.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ToggleRocker.draw_bg.pixel = fn() {
let m = 5.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let sb = lcd_markbox(self.rect_size, self.rect_pos, self.size * self.pill_aspect, self.size, self.mark_at_end, px)
let t = step(0.5, mix(self.active, self.drag_pos, self.drag))
return lcd_rocker(p, sb, self.rect_pos, px, t, self.hover, self.down, self.focus, self.error, self.disabled)
}
mod.widgets.ToggleRocker.clip_x = false
mod.widgets.ToggleRocker.clip_y = false
// The slide: its thrown cell is the knob, a square of ink a pixel or more
// clear of the box, its corners notched, with three grip lines of lit
// glass across its middle. It follows a drag and throws the ink's shadow
// on the slot; the free end of the slot holds a small square segment, ink
// while on and a ghost while off.
let lcd_slide = fn(p: vec2, sb: vec4, o: vec2, px: float, kt: float, inset: float, pitch: float, hover: float, down: float, focus: float, fault: float, disabled: float) -> vec4 {
let live = 1.0 - disabled
let held = down * live
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let d = lcd_rbox(p, c, h, 0.01)
let inside = Finish.cover(d, px)
// the knob: whole device pixels, the line and the inset in from the
// box, travelling the slot's length less its own
let cl = px + max(floor(inset / px + 0.5), 1.0) * px
let ks = max(sb.w - sb.y - 2.0 * cl, 2.0 * px)
let travel = max(sb.z - sb.x - 2.0 * cl - ks, 0.0)
let t = step(0.5, kt)
let kx = Finish.snap(o.x + sb.x + cl + clamp(kt, 0.0, 1.0) * travel, px) - o.x
let ky = sb.y + cl
let kh = vec2(ks * 0.5, ks * 0.5)
let kc = vec2(kx, ky) + kh
let pq = lcd_pix(p, o, px)
let knob = step(Finish.sd_chamfer(pq, kc, kh, 1.5 * px), 0.0)
// the mark: the knob's square at the free end, a third of it in on
// every side
let fx = mix(sb.z - cl - ks * 0.5, sb.x + cl + ks * 0.5, t)
let mm = floor(ks / px / 3.0 + 0.5) * px
let mh = max(kh - vec2(mm, mm), vec2(px * 0.5, px * 0.5))
let mark = step(lcd_rbox(pq, vec2(fx, kc.y), mh, 0.01), 0.0) * (1.0 - knob)
// the ink's shadow: the line's round the box, the knob's on the slot
let th = lcd_throw()
let sl = lcd_inkshade(abs(lcd_rbox(p - th, c, h, 0.01)) - px * 0.5, 0.14) * (1.0 - inside)
let sk = lcd_inkshade(Finish.sd_chamfer(p - th, kc, kh, 1.5 * px), 0.14) * (1.0 - knob) * inside
let sh = max(sl, sk) * live
var out = vec4(lcd_ink() * sh, sh)
// the free slot: dithered under the pointer, darker while pressed
let oa = inside * (1.0 - knob) * lcd_checker(p, o, px) * max(hover * live, held)
out = lcd_over(out, vec4(mix(lcd_mid(), lcd_dark(), held) * oa, oa))
// the box, doubled by an error
let icol = mix(lcd_ink(), lcd_mid(), disabled)
let la = Finish.band(d, 0.0, px, px)
out = lcd_over(out, vec4(icol * la, la))
let el = Finish.ring_out(d, px, 2.0 * px, px) * fault
out = lcd_over(out, vec4(lcd_ink() * el, el))
// the mark: ink while on, a ghost while off
let mcol = mix(mix(lcd_ghost(), vec3(0.604, 0.631, 0.529), disabled), icol, t)
out = lcd_over(out, vec4(mcol * mark, mark))
// the knob, and its grip: three lit columns one device pixel wide,
// `pitch` apart, over the middle half of its height
out = lcd_over(out, vec4(icol * knob, knob))
let kw = floor(ks / px + 0.5)
let gp = max(floor(pitch / px + 0.5), 2.0)
let qk = floor((p + o) / px + vec2(0.001, 0.001)) - floor((vec2(kx, ky) + o) / px + vec2(0.5, 0.5))
let rel = qk.x - floor(kw * 0.5)
let gq = floor(rel / gp + 0.5)
let rim = floor(kw * 0.25)
let grip = step(abs(rel - gq * gp), 0.5) * step(abs(gq), 1.0) * step(rim, qk.y) * step(qk.y, kw - 1.0 - rim) * knob
out = lcd_over(out, vec4(mix(lcd_lit(), lcd_glass(), disabled) * grip, grip))
let kf = lcd_dash_ring(d, p, c, h, o, 2.0, px) * focus * live
out = lcd_over(out, vec4(lcd_ink() * kf, kf))
return out
}
mod.widgets.ToggleSlide.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 5.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ToggleSlide.draw_bg.pixel = fn() {
let m = 5.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let sb = lcd_markbox(self.rect_size, self.rect_pos, self.size * self.pill_aspect, self.size, self.mark_at_end, px)
let kt = mix(self.active, self.drag_pos, self.drag)
return lcd_slide(p, sb, self.rect_pos, px, kt, self.knob_inset, self.grip_pitch, self.hover, self.down, self.focus, self.error, self.disabled)
}
mod.widgets.ToggleSlide.clip_x = false
mod.widgets.ToggleSlide.clip_y = false
// ---------------------------------------------------------------------------
// THE NEEDLE METER. A scale printed in ink on the face, which is the
// screen's glass, framed in one device pixel with its corners cut in
// two-pixel steps: an arc of one pixel, a tick at every division, the
// majors two pixels wide and longer, and the majors' values in small
// segment digits inside the arc wherever they have the room. The loud end
// is a band of ink dither just outside the arc, no colour. The needle is a
// bar of ink two pixels wide from the pivot to past the scale, under a
// disc of ink cut on the pixel grid; the needle, the pivot, the scale and
// the frame throw the ink's shadow. The quad is grown 4 points for the
// frame's.
// The distance to the scale's arc, at q from the pivot: a band one device
// pixel wide, ended square at both ends of the sweep. The angle is taken a
// hair off the pivot, which may lie on a pixel centre.
let lcd_arc_d = fn(q: vec2, rr: float, sweep: float, px: float) -> float {
let r = length(q)
let t = atan2(q.x, 0.0001 - q.y) / sweep + 0.5
return max(abs(r - rr) - px * 0.5, max(-t, t - 1.0) * sweep * r)
}
// The distance to the nearest tick of a scale of n divisions, a major one
// every `nmin`, at q from the pivot: a minor one device pixel wide, a major
// two, each running in from the arc by its length.
let lcd_tick_d = fn(q: vec2, rr: float, sweep: float, n: float, nmin: float, tmaj: float, tmin: float, px: float) -> float {
let t = atan2(q.x, 0.0001 - q.y) / sweep + 0.5
let k = floor(clamp(t, 0.0, 1.0) * n + 0.5)
let ak = (k / n - 0.5) * sweep
let dir = vec2(sin(ak), -cos(ak))
let major = 1.0 - step(0.5, k - nmin * floor(k / nmin + 0.0001))
let r = length(q)
let perp = abs(q.x * dir.y - q.y * dir.x) - px * mix(0.5, 1.0, major)
let radial = max(rr - mix(tmin, tmaj, major) - r, r - rr)
return max(max(perp, radial), -dot(q, dir))
}
// The distance to the needle: a bar `w` either side of its axis, from the
// pivot out to `reach` along nd.
let lcd_needle_d = fn(q: vec2, nd: vec2, reach: float, w: float) -> float {
let along = dot(q, nd)
let across = abs(q.x * nd.y - q.y * nd.x)
return max(across - w, max(along - reach, -along))
}
// The value of major j of nmaj, 0 to 100, in small segment digits set
// upright inside the arc, in from `inner` along the major by half the
// number's own extent there: coverage at p.
let lcd_meter_number = fn(p: vec2, o: vec2, pv: vec2, sweep: float, j: float, nmaj: float, inner: float, px: float) -> float {
let dh = 5.5
let adv = dh * 0.68
let cw = dh * 0.5
let v = floor(j / nmaj * 100.0 + 0.5)
let nd = 1.0 + step(9.5, v) + step(99.5, v)
let w = (nd - 1.0) * adv + cw
let aj = (j / nmaj - 0.5) * sweep
let dir = vec2(sin(aj), -cos(aj))
let ext = w * 0.5 * abs(dir.x) + dh * 0.5 * abs(dir.y)
let cc = pv + dir * (inner - ext)
let left = Finish.snap(o.x + cc.x - w * 0.5, px) - o.x
let top = Finish.snap(o.y + cc.y - dh * 0.5, px) - o.y
let k = floor((p.x - left) / adv)
let u = (p.x - left - k * adv) / cw
// the digit k places from the left: hundreds, tens or units
let ex = nd - 1.0 - k
let pw = mix(1.0, mix(10.0, 100.0, step(1.5, ex)), step(0.5, ex))
var dg = floor(v / pw + 0.0001)
dg = dg - 10.0 * floor(dg / 10.0 + 0.0001)
let inside = step(0.0, k) * step(k, nd - 0.5) * step(0.0, u) * step(u, 1.0)
return Finish.seg7(vec2(u, (p.y - top) / dh), dg, px / dh) * inside
}
// The meter's face: the glass and its frame, the scale, the values, the
// zone, the needle and the pivot.
let lcd_meter = fn(p: vec2, size: vec2, o: vec2, px: float, face: vec4, sheen: float, pv: vec2, rr: float, sweep: float, majors: float, minors: float, zone: float, tmaj: float, tmin: float, pr: float, na: float) -> vec4 {
// the face on whole device pixels, its corners cut in two-pixel steps
let lo = vec2(Finish.snap(o.x, px), Finish.snap(o.y, px)) - o
let hi = vec2(Finish.snap(o.x + size.x, px), Finish.snap(o.y + size.y, px)) - o
let c = (lo + hi) * 0.5
let h = (hi - lo) * 0.5
let pq = lcd_pix(p, o, px)
let dq = Finish.sd_chamfer(pq, c, h, 2.5 * px)
let fa = step(dq, 0.0)
let th = lcd_throw()
// the frame's shadow on the glass round the face
let sf = lcd_inkshade(abs(Finish.sd_chamfer(p - th, c, h, 2.5 * px) + px * 0.5) - px * 0.5, 0.14)
let so = sf * (1.0 - fa)
var out = vec4(lcd_ink() * so, so)
// the screen's glass with the windows' sheen, grain and pixel matrix
let u = ((p.x - lo.x) / max(2.0 * h.x, 1.0) - (p.y - lo.y) / max(2.0 * h.y, 1.0)) * 0.5
let gcol = lcd_glass_at(face.rgb, p + o, px, 2.0 * sheen * u)
out = lcd_over(out, vec4(gcol * fa, fa))
// the frame, the arc and the ticks
let nmaj = max(floor(majors + 0.5), 1.0)
let nmin = max(floor(minors + 0.5), 1.0)
let n = nmaj * nmin
let q = p - pv
let frame = fa * step(-px, dq)
let arc = Finish.cover(lcd_arc_d(q, rr, sweep, px), px)
let tick = Finish.cover(lcd_tick_d(q, rr, sweep, n, nmin, tmaj, tmin, px), px)
// the values, read off the major nearest p, where the majors stand far
// enough apart for the widest of them
let t = atan2(q.x, 0.0001 - q.y) / sweep + 0.5
let j = floor(clamp(t, 0.0, 1.0) * nmaj + 0.5)
let roomy = step(13.0, (rr - tmaj - 7.5) * sweep / nmaj)
let num = lcd_meter_number(p, o, pv, sweep, j, nmaj, rr - tmaj - 2.0, px) * roomy
// the loud end: a checker of ink just outside the arc
var zn = 0.0
if zone < 0.999 {
let rz = length(q) - rr
zn = step(zone, t) * step(t, 1.0001) * step(px * 0.5, rz) * step(rz, 3.0) * lcd_checker(p, o, px)
}
// the needle, and the disc of the pivot over its root
let nd = vec2(sin(na), -cos(na))
let needle = Finish.cover(lcd_needle_d(q, nd, rr + 3.0, px), px)
let pin = step(length(pq - pv), pr)
let ink = max(max(max(frame, arc), max(tick, num)), max(max(zn, needle), pin)) * fa
// the ink's shadow on the glass, of all but the values and the zone
let qs = q - th
let ss = lcd_inkshade(min(lcd_arc_d(qs, rr, sweep, px), lcd_tick_d(qs, rr, sweep, n, nmin, tmaj, tmin, px)), 0.14)
let sn = lcd_inkshade(min(lcd_needle_d(qs, nd, rr + 3.0, px), length(qs) - pr), 0.14)
let sh = max(sf, max(ss, sn)) * fa * (1.0 - ink)
out = lcd_over(out, vec4(lcd_ink() * sh, sh))
out = lcd_over(out, vec4(lcd_ink() * ink, ink))
return out * face.a
}
mod.widgets.NeedleMeter.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.NeedleMeter.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let pv = lcd_pix(self.pivot(), o, px)
let na = self.needle_angle(self.value)
return lcd_meter(p, self.rect_size, o, px, self.color, self.sheen, pv, self.scale_radius(), self.sweep * 0.017453293, self.majors, self.minors, self.zone, self.tick_major, self.tick_minor, self.pivot_size, na) * self.opacity
}
mod.widgets.NeedleMeter.clip_x = false
mod.widgets.NeedleMeter.clip_y = false
// ---------------------------------------------------------------------------
// RINGS AND ARCS. The gauge bar bent round: a band outlined in one device
// pixel of ink along both its edges and across its ends, which are cut
// square. Inside the line, past a device pixel of glass where the band is
// seven device pixels or more across, the part still to go stands at the
// ghost and the part travelled is solid ink, from the start and never
// shorter than it is wide; a whole ring has no ends, so its run leaves the
// top with no gap, and a ring cut into sections is a row of such pieces.
// The amount that is not known stands still as the checker dither round
// the whole inside. The line throws the ink's shadow outside the band and
// the run on the glass and the ghost beside it. Round caps and the intent
// colours are not read, so the activity rings nest three of these bands
// all in ink, told apart by the glass between them and not by a hue. The
// label stays in the middle, at the foot of an arc, in the ink. The quad
// is grown 4 points for the shadow.
// Where q, from the centre, stands on a piece of ring that leaves the
// shader angle a0 (nought straight down, rising clockwise, -pi at the
// top) and runs `span` radians on, clockwise for dir 1 and back for -1:
// .x its distance past the start and .y past the end, in points along
// the arc, and .z the angle travelled from the start.
let lcd_sweep_at = fn(q: vec2, a0: float, span: float, dir: float) -> vec3 {
let rl = length(q)
var dl = atan2(-q.x, q.y) - a0 - dir * span * 0.5
dl = dl - 2.0 * PI * floor((dl + PI) / (2.0 * PI))
let u = dir * dl + span * 0.5
return vec3(-u * rl, (u - span) * rl, u)
}
// One piece of band about c, its outer edge r1 out and `th` across, from
// the shader angle a0 through `span` (dir as above), with its run of ink
// `run` radians from the start, below nought when the amount is not known.
let lcd_ring_face = fn(p: vec2, c: vec2, o: vec2, px: float, r1: float, th: float, a0: float, span: float, dir: float, run: float, track: float) -> vec4 {
let q = p - c
let qs = q - lcd_throw()
let rl = length(q)
let rls = length(qs)
let hw = th * 0.5
let rc = r1 - hw
// the inside: in past the line and, where the band has the room, a
// device pixel of glass
let gi = px + px * step(7.0 * px, th + 0.5 * px)
let hn = max(hw - gi, min(0.5 * px, hw))
// a whole ring has no ends to cut
let cut = step(span, 2.0 * PI - 0.001)
let e = lcd_sweep_at(q, a0, span, dir)
let es = lcd_sweep_at(qs, a0, span, dir)
let de = mix(-1000.0, max(e.x, e.y), cut)
let d = max(abs(rl - rc) - hw, de)
let ds = max(abs(rls - rc) - hw, mix(-1000.0, max(es.x, es.y), cut))
let inside = Finish.cover(d, px)
let ia = Finish.cover(max(abs(rl - rc) - hn, de + gi), px)
let la = Finish.band(d, 0.0, px, px) * track
var ga = 0.0
var fa = 0.0
var shf = 0.0
if run >= 0.0 {
// the run, square at both ends; at an end of the band it keeps the
// inside's inset, and it is never shorter than it is wide
let gs = gi * cut
let on = step(0.0001, run)
let sf = min(max(run, (2.0 * hn + gs) / max(rc, px)), span)
let whole = step(2.0 * PI - 0.001, sf)
let fe = lcd_sweep_at(q, a0, sf, dir)
let fs = lcd_sweep_at(qs, a0, sf, dir)
let df = max(max(abs(rl - rc) - hn, mix(max(fe.x + gs, fe.y), -1000.0, whole)), mix(-1000.0, e.y + gi, cut))
let dfs = max(max(abs(rls - rc) - hn, mix(max(fs.x + gs, fs.y), -1000.0, whole)), mix(-1000.0, es.y + gi, cut))
ga = ia * track
fa = Finish.cover(df, px) * on
shf = lcd_inkshade(dfs, 0.14) * on * (1.0 - fa)
} else {
// not known: the checker dither round the whole inside
fa = ia * lcd_checker(p, o, px)
}
// the ink's shadow on the reflector: the line's outside the band, the
// run's on the glass and the ghost beside it
let shl = lcd_inkshade(abs(ds + px * 0.5) - px * 0.5, 0.14) * track * (1.0 - inside)
let sh = max(shl, shf)
var out = vec4(lcd_ghost() * ga, ga)
out = lcd_over(out, vec4(lcd_ink() * sh, sh))
out = lcd_over(out, vec4(lcd_ink() * la, la))
out = lcd_over(out, vec4(lcd_ink() * fa, fa))
return out
}
mod.widgets.ProgressRingFlat.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.ProgressRingFlat.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
// the centre on a pixel corner and the edges whole device pixels out
// from it; a nested ring stands `inset` in
let c = vec2(Finish.snap(o.x + self.rect_size.x * 0.5, px), Finish.snap(o.y + self.rect_size.y * self.center_y, px)) - o
let r1 = max(floor((min(self.rect_size.x * 0.5, self.rect_size.y * self.center_y) - self.inset) / px + 0.001) * px, 3.0 * px)
let th = clamp(Finish.snap(self.thickness, px), 3.0 * px, r1)
// the stock's angles; a negative direction runs from the far end
let flip = step(self.direction, 0.0)
let a0 = self.base_angle + mix(self.start, 1.0 - self.start, flip) * self.sweep
var run = -1.0
if self.value >= 0.0 {
run = (clamp(self.value, self.start, self.end) - self.start) * self.sweep
}
return lcd_ring_face(p, c, o, px, r1, th, a0, max(self.end - self.start, 0.0) * self.sweep, 1.0 - 2.0 * flip, run, self.track) * self.opacity
}
mod.widgets.ProgressRingFlat.clip_x = false
mod.widgets.ProgressRingFlat.clip_y = false
mod.widgets.ProgressRingFlat.draw_text.color = #292b20
// GAUGES. The dial is the ring's band over three quarters of a turn, open
// at the foot, with the scale printed inside it: ten steps two points in
// from the band and a long tick at 0, 50 and 100, one device pixel of ink
// cut on the pixel grid, so the centre stands in the middle of a pixel.
// The zones are dithers of ink on the ghost inside the band, never a
// colour: the safe zone plain, the warning zone one pixel in four, the
// critical zone the checker. The needle is two device pixels of ink from
// six points behind the hub to the scale, on a hub three points round cut
// on the pixel grid; with a needle the band keeps its zones whole and the
// needle carries the reading, without one the part travelled fills with
// ink. Laid flat, the band is the gauge bar's pill, its scale nine ink
// dividers across the inside at the tenths and its marker two device
// pixels of ink across the pill, two points proud of it. The line, the
// run, the ticks, the needle and the marker throw the ink's shadow.
// `band` at nought leaves the zones out; their blend and the palette are
// not read. The quad is grown 4 points for the shadow.
// The dial about c, its outer edge r1 out: the band from the lower left
// through `sweep` to the lower right, the zones, the scale and the needle
// at v.
let lcd_gauge_dial = fn(p: vec2, c: vec2, o: vec2, px: float, r1: float, th: float, sweep: float, v: float, warn: float, crit: float, zones: float, needle: float) -> vec4 {
let q = p - c
let qs = q - lcd_throw()
let rl = length(q)
let rls = length(qs)
// the opening is centred on the foot
let a0 = PI - sweep * 0.5
let hw = th * 0.5
let rc = r1 - hw
let gi = px + px * step(7.0 * px, th + 0.5 * px)
let hn = max(hw - gi, min(0.5 * px, hw))
let e = lcd_sweep_at(q, a0, sweep, 1.0)
let es = lcd_sweep_at(qs, a0, sweep, 1.0)
let d = max(abs(rl - rc) - hw, max(e.x, e.y))
let ds = max(abs(rls - rc) - hw, max(es.x, es.y))
let inside = Finish.cover(d, px)
let ia = Finish.cover(max(abs(rl - rc) - hn, max(e.x, e.y) + gi), px)
let la = Finish.band(d, 0.0, px, px)
// the zones by the share of the sweep: plain, then one pixel in four,
// then the checker
let f = e.z / max(sweep, 0.001)
let z = step(warn, f) + step(crit, f)
let za = ia * mix(lcd_quarter(p, o, px), lcd_checker(p, o, px), step(1.5, z)) * step(0.5, z) * zones
// without a needle, the run of ink as on a ring
let sf = min(max(v * sweep, (2.0 * hn + gi) / max(rc, px)), sweep)
let fe = lcd_sweep_at(q, a0, sf, 1.0)
let fs = lcd_sweep_at(qs, a0, sf, 1.0)
let on = step(0.001, v) * (1.0 - needle)
let fa = Finish.cover(max(max(abs(rl - rc) - hn, max(fe.x + gi, fe.y)), e.y + gi), px) * on
let shf = lcd_inkshade(max(max(abs(rls - rc) - hn, max(fs.x + gi, fs.y)), es.y + gi), 0.14) * on * (1.0 - fa)
// the scale: a tick at each tenth, the long ones at 0, 50 and 100,
// two points in from the band
let rt = rc - hw - 2.0
let k = clamp(floor(f * 10.0 + 0.5), 0.0, 10.0)
let tl = mix(3.0, 5.0, step(fract(k / 5.0 + 0.01), 0.1))
let tk = step(abs(f - k * 0.1) * sweep * rl, 0.5 * px) * step(rt - tl, rl) * step(rl, rt)
let fsh = es.z / max(sweep, 0.001)
let ks = clamp(floor(fsh * 10.0 + 0.5), 0.0, 10.0)
let tls = mix(3.0, 5.0, step(fract(ks / 5.0 + 0.01), 0.1))
let sht = lcd_inkshade(max(abs(fsh - ks * 0.1) * sweep * rls - 0.5 * px, max(rt - tls - rls, rls - rt)), 0.14) * (1.0 - tk)
// the needle from behind the hub to the scale, and the hub
let av = a0 + v * sweep
let nd = vec2(-sin(av), cos(av))
let tip = nd * rt
let tail = vec2(0.0, 0.0) - nd * 6.0
let hub = step(length(lcd_pix(p, o, px) - c), 3.0)
let na = max(Finish.cover(lcd_seg(q, tail, tip) - px, px), hub) * needle
let nsh = lcd_inkshade(min(lcd_seg(qs, tail, tip) - px, rls - 3.0), 0.14) * needle * (1.0 - na)
// the line's shadow outside the band, and the rest
let shl = lcd_inkshade(abs(ds + px * 0.5) - px * 0.5, 0.14) * (1.0 - inside)
let sh = max(max(shl, shf), max(nsh, sht))
var out = vec4(lcd_ghost() * ia, ia)
out = lcd_over(out, vec4(lcd_ink() * sh, sh))
out = lcd_over(out, vec4(lcd_ink() * za, za))
out = lcd_over(out, vec4(lcd_ink() * tk, tk))
out = lcd_over(out, vec4(lcd_ink() * la, la))
out = lcd_over(out, vec4(lcd_ink() * fa, fa))
out = lcd_over(out, vec4(lcd_ink() * na, na))
return out
}
// Laid flat: the gauge bar's pill `th` tall across the walk, the zones
// along its inside, the dividers and the marker at v.
let lcd_gauge_bar = fn(p: vec2, o: vec2, size: vec2, px: float, th: float, v: float, warn: float, crit: float, zones: float, needle: float) -> vec4 {
let tw = lcd_throw()
let hgt = min(th, size.y)
let y0 = (size.y - hgt) * 0.5
let sb = lcd_snapbox(o, vec2(0.0, y0), vec2(size.x, y0 + hgt), px)
let c = (sb.xy + sb.zw) * 0.5
let h = (sb.zw - sb.xy) * 0.5
let r = min(h.x, h.y)
let d = lcd_rbox(p, c, h, r)
let inside = Finish.cover(d, px)
let la = Finish.band(d, 0.0, px, px)
// the inside, two device pixels in, as on the bar
let hf = max(h.y - 2.0 * px, min(px, h.y))
let xi0 = sb.x + 2.0 * px
let xi1 = max(sb.z - 2.0 * px, xi0)
let kh = vec2((xi1 - xi0) * 0.5, hf)
let ia = Finish.cover(lcd_rbox(p, vec2(xi0 + kh.x, c.y), kh, min(kh.x, hf)), px)
let f = (p.x - xi0) / max(xi1 - xi0, 0.001)
let z = step(warn, f) + step(crit, f)
let za = ia * mix(lcd_quarter(p, o, px), lcd_checker(p, o, px), step(1.5, z)) * step(0.5, z) * zones
// the scale: a divider one device pixel wide at each tenth between
let k = clamp(floor(f * 10.0 + 0.5), 1.0, 9.0)
let xk = Finish.snap(o.x + mix(xi0, xi1, k * 0.1), px) - o.x
let tk = ia * step(xk, p.x) * step(p.x, xk + px)
// without a marker, the run of ink as on the bar
let xv = Finish.snap(o.x + mix(xi0, xi1, v), px) - o.x
let xe = min(max(xv, xi0 + 2.0 * hf), xi1)
let fh = vec2((xe - xi0) * 0.5, hf)
let fc = vec2(xi0 + fh.x, c.y)
let rf = min(fh.x, hf)
let on = step(0.001, v) * (1.0 - needle)
let fa = Finish.cover(lcd_rbox(p, fc, fh, rf), px) * on
let shf = lcd_inkshade(lcd_rbox(p - tw, fc, fh, rf), 0.14) * on * (1.0 - fa)
// the marker: two device pixels of ink across the pill, two points
// proud of it either side
let mc = vec2(xv, c.y)
let mh = vec2(px, h.y + Finish.snap(2.0, px))
let ma = Finish.cover(lcd_rbox(p, mc, mh, 0.0), px) * needle
let msh = lcd_inkshade(lcd_rbox(p - tw, mc, mh, 0.0), 0.14) * needle * (1.0 - ma)
let shl = lcd_inkshade(abs(lcd_rbox(p - tw, c, h, r) + px * 0.5) - px * 0.5, 0.14) * (1.0 - inside)
let sh = max(max(shl, shf), msh)
var out = vec4(lcd_ghost() * ia, ia)
out = lcd_over(out, vec4(lcd_ink() * sh, sh))
out = lcd_over(out, vec4(lcd_ink() * za, za))
out = lcd_over(out, vec4(lcd_ink() * tk, tk))
out = lcd_over(out, vec4(lcd_ink() * la, la))
out = lcd_over(out, vec4(lcd_ink() * fa, fa))
out = lcd_over(out, vec4(lcd_ink() * ma, ma))
return out
}
mod.widgets.Gauge.draw_bg.vertex = fn() {
let g = lcd_grow(self.rect_pos, self.rect_size, 4.0)
self.vertex_pos = self.clip_and_transform_vertex(g.xy, g.zw)
}
mod.widgets.Gauge.draw_bg.pixel = fn() {
let m = 4.0
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m)
let o = self.rect_pos
let v = clamp(self.value, 0.0, 1.0)
let zoned = step(0.01, self.band)
let hand = step(0.5, self.needle)
var out = vec4(0.0, 0.0, 0.0, 0.0)
if self.linear > 0.5 {
out = lcd_gauge_bar(p, o, self.rect_size, px, self.thickness, v, self.zone_warn, self.zone_critical, zoned, hand)
} else {
// the centre in the middle of a pixel and the edges half a pixel
// off it: the ticks run through whole pixels, the band's edges
// between them
let c = (floor((o + self.rect_size * 0.5) / px + vec2(0.001, 0.001)) + vec2(0.5, 0.5)) * px - o
let r1 = (floor(min(self.rect_size.x, self.rect_size.y) * 0.5 / px - 0.5) + 0.5) * px
let th = clamp(Finish.snap(self.thickness, px), 3.0 * px, max(r1 * 0.5, 3.0 * px))
out = lcd_gauge_dial(p, c, o, px, r1, th, self.sweep, v, self.zone_warn, self.zone_critical, zoned, hand)
}
return out * self.opacity
}
mod.widgets.Gauge.clip_x = false
mod.widgets.Gauge.clip_y = false
mod.widgets.Gauge.draw_text.color = #292b20
mod.widgets.Gauge.draw_range.color = #45493a
// THE NAVIGATION LINE. One device pixel of ink through the middle of its
// strip, run out to the value, and the rest of the strip's width at the
// ghost. Done, it does not fade under a veil: as its opacity falls it
// drops out a pixel at a time, in a fixed order of four along the line.
mod.widgets.NavigationProgress.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size
let o = self.rect_pos
let y = Finish.snap(o.y + (self.rect_size.y - px) * 0.5, px) - o.y
let row = step(y, p.y) * step(p.y, y + px)
let xv = Finish.snap(o.x + self.rect_size.x * clamp(self.value, 0.0, 1.0), px) - o.x
let col = mix(lcd_ghost(), lcd_ink(), step(p.x, xv))
// a pixel's place in the order, counted in window pixels along x
let i = floor(fract(floor((p.x + o.x) / px + 0.001) * 0.25) * 4.0 + 0.5)
let a = row * step((4.0 * fract(i * 0.5) + floor(i * 0.5) + 0.5) * 0.25, self.opacity)
return vec4(col * a, a)
}
// ---------------------------------------------------------------------------
// THE GROUND. The window is the black plate of the wristwatch's face, and
// the glass is in the windows set into it: every pane and panel is one,
// so the plate shows as the bezels between them. Flat near-black with a
// grain one device pixel fine, no sheen.
mod.widgets.Window.show_bg = true
mod.widgets.Window.draw_bg.pixel = fn() {
let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5)
let p = self.pos * self.rect_size + self.rect_pos
let g = fract(sin(dot(floor(p / px), vec2(12.9898, 78.233))) * 43758.5453)
let col = vec3(0.039, 0.043, 0.051) * (0.94 + 0.12 * g)
return vec4(col, 1.0)
}
true