use mod.prelude.widgets_internal.* use mod.sdf.Cap // THE MATERIAL. One pale ground, pushed up into raised parts or pressed into // wells. Every face below is built from the same few pieces, written once // here and handed what each template knows about itself. Light comes from // the top left: dark falls down-right, light up-left. No edge is a drawn // line: an edge is light or shade on a shoulder, and the shadows either side // of it. Distances are points, and a face's shadows are drawn OUTSIDE its // layout box, in a quad grown in `vertex`, so they cost no padding. // The page, and the grain every surface shares, so a face and the ground // round it read as one material: one value per device pixel, about one // level of 255, taken in window space. let nm_grain = fn(q: vec2, px: float) -> float { return Finish.grain(q / px, 2.4 / 255.0) } let nm_ground = fn() -> vec3 { return vec3(0.914, 0.929, 0.941) } // The slate every shadow on this material is cast in: the page's cool white // taken down to about two thirds of its light, toward blue. let nm_slate_a = fn() -> vec3 { return vec3(0.541, 0.596, 0.690) } // What a raised part throws round itself, premultiplied: a slate shadow down // and right, deepest at the foot of the edge (about 0.7 of the page there), // a tight contact shadow under the edge, and up and left a halo of light. // The halo is light, not paint: it lifts what it falls on, the page to white, // and covers nothing, so an accent fill beside a knob keeps its colour. `s` // is the part's own height and `k` their strength. The shadows keep the // part's proportions, gone about three tenths of its height out, and stop // growing at a 40 point part: a quad grown by three tenths of its part's // height, or by twelve points, holds the whole cast. let nm_cast = fn(p: vec2, c: vec2, h: vec2, r: float, s: float, k: float) -> vec4 { let t = min(s, 40.0) let amb = Finish.drop(p, c, h, r, vec2(0.09, 0.11) * t, max(0.1 * t, 0.9)) * 0.9 * k let con = Finish.drop(p, c, h, r, vec2(0.012, 0.03) * t, max(0.025 * t, 0.5)) * 0.28 * k // Held at white, so a hovered part cannot push the page past it. let lit = min(Finish.drop(p, c, h, r, vec2(-0.08, -0.1) * t, max(0.09 * t, 0.8)) * k, 1.0) var acc = vec4(nm_slate_a() * amb, amb) acc = Finish.over(acc, vec4(vec3(0.478, 0.525, 0.608) * con, con)) return acc + vec4((vec3(1.0, 1.0, 1.0) - nm_ground()) * lit, 0.0) } // The light on a face's shoulder, signed: up to 1 where the edge faces the // top left, down to -1 where it faces the bottom right, fading from the edge // to nothing `w` points in. The edge is taken to turn as if the face's // corners were rounded by `m`: exact on a disc or a pill given its half // height, and round the tighter corners of a box the light turns a little // more gently than the outline does. let nm_shoulder_a = fn(p: vec2, c: vec2, h: vec2, m: float, d: float, w: float) -> float { let e = max(abs(p - c) - h + vec2(m, m), vec2(0.0, 0.0)) let n = sign(p - c) * e / max(length(e), 0.0001) return dot(n, vec2(-0.6, -0.8)) * (1.0 - smoothstep(0.0, w, -d)) } // A raised face: the page a little lighter toward the top left and a little // darker toward the bottom right, corner to corner, and on its shoulder, the // outer two and a half points (never under three device pixels), the light // the edge catches where it faces the top left and the shade where it faces // away, both fading in from the edge. No line is drawn on it. `rim` is how // much light and shade the shoulder takes. let nm_face = fn(p: vec2, c: vec2, h: vec2, d: float, px: float, lift: float, rim: float) -> vec3 { let u = clamp(((p.x - c.x) / max(h.x, 1.0) + (p.y - c.y) / max(h.y, 1.0)) * 0.25 + 0.5, 0.0, 1.0) let g = mix(0.028, -0.024, u) + lift var col = nm_ground() + vec3(g, g, g) let sh = nm_shoulder_a(p, c, h, min(h.x, h.y), d, max(2.5, 3.0 * px)) * rim col = mix(col, vec3(0.984, 0.988, 1.0), max(sh, 0.0) * 0.85) col = mix(col, vec3(0.745, 0.776, 0.831), max(-sh, 0.0) * 0.55) return col } // A well: the surround's shadow falls in deep from the top left and light // collects along the bottom right, both soft, deep enough to read as a hole, // and no line is drawn on the rim. `k` is its depth (a well asked for more // than full depth holds at the slate and at white), `s` the well's own // height. let nm_well = fn(col: vec3, p: vec2, c: vec2, h: vec2, r: float, d: float, px: float, s: float, k: float) -> vec3 { let dk = Finish.inset(p, c, h, r, vec2(0.08 * s, 0.12 * s), max(0.13 * s, 1.0)) let lt = Finish.inset(p, c, h, r, vec2(-0.05 * s, -0.07 * s), max(0.1 * s, 0.8)) let o = mix(col, nm_slate_a(), clamp(dk * 1.0 * k, 0.0, 1.0)) return mix(o, vec3(1.0, 1.0, 1.0), clamp(lt * 0.9 * k, 0.0, 1.0)) } // The focus mark: a one-pixel accent ring two points outside the face, and a // short soft fall-off either side of it. let nm_focus = fn(d: float, px: float, f: float) -> vec4 { let ring = Finish.ring_out(d, 2.0, 2.0 + px, px) * 0.6 let soft = (1.0 - smoothstep(0.0, 2.0, abs(d - 2.0))) * 0.15 * step(0.0, d) let a = max(ring, soft) * f return vec4(vec3(0.290, 0.486, 0.965) * a, a) } // The distance to an arc of radius R about c, running clockwise from a0 to // a1 measured from straight down, with round ends, w wide. Written here // because the stock arc gives up on any sweep past half a turn. let nm_arc = fn(p: vec2, c: vec2, R: float, a0: float, a1: float, w: float) -> float { let q = p - c var ap = atan2(-q.x, q.y) if ap < 0.0 { ap = ap + 2.0 * PI } let rel = ap - a0 - floor((ap - a0) / (2.0 * PI)) * 2.0 * PI let on = step(rel, a1 - a0) let band = abs(length(q) - R) - 0.5 * w let e0 = length(q - vec2(-sin(a0), cos(a0)) * R) - 0.5 * w let e1 = length(q - vec2(-sin(a1), cos(a1)) * R) - 0.5 * w return mix(min(e0, e1), band, on) } // BUTTONS. No line is drawn round a face: every edge is light on a // shoulder, and a raised face throws its two shadows into a quad grown by // 8 points. The last quarter of that room fades whatever reaches it, along // a box rounded like the face, so a cast never ends on the quad's edge. let nm_room_b = fn(pq: vec2, size: vec2, r: float, m: float) -> float { let qc = size * 0.5 let edge = -Material.sd_box(pq, qc, qc + vec2(m, m), r + m) return smoothstep(0.0, 0.25 * m, edge) } // A button face. `rest` is how raised it stands at rest (1 raised, 0 flat // on the page), `ghost` how far a flat face rises under the pointer (0 // halfway, 1 the whole way), `hollow` shows nothing until the pointer asks, // and `fill` is the colour the face is filled with, its alpha how far: the // face keeps its gradient, its shoulders and its shadows, and only its // colour moves. Held, any of them is the same shape pressed into a well. let nm_button = fn(pq: vec2, size: vec2, org: vec2, px: float, hover: float, down: float, focus: float, disabled: float, loading: float, anim: float, ink: vec4, rest: float, ghost: float, hollow: float, fill: vec4) -> vec4 { let lo = vec2(Finish.snap(org.x, px), Finish.snap(org.y, px)) - org let hi = vec2(Finish.snap(org.x + size.x, px), Finish.snap(org.y + size.y, px)) - org let c = (lo + hi) * 0.5 let h = (hi - lo) * 0.5 let hh = min(size.y, 28.0) // Round and generous: a 10 point corner on a row of 26, growing with a // taller face to 16, and a full pill on a face too short for 10. let r = min(clamp(0.36 * size.y, 10.0, 16.0), min(h.x, h.y)) let d = Material.sd_box(pq, c, h, r) let en = 1.0 - disabled let press = down * en // How far the face stands up: raised faces lift a little under the // hand, flat ones rise halfway or the whole way, hollow ones stay down. let up = mix(mix(mix(0.6, 1.0, ghost) * hover, rest * (1.0 + 0.3 * hover), step(0.5, rest)), 0.0, hollow) * (1.0 - press) let k = up * mix(1.0, 0.35, disabled) var acc = nm_cast(pq, c, h, r, hh, k) * nm_room_b(pq, size, r, 8.0) let a = Finish.cover(d, px) // The shoulders stand up with the face and fade with it when disabled. var col = nm_face(pq, c, h, d, px, 0.018 * hover * en, min(up, 1.0) * mix(1.0, 0.35, disabled)) // A flat face is the page itself until it rises. let flatness = (1.0 - step(0.5, rest)) * (1.0 - min(up * 1.8, 1.0)) col = mix(col, nm_ground(), flatness) // Pressed, or the faint well a hollow face shows under the pointer. let sink = max(press, hollow * hover * 0.4 * en) col = mix(col, nm_well(col - vec3(0.013, 0.013, 0.013) * press, pq, c, h, r, d, px, hh, 1.0), sink) // The fill moves the whole face from the page's colour to its own, so // the light on it, its shoulders and its well move along; disabled, it // is the page's face again. col = clamp(col + (fill.rgb - nm_ground()) * fill.a * en, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0)) col = col + vec3(nm_grain(org + pq, px)) acc = Finish.over(acc, vec4(col * a, a)) acc = Finish.over(acc, nm_focus(d, px, focus * en)) // The wait spinner is drawn by the face, so a face that replaces the // stock one has to draw it again; on a filled face it is white, as the // label it stands in for. let rr = max(min(h.x, h.y) - 5.0, 2.0) let a0 = anim * 2.0 * PI let sa = Finish.cover(nm_arc(pq, c, rr, a0, a0 + 1.5 * PI, 1.5), px) * ink.a * loading let si = mix(ink.rgb, vec3(1.0, 1.0, 1.0), fill.a * en) return Finish.over(acc, vec4(si * sa, sa)) } // The room round every button for its shadows and its focus ring. mod.widgets.ButtonFlat.draw_bg.vertex = fn() { let m = 8.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.ButtonFlat.clip_x = false mod.widgets.ButtonFlat.clip_y = false // A view lets what it holds draw past its edge; scroll views still clip. mod.widgets.View.clip_x = false mod.widgets.View.clip_y = false // The flat button: the page until the pointer raises it. mod.widgets.ButtonFlat.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 0.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } // The standard, sized and compound buttons stand up off the page. mod.widgets.Button.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonXs.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonSm.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonLg.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonXl.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonCompound.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } // The primary is the one filled face: the accent all over, raised and lit // as the page's faces are, with a white label. The rest of its family keep // the page's face and speak through their labels alone, the destructive one // in red. mod.widgets.ButtonPrimary.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.220, 0.376, 0.839, 1.0)), m) } mod.widgets.ButtonSecondary.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonTertiary.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } mod.widgets.ButtonDanger.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } // The outlined button is the page with an accent label and nothing drawn // round it; under the pointer it rises the whole way, as far as a standard // button stands. mod.widgets.ButtonOutline.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 0.0, 1.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } // The borderless button shows nothing until the pointer finds it, and then // only a faint hollow. mod.widgets.ButtonFlatter.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return self.host_over(nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 0.0, 0.0, 1.0, vec4(0.0, 0.0, 0.0, 0.0)), m) } // Rows of 26: 5 above and below a 16 point line, 14 either side. mod.widgets.ButtonFlat.padding = mod.turtle.Inset{left: 14.0 right: 14.0 top: 5.0 bottom: 5.0} // NOTHING PAINTS OVER A NEIGHBOUR, whatever row an app puts it in. The // casts stay long and soft; instead every raised part keeps room for them // as its own margin, so two parts set edge to edge, or rows wrapped with no // gap between them, still stand 8 points apart: the shadow of one and the // halo of the other may meet in that gap, and each has faded out before the // other's face. The room is paid in layout: a 26 point row of buttons takes // 34 points, and buttons on a page's 8 point spacing stand 16 apart. mod.widgets.ButtonFlat.margin = mod.turtle.Inset{left: 4.0 right: 4.0 top: 4.0 bottom: 4.0} mod.widgets.ButtonFlat.spacing = 6.0 mod.widgets.ButtonFlat.icon_walk.width = 14.0 mod.widgets.ButtonIcon.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 5.0 bottom: 5.0} mod.widgets.ButtonFlatIcon.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 5.0 bottom: 5.0} mod.widgets.ButtonFlatterIcon.padding = mod.turtle.Inset{left: 6.0 right: 6.0 top: 5.0 bottom: 5.0} mod.widgets.ButtonFlat.draw_bg.loading_color = #4a7cf6 // Label inks. White at a medium weight on the primary's accent face; on the // page's faces the slate on the secondary, the accent on the tertiary and // the outlined, and the error ink on the destructive one, its only red. // The weight re-weights the theme family's first member and keeps its // fallbacks. let nm_medium = mod.theme.font_regular{font_family +: {ibm_plex_text := FontMember{res: crate_resource("makepad_widgets:resources/Inter.ttf") weight: 540.0 asc: 0.0 desc: 0.0}}} mod.widgets.ButtonPrimary.draw_text.color = #ffffff mod.widgets.ButtonPrimary.draw_text.color_hover = #ffffff mod.widgets.ButtonPrimary.draw_text.color_down = #ffffff mod.widgets.ButtonPrimary.draw_text.color_focus = #ffffff mod.widgets.ButtonPrimary.draw_text.color_disabled = #b3bac6 mod.widgets.ButtonPrimary.draw_text.text_style = nm_medium mod.widgets.ButtonPrimary.draw_icon.color = #ffffff mod.widgets.ButtonSecondary.draw_text.color = #535a6a mod.widgets.ButtonSecondary.draw_text.color_hover = #474e5e mod.widgets.ButtonSecondary.draw_text.color_down = #474e5e mod.widgets.ButtonSecondary.draw_text.color_focus = #535a6a mod.widgets.ButtonSecondary.draw_icon.color = #535a6a mod.widgets.ButtonTertiary.draw_text.color = #3860d6 mod.widgets.ButtonTertiary.draw_text.color_hover = #2f55c8 mod.widgets.ButtonTertiary.draw_text.color_down = #2f55c8 mod.widgets.ButtonTertiary.draw_text.color_focus = #3860d6 mod.widgets.ButtonTertiary.draw_text.text_style = mod.theme.font_regular mod.widgets.ButtonTertiary.draw_icon.color = #3860d6 mod.widgets.ButtonDanger.draw_text.color = #bd3a31 mod.widgets.ButtonDanger.draw_text.color_hover = #a8322a mod.widgets.ButtonDanger.draw_text.color_down = #a8322a mod.widgets.ButtonDanger.draw_text.color_focus = #bd3a31 mod.widgets.ButtonDanger.draw_icon.color = #bd3a31 mod.widgets.ButtonOutline.draw_text.color = #3860d6 mod.widgets.ButtonOutline.draw_text.color_hover = #2f55c8 mod.widgets.ButtonOutline.draw_text.color_down = #2f55c8 mod.widgets.ButtonOutline.draw_text.color_focus = #3860d6 mod.widgets.ButtonOutline.draw_icon.color = #3860d6 // CHECK BOX. A 16 point well with a 5 point corner, pressed deep into the // page, and no line round it: focused, the well is pressed a touch deeper, // and under the error intent its floor blushes with the error ink. On, the // check lies in it as a flat accent fill two points wide with round ends; // mixed, a dash of the same. // The distance to the check in a box of side `box` about `c`: a short // stroke down from the left and a long one up to the right, `w` wide. let nm_tick_c = fn(p: vec2, c: vec2, box: float, w: float) -> float { let a = c + vec2(-0.24 * box, 0.01 * box) let b = c + vec2(-0.06 * box, 0.19 * box) let e = c + vec2(0.25 * box, -0.17 * box) let pa = p - a let ba = b - a let pb = p - b let eb = e - b let t0 = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0) let t1 = clamp(dot(pb, eb) / dot(eb, eb), 0.0, 1.0) return min(length(pa - ba * t0), length(pb - eb * t1)) - 0.5 * w } // An accent fill lit as the faces are, a shade lighter toward the top left // and darker toward the bottom right; `q` is the point from the fill's // middle over its half size. let nm_lit_c = fn(ink: vec3, q: vec2) -> vec3 { return min(ink * (1.0 + 0.08 * clamp(-0.6 * q.x - 0.8 * q.y, -1.0, 1.0)), vec3(1.0, 1.0, 1.0)) } let nm_check = fn(pq: vec2, size: vec2, org: vec2, px: float, box: float, hover: float, down: float, focus: float, active: float, disabled: float, mixed: float, error: float, round: float, at_end: float) -> vec4 { let y0 = Finish.snap(org.y + size.y * 0.5 - box * 0.5, px) - org.y let x0 = Finish.snap(org.x + (size.x - box) * at_end, px) - org.x let c = vec2(x0 + box * 0.5, y0 + box * 0.5) let h = vec2(box * 0.5, box * 0.5) let r = mix(5.0, box * 0.5, round) let d = Material.sd_box(pq, c, h, r) let en = 1.0 - disabled // The well: hovered, held or focused, it is pressed a touch deeper. let base = mix(vec3(0.918, 0.929, 0.957), vec3(0.741, 0.227, 0.192), 0.14 * error) let k = min(1.25 + 0.12 * hover + 0.1 * down + 0.25 * focus * en, 1.5) * mix(1.0, 0.35, disabled) var col = nm_well(base, pq, c, h, r, d, px, 18.0, k) col = col + vec3(nm_grain(org + pq, px)) let a = Finish.cover(d, px) // The mark: the check, or the dash while mixed. let ink = mix(mix(vec3(0.290, 0.486, 0.965), vec3(0.741, 0.227, 0.192), error), vec3(0.725, 0.776, 0.902), disabled) let tick = Finish.cover(nm_tick_c(pq, c, box, 2.0), px) * active * (1.0 - mixed) let dash = Finish.cover(Material.sd_box(pq, c, vec2(0.22 * box, 1.0), 1.0), px) * mixed let mk = max(tick, dash) * a return Finish.over(vec4(col * a, a), vec4(nm_lit_c(ink, (pq - c) / (0.5 * box)) * mk, mk)) } mod.widgets.CheckBoxFlat.draw_bg.vertex = fn() { let m = 4.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.CheckBoxFlat.clip_x = false mod.widgets.CheckBoxFlat.clip_y = false mod.widgets.CheckBoxFlat.draw_bg.pixel = fn() { let m = 4.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_check(pq, self.rect_size, self.rect_pos, px, 16.0, self.hover, self.down, self.focus, self.active, self.disabled, self.mixed, self.error, 0.0, self.mark_at_end) } mod.widgets.CheckBoxCircle.draw_bg.pixel = fn() { let m = 4.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_check(pq, self.rect_size, self.rect_pos, px, 16.0, self.hover, self.down, self.focus, self.active, self.disabled, self.mixed, self.error, 1.0, self.mark_at_end) } // The size stays a uniform, as the library declares it. mod.widgets.CheckBoxFlat.draw_bg.size = uniform(16.0) mod.widgets.CheckBoxFlat.padding = mod.turtle.Inset{left: 0.0 right: 4.0 top: 5.0 bottom: 5.0} mod.widgets.CheckBoxFlat.label_walk.margin.left = 24.0 // TOGGLE. A 34 by 18 point pill well with a 14 point knob lying in it, two // points clear of the wall all round, and no line anywhere: focused, the // well is pressed a touch deeper. The knob is a low dome lit from the top // left, and it throws its own two shadows on the floor, the dark one down // and right and the white one up and left, both cut off by the wall. On, // the well fills with the accent and the knob turns white; the accent is // darkened and lit wherever the pale floor would be, so the well's relief // and the knob's shadows carry over in the accent's own hue. // How a knob's low dome turns to the light, 0 at its middle: its upper left // catches a little more, its lower right falls away. `q` is the point from // the knob's centre, `r` its radius. let nm_dome_c = fn(q: vec2, r: float) -> float { let n = normalize(vec3(0.55 * q.x / r, 0.55 * q.y / r, 1.0)) return dot(n, vec3(-0.45, -0.6, 0.66)) - 0.66 } let nm_toggle = fn(pq: vec2, size: vec2, org: vec2, px: float, hover: float, down: float, focus: float, active: float, disabled: float, error: float, t: float, at_end: float) -> vec4 { let w = 34.0 let hh = 9.0 let kr = 7.0 let x0 = (size.x - w) * at_end let cy = Finish.snap(org.y + size.y * 0.5, px) - org.y let tc = vec2(x0 + w * 0.5, cy) let th = vec2(w * 0.5, hh) let td = Material.sd_box(pq, tc, th, hh) let en = 1.0 - disabled let dis = mix(1.0, 0.35, disabled) // The knob's place, centred in the well's height at both ends of its // travel, and what it throws. let kc = vec2(x0 + hh + (w - 2.0 * hh) * t, cy) let kh = vec2(kr, kr) let kd = length(pq - kc) - kr let sh = nm_cast(pq, kc, kh, kr, 2.0 * kr, (1.0 + 0.3 * hover * en) * (1.0 - 0.4 * down * en) * dis) // The floor: the well with the knob's shadows on it, and on, the accent // shaded as that floor is. let base = vec3(0.918, 0.933, 0.945) let pale = nm_well(base, pq, tc, th, hh, td, px, 18.0, (1.15 + 0.12 * hover + 0.25 * focus * en) * dis) let under = sh.rgb + pale * (1.0 - sh.a) let fillc = mix(mix(vec3(0.290, 0.486, 0.965), vec3(0.741, 0.227, 0.192), error), vec3(0.725, 0.776, 0.902), disabled) let fc = min(fillc * (Finish.lum(under) / Finish.lum(base)), vec3(1.0, 1.0, 1.0)) var col = mix(under, fc, active) col = col + vec3(nm_grain(org + pq, px)) let ta = Finish.cover(td, px) let acc = vec4(col * ta, ta) // The knob's face: the low dome, a step whiter when on. var kcol = nm_face(pq, kc, kh, kd, px, 0.018 * hover * en - 0.012 * down * en + 0.065 * active, en) kcol = kcol + vec3(0.05 * nm_dome_c(pq - kc, kr) * dis) kcol = min(kcol + vec3(nm_grain(org + pq, px)), vec3(1.0, 1.0, 1.0)) let ka = Finish.cover(kd, px) return Finish.over(acc, vec4(kcol * ka, ka)) } mod.widgets.ToggleFlat.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.ToggleFlat.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let t = mix(self.active, self.drag_pos, self.drag) return nm_toggle(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.active, self.disabled, self.error, t, self.mark_at_end) } // Size and aspect stay uniforms: the widget reads them back, with the // knob's inset, to carry an icon on the knob and to measure a drag. The // knob stands centred in the well's height at both ends, two points in from // the wall, so it travels the well's length less its height. mod.widgets.ToggleFlat.draw_bg.size = uniform(18.0) mod.widgets.ToggleFlat.draw_bg.pill_aspect = uniform(1.889) mod.widgets.ToggleFlat.draw_bg.knob_centred = uniform(1.0) mod.widgets.ToggleFlat.draw_bg.knob_inset = uniform(2.0) mod.widgets.ToggleFlat.label_walk.margin.left = 42.0 // RADIO. A 16 point round well pressed deep into the page, and no line // round it: focused, the well is pressed a touch deeper. On, an accent dot // half its width lies in the middle as a flat fill, lit as the check is. let nm_radio = fn(pq: vec2, size: vec2, org: vec2, px: float, hover: float, down: float, focus: float, active: float, disabled: float) -> vec4 { let rr = 8.0 let cy = Finish.snap(org.y + size.y * 0.5, px) - org.y let c = vec2(rr, cy) let h = vec2(rr, rr) let d = length(pq - c) - rr let en = 1.0 - disabled let k = min(1.25 + 0.12 * hover + 0.1 * down + 0.25 * focus * en, 1.5) * mix(1.0, 0.35, disabled) var col = nm_well(vec3(0.918, 0.929, 0.957), pq, c, h, rr, d, px, 18.0, k) // The dot. let dr = 0.49 * rr let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), disabled) col = mix(col, nm_lit_c(ink, (pq - c) / dr), Finish.cover(length(pq - c) - dr, px) * active) col = col + vec3(nm_grain(org + pq, px)) let a = Finish.cover(d, px) return vec4(col * a, a) } mod.widgets.RadioButtonFlat.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.RadioButtonFlat.clip_x = false mod.widgets.RadioButtonFlat.clip_y = false mod.widgets.RadioButtonFlat.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_radio(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.active, self.disabled) } mod.widgets.RadioButtonFlatter.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_radio(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.active, self.disabled) } // The size stays a uniform, as the library declares it. mod.widgets.RadioButtonFlat.draw_bg.size = uniform(16.0) mod.widgets.RadioButtonFlat.padding = mod.turtle.Inset{left: 0.0 right: 4.0 top: 5.0 bottom: 5.0} mod.widgets.RadioButtonFlat.label_walk.margin.left = 24.0 // A distance to a segment, for the few strokes the faces draw by hand. let nm_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) } // A 7 by 4 point chevron, 1.5 point round-capped stroke, pointing down. let nm_chevron = fn(p: vec2, cx: float, cy: float, px: float) -> float { let d = min(nm_seg(p, vec2(cx - 3.5, cy - 2.0), vec2(cx, cy + 2.0)), nm_seg(p, vec2(cx, cy + 2.0), vec2(cx + 3.5, cy - 2.0))) return Finish.cover(d - 0.75, px) } // TEXT FIELDS. A well pressed into the page with a 6 point corner and no // line round it: the surround's shadow falls in from the top left and light // collects at the bottom right. The floor is the page's own material one // step down, never a paler card laid in the hole, and one step only, so body // ink still reads 4.5:1 on it. The pointer deepens the well a little and // focus a touch more; focus draws no ring, the caret the stock draws in the // accent says which field has the keys. let nm_field = fn(pq: vec2, size: vec2, org: vec2, px: float, r: float, hover: float, focus: float, disabled: float) -> vec4 { let lo = vec2(Finish.snap(org.x, px), Finish.snap(org.y, px)) - org let hi = vec2(Finish.snap(org.x + size.x, px), Finish.snap(org.y + size.y, px)) - org let c = (lo + hi) * 0.5 let h = (hi - lo) * 0.5 let rr = min(r, min(h.x, h.y)) let d = Material.sd_box(pq, c, h, rr) let en = 1.0 - disabled // Deeper is a shadow reaching further in and falling a little harder on // a floor a little darker. The shadows keep a one-line field's // proportions however tall the box grows. let deep = max(focus, 0.35 * hover) * en let s = min(2.0 * h.y, 26.0) * (1.0 + 0.12 * deep) let sink = 0.010 + 0.004 * deep - 0.004 * disabled var col = nm_ground() - vec3(sink, sink, sink) col = nm_well(col, pq, c, h, rr, d, px, s, (1.0 + 0.1 * deep) * mix(1.0, 0.35, disabled)) col = col + vec3(nm_grain(org + pq, px)) // All of it inside the field's own box: a field clips what it draws, // and a long line of text must not spill past it. let a = Finish.cover(d, px) return vec4(col * a, a) } // The inputs a slider, a combo box, a number or a well carries inside // themselves draw nothing: the control round them is the box. mod.widgets.TextInputFlat.draw_bg.pixel = fn() { if self.color.w < 0.01 || self.border_radius < 0.5 { return vec4(0.0, 0.0, 0.0, 0.0) } let pq = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_field(pq, self.rect_size, self.rect_pos, px, self.border_radius, self.hover, self.focus, self.disabled) } mod.widgets.TextInputFlat.padding.left = 10.0 mod.widgets.TextInputFlat.padding.right = 10.0 // The box a field's input sits in, and the box of chosen tags, are the same // well. A refused tag warms the tag field's floor instead of ringing it. mod.widgets.FieldWell.draw_bg.pixel = fn() { if self.color.w < 0.01 { return vec4(0.0, 0.0, 0.0, 0.0) } let pq = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_field(pq, self.rect_size, self.rect_pos, px, self.border_radius, self.hover, self.focus, self.disabled) } mod.widgets.TagField.draw_bg.pixel = fn() { if self.color.w < 0.01 { return vec4(0.0, 0.0, 0.0, 0.0) } let pq = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let acc = nm_field(pq, self.rect_size, self.rect_pos, px, self.border_radius, self.hover, self.focus, self.disabled) let hs = self.rect_size * 0.5 let d = Material.sd_box(pq, hs, hs, min(self.border_radius, min(hs.x, hs.y))) let warm = Finish.cover(d + 1.0, px) * self.alarm * (1.0 - self.disabled) * 0.14 return Finish.over(acc, vec4(self.border_color_alarm.rgb * warm, warm)) } // The number field: the same well, its stepper standing in the right end // with nothing cut round it. The two chevrons lie on the well's floor; the // half under the pointer rises as a small round key, and the half held is // pressed into the floor. mod.widgets.NumberField.draw_bg.pixel = fn() { let pq = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) return nm_field(pq, self.rect_size, self.rect_pos, px, 6.0, self.hover, self.focus, self.disabled) } mod.widgets.NumberField.pad_left = 10.0 // One key of the stepper, a disc of radius r about c. Risen, it is a face // with its two shadows; held, it is a dimple in the floor. let nm_key_d = fn(p: vec2, org: vec2, c: vec2, r: float, px: float, rise: float, press: float) -> vec4 { let h = vec2(r, r) let d = length(p - c) - r let acc = nm_cast(p, c, h, r, 2.0 * r, rise * (1.0 - press)) var col = nm_face(p, c, h, d, px, 0.006, 1.0) col = mix(col, nm_well(nm_ground() - vec3(0.02, 0.02, 0.02), p, c, h, r, d, px, 3.0 * r, 1.0), press) col = col + vec3(nm_grain(org + p, px)) let a = Finish.cover(d, px) * max(rise, press) return Finish.over(acc, vec4(col * a, a)) } // The stepper's chevrons: the drop-down's, drawn five points wide so a key // keeps a margin round its own, and the upper one turned to point up. let nm_spin_vee_d = fn(p: vec2, c: vec2, up: float, px: float) -> float { let q = (p - c) * 1.4 return nm_chevron(c + vec2(q.x, mix(q.y, -q.y, up)), c.x, c.y, px * 1.4) } mod.widgets.NumberSpin.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let w = self.rect_size.x let h = self.rect_size.y let mid = Finish.snap(self.rect_pos.y + h * 0.5, px) - self.rect_pos.y let live = 1.0 - self.disabled // The pointer and the press each name a half: 1 the upper, -1 the lower. let hu = max(0.0, self.hot) * live let hl = max(0.0, -self.hot) * live let du = max(0.0, self.down) * live let dl = max(0.0, -self.down) * live // A key leaves free round itself the three tenths of its size its cast // reaches, so the shadows fade out inside the stepper's own box and the // well's edge never cuts them. let kr = clamp(min(0.3 * w, 0.15 * h), 3.0, 6.0) let cu = vec2(0.5 * w, 0.5 * mid) let cl = vec2(0.5 * w, 0.5 * (mid + h)) var acc = nm_key_d(p, self.rect_pos, cu, kr, px, max(hu, du), du) acc = Finish.over(acc, nm_key_d(p, self.rect_pos, cl, kr, px, max(hl, dl), dl)) // Slate chevrons, a step darker on the half the hand is on. let dim = mix(1.0, 0.4, self.disabled) let au = nm_spin_vee_d(p, cu, 1.0, px) * dim let al = nm_spin_vee_d(p, cl, 0.0, px) * dim let iu = mix(vec3(0.388, 0.416, 0.478), vec3(0.310, 0.337, 0.400), max(hu, du)) let il = mix(vec3(0.388, 0.416, 0.478), vec3(0.310, 0.337, 0.400), max(hl, dl)) acc = Finish.over(acc, vec4(iu * au, au)) return Finish.over(acc, vec4(il * al, al)) } // The printed arrow glyphs are hidden; the face draws its own chevrons. mod.widgets.NumberSpin.draw_text.color = #636a7a00 mod.widgets.NumberSpin.draw_text.color_disabled = #636a7a00 // The drag-number: the field's well. A track field fills it back up toward // the page as far as its value, a wash of light over the floor rather than // an accent, under slate digits. mod.widgets.ValueInput.draw_bg.pixel = fn() { let pq = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let acc = nm_field(pq, self.rect_size, self.rect_pos, px, 5.0, max(self.hover, self.drag), self.focus, 0.0) let d = Material.sd_box(pq, self.rect_size * 0.5, self.rect_size * 0.5, 5.0) let xe = 1.0 + (self.rect_size.x - 2.0) * max(self.fill, 0.0) let wash = step(0.0, self.fill) * Finish.cover(pq.x - xe, px) * Finish.cover(d + 1.0, px) * 0.3 return Finish.over(acc, vec4(wash, wash, wash, wash)) } mod.widgets.ValueInput.draw_text.color = #4f5666 mod.widgets.ValueInput.arrow_color = #7f8796 mod.widgets.ValueInput.text_input.draw_bg.color = #00000000 // DROP-DOWN. A button: the buttons' own raised face, shadows and press, and // a chevron at the right end; open, it is pressed in. // The chevron every choice wears, its tip ten points in from the right end: // slate, a step darker under the pointer, faded when nothing answers. let nm_choice_vee_d = fn(pq: vec2, size: vec2, px: float, hover: float, disabled: float) -> vec4 { let a = nm_chevron(pq, size.x - 13.5, size.y * 0.5 + 0.5, px) * mix(1.0, 0.4, disabled) let ink = mix(vec3(0.388, 0.416, 0.478), vec3(0.325, 0.353, 0.416), hover * (1.0 - disabled)) return vec4(ink * a, a) } mod.widgets.DropDownFlat.draw_bg.vertex = fn() { let m = 8.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.DropDownFlat.clip_x = false mod.widgets.DropDownFlat.clip_y = false mod.widgets.DropDownFlat.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let face = nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, max(self.down, self.active), self.focus, self.disabled, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0), 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)) return Finish.over(face, nm_choice_vee_d(pq, self.rect_size, px, self.hover, self.disabled)) } mod.widgets.DropDownFlat.padding.left = 12.0 mod.widgets.DropDownFlat.padding.right = 28.0 // The room its cast reaches, as on a button. mod.widgets.DropDownFlat.margin = mod.turtle.Inset{left: 4.0 right: 4.0 top: 4.0 bottom: 4.0} // The select wears a button of its own, built before this sheet: it is // given the drop-down's face, chevron and all. // Being built first, the select's face does not inherit the button's grown // quad either, so it is given the same one here: without it the face is // squeezed eight points in and its shadows cut off. mod.widgets.Select.face.draw_bg.vertex = fn() { let m = 8.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.Select.face.clip_x = false mod.widgets.Select.face.clip_y = false mod.widgets.MultiSelect.face.draw_bg.vertex = fn() { let m = 8.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.MultiSelect.face.clip_x = false mod.widgets.MultiSelect.face.clip_y = false // The select round the face is a view of its own, and clips unless told. mod.widgets.Select.clip_x = false mod.widgets.Select.clip_y = false mod.widgets.MultiSelect.clip_x = false mod.widgets.MultiSelect.clip_y = false mod.widgets.Select.face.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let face = nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)) return Finish.over(face, nm_choice_vee_d(pq, self.rect_size, px, self.hover, self.disabled)) } mod.widgets.Select.face.padding.right = 28.0 mod.widgets.MultiSelect.face.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let face = nm_button(pq, self.rect_size, self.rect_pos, px, self.hover, self.down, self.focus, self.disabled, self.loading, self.anim_time, self.loading_color, 1.0, 0.0, 0.0, vec4(0.0, 0.0, 0.0, 0.0)) return Finish.over(face, nm_choice_vee_d(pq, self.rect_size, px, self.hover, self.disabled)) } mod.widgets.MultiSelect.face.padding.right = 28.0 // The face is a button, so it already keeps the button's four points of room // for its cast; the select round it adds none of its own. // COMBO BOX. A field one can type in, so a well, with the same chevron. mod.widgets.ComboBoxFlat.draw_bg.pixel = fn() { let pq = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let face = nm_field(pq, self.rect_size, self.rect_pos, px, 6.0, self.hover, self.focus, self.disabled) return Finish.over(face, nm_choice_vee_d(pq, self.rect_size, px, self.hover, self.disabled)) } mod.widgets.ComboBoxFlat.padding.left = 10.0 mod.widgets.ComboBoxFlat.padding.right = 28.0 mod.widgets.ComboBoxFlat.input.min_height = 0.0 // SLIDERS. The track is a pill well; the travelled part of it is filled // flat with the accent and laid over the thumb's shadows, so the white one // never pales it; the thumb is a round raised disc throwing its two // shadows. Every slider face draws its thumb and its shadows past its own // box, so the family's root grows the quad and stops clipping. mod.widgets.SliderMinimal.draw_bg.vertex = fn() { let m = 8.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.SliderMinimal.clip_x = false mod.widgets.SliderMinimal.clip_y = false // A raised disc of radius `r` at `hc`, laid over `acc`: the two shadows of // a part `s` points tall, dark down and to the right and white up and to // the left, kept to where `clip` lets them fall; then `lay`, what lies round // the disc clean of those shadows (a slider's fill, a knob's value arc); // then the face, a shade lighter than the page, and the focus mark. let nm_disc_e = fn(acc: vec4, lay: vec4, pq: vec2, org: vec2, hc: vec2, r: float, s: float, clip: float, px: float, hover: float, drag: float, focus: float, disabled: float) -> vec4 { let en = 1.0 - disabled let dis = mix(1.0, 0.35, disabled) let kh = vec2(r, r) let hd = length(pq - hc) - r var o = Finish.over(acc, nm_cast(pq, hc, kh, r, s, (1.0 + 0.3 * max(hover, drag)) * dis) * clip) o = Finish.over(o, lay) var col = nm_face(pq, hc, kh, hd, px, 0.016 + 0.012 * hover * en - 0.012 * drag * en, en) col = col + vec3(nm_grain(org + pq, px)) let ha = Finish.cover(hd, px) o = Finish.over(o, vec4(col * ha, ha)) return Finish.over(o, nm_focus(hd, px, focus * en)) } // A thumb: that disc standing on the page with nothing laid round it, its // shadows those of a part a quarter taller than it is wide, which keeps // them inside the eight points a slider's quad grows by. let nm_thumb = fn(acc: vec4, pq: vec2, org: vec2, hc: vec2, r: float, px: float, hover: float, drag: float, focus: float, disabled: float) -> vec4 { return nm_disc_e(acc, vec4(0.0, 0.0, 0.0, 0.0), pq, org, hc, r, 2.5 * r, 1.0, px, hover, drag, focus, disabled) } // The groove a thumb runs in: a pill well, cool slate, its shadow in // proportion to its height, so a thin groove reads as deep as a thick one. let nm_groove = fn(col: vec3, pq: vec2, c: vec2, h: vec2, px: float, k: float) -> vec3 { let r = min(h.x, h.y) let d = Material.sd_box(pq, c, h, r) return nm_well(col, pq, c, h, r, d, px, max(4.0 * r, 10.0), k) } // The accent laid flat on a well's floor, where the well's shadow still // falls on it: a little darker along its upper edge and a little lighter // along its lower one, so the fill lies in the hole rather than on it. `c`, // `h` and `r` are the well's, `s` how far its shade reaches, `k` its depth. let nm_fill_e = fn(ink: vec3, pq: vec2, c: vec2, h: vec2, r: float, s: float, k: float) -> vec3 { let dk = Finish.inset(pq, c, h, r, vec2(0.05 * s, 0.12 * s), max(0.12 * s, 0.6)) let lt = Finish.inset(pq, c, h, r, vec2(-0.03 * s, -0.08 * s), max(0.1 * s, 0.5)) let o = mix(ink, ink * vec3(0.70, 0.74, 0.84), dk * 0.6 * k) return mix(o, vec3(1.0, 1.0, 1.0), lt * 0.16 * k) } // How much of a pixel the travelled part of a pill well covers: the well's // own shape from the stop, or from the default when the slider asks, to the // thumb's centre. `x0` and `x1` are the thumb's centre at the two stops; a // fill that starts or ends at a stop runs on into the well's round end. let nm_span_e = fn(pq: vec2, c: vec2, h: vec2, x0: float, x1: float, at: float, org_at: float, from_org: float, px: float) -> float { let f0 = mix(0.0, min(org_at, at), from_org) let f1 = mix(at, max(org_at, at), from_org) let e0 = mix(mix(x0, x1, f0), c.x - h.x, step(f0, 0.0001)) let e1 = mix(mix(x0, x1, f1), c.x + h.x, step(0.9999, f1)) let fd = Material.sd_box(pq, vec2((e0 + e1) * 0.5, c.y), vec2(max((e1 - e0) * 0.5, h.y), h.y), h.y) return Finish.cover(fd, px) * step(0.0005, f1 - f0) } // The plain slider: an 8 point pill well, the accent filling it from the // stop (or from the default) to a 16 point raised thumb. The label and the // readout stand in the 20 points above. mod.widgets.SliderFlat.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let dis = mix(1.0, 0.35, self.disabled) let gr = vec3(nm_grain(org + pq, px)) let top = 20.0 let cy = Finish.snap(org.y + top + (self.rect_size.y - top) * 0.5, px) - org.y let hr = 8.0 let x0 = hr + 1.0 let x1 = self.rect_size.x - hr - 1.0 let gc = vec2(self.rect_size.x * 0.5, cy) let gh = vec2(self.rect_size.x * 0.5 - 1.0, 4.0) let ga = Finish.cover(Material.sd_box(pq, gc, gh, 4.0), px) let col = nm_groove(vec3(0.878, 0.898, 0.925), pq, gc, gh, px, dis) + gr let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), self.disabled) let fcol = nm_fill_e(ink, pq, gc, gh, 4.0, 8.0, dis) + gr let fa = nm_span_e(pq, gc, gh, x0, x1, self.slide_pos, self.origin_pos, self.arc_origin, px) return nm_disc_e(vec4(col * ga, ga), vec4(fcol * fa, fa), pq, org, vec2(mix(x0, x1, self.slide_pos), cy), hr, 2.5 * hr, 1.0, px, self.hover, self.drag, self.focus, self.disabled) } // The minimal slider: a 4 point pill groove with no line drawn along it, // the accent filling it from the stop (or from the default) to a 10 point // raised dot. mod.widgets.SliderMinimal.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let dis = mix(1.0, 0.35, self.disabled) let gr = vec3(nm_grain(org + pq, px)) let top = self.offset_y let cy = Finish.snap(org.y + top + (self.rect_size.y - top) * 0.5, px) - org.y let hr = 5.0 let x0 = hr let x1 = self.rect_size.x - hr let gc = vec2(self.rect_size.x * 0.5, cy) let gh = vec2(self.rect_size.x * 0.5, 2.0) let ga = Finish.cover(Material.sd_box(pq, gc, gh, 2.0), px) let col = nm_groove(vec3(0.878, 0.898, 0.925), pq, gc, gh, px, 0.9 * dis) + gr let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), self.disabled) let fcol = nm_fill_e(ink, pq, gc, gh, 2.0, 4.0, dis) + gr let fa = nm_span_e(pq, gc, gh, x0, x1, self.slide_pos, self.origin_pos, self.arc_origin, px) return nm_disc_e(vec4(col * ga, ga), vec4(fcol * fa, fa), pq, org, vec2(mix(x0, x1, self.slide_pos), cy), hr, 2.5 * hr, 1.0, px, self.hover, self.drag, self.focus, self.disabled) } // The round slider: the track is a pill well the height of the control // beside its label, the travelled part of it filled flat with the accent, // and the thumb a raised disc standing in the well two points clear of its // edge, its shadows kept to the well. mod.widgets.SliderRoundFlat.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let dis = mix(1.0, 0.35, self.disabled) let gr = vec3(nm_grain(org + pq, px)) let cy = Finish.snap(org.y + self.rect_size.y * 0.5, px) - org.y let sx0 = self.label_size let sh = min(self.rect_size.y * 0.5, 10.0) let sc = vec2((sx0 + self.rect_size.x) * 0.5, cy) let shh = vec2((self.rect_size.x - sx0) * 0.5, sh) let sd = Material.sd_box(pq, sc, shh, sh) let sa = Finish.cover(sd, px) let col = nm_well(vec3(0.918, 0.929, 0.957), pq, sc, shh, sh, sd, px, 2.0 * sh + 4.0, dis) + gr let x0 = sx0 + sh let x1 = self.rect_size.x - sh let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), self.disabled) let fcol = nm_fill_e(ink, pq, sc, shh, sh, 2.0 * sh, dis) + gr let fa = nm_span_e(pq, sc, shh, x0, x1, self.slide_pos, self.origin_pos, self.arc_origin, px) let hr = sh - 2.0 return nm_disc_e(vec4(col * sa, sa), vec4(fcol * fa, fa), pq, org, vec2(mix(x0, x1, self.slide_pos), cy), hr, 2.5 * hr, sa, px, self.hover, self.drag, self.focus, self.disabled) } // The fader: the slider bench's white spheres puck. A round white cap 16 // points across and 8 tall, its top cut by a sphere into a shallow dish // inside a rounded rim, lit by one key light from the top left and throwing // a soft shadow down and to the right. It rides a 6 point pill well sunk in // the page, this sheet's own well in place of the bench's slot, with twenty // ticks on the far side, every fifth long, and no value fill: the cap is // the value. The shape, its curves, the material numbers and the inks are // the bench preset's; the ground and the well are this sheet's. The puck is // as wide as it is long, so `cap_cross` is not read. Lying down or standing // up. mod.widgets.SliderFader.cap_size = 16.0 mod.widgets.SliderFader.track_inset = 8.0 mod.widgets.SliderFader.draw_bg.track_size = uniform(6.0) mod.widgets.SliderFader.clip_x = false mod.widgets.SliderFader.clip_y = false // The material, live in the tweaker: the key light in screen space (x // right, y down, z toward the viewer) and its intensity; the highlight, its // roughness, the broad gloss sheen and the rim toward grazing; the // occlusion at the foot and how far up it reaches; the plain sky's // reflection and how close the eye sits over the cap (0 far above); metal, // turned grain and self-light, all off here; the exposure in stops and the // highlight roll-off; the cast shadow's strength and its softest edge (held // to the cap's height) and the contact shadow's strength. mod.widgets.SliderFader.draw_bg.cap_light = uniform(vec3(-0.36, -0.6, 0.72)) mod.widgets.SliderFader.draw_bg.cap_light_intensity = uniform(0.9) mod.widgets.SliderFader.draw_bg.cap_specular = uniform(0.08) mod.widgets.SliderFader.draw_bg.cap_roughness = uniform(0.38) mod.widgets.SliderFader.draw_bg.cap_gloss = uniform(0.16) mod.widgets.SliderFader.draw_bg.cap_rim = uniform(0.54) mod.widgets.SliderFader.draw_bg.cap_ao = uniform(0.32) mod.widgets.SliderFader.draw_bg.cap_ao_reach = uniform(1.4) mod.widgets.SliderFader.draw_bg.cap_reflection = uniform(0.16) mod.widgets.SliderFader.draw_bg.cap_reflection_perspective = uniform(0.35) mod.widgets.SliderFader.draw_bg.cap_metal = uniform(0.0) mod.widgets.SliderFader.draw_bg.cap_spun = uniform(0.0) mod.widgets.SliderFader.draw_bg.cap_glow = uniform(0.0) mod.widgets.SliderFader.draw_bg.cap_exposure = uniform(0.0) mod.widgets.SliderFader.draw_bg.cap_rolloff = uniform(0.6) mod.widgets.SliderFader.draw_bg.cap_shadow = uniform(0.45) mod.widgets.SliderFader.draw_bg.cap_shadow_blur = uniform(18.0) mod.widgets.SliderFader.draw_bg.cap_contact = uniform(0.25) // The inks, the preset's: a white cap, a white light, a slate-blue shade (a // multiplier: what a full shadow does to what it falls on) and pale ticks. // The ground is the page's own colour, so the fader, laid over the page // premultiplied, leaves the page itself untouched. mod.widgets.SliderFader.draw_bg.cap_ground = uniform(mod.theme.color_bg_app) mod.widgets.SliderFader.draw_bg.cap_body = uniform(#ffffff) mod.widgets.SliderFader.draw_bg.cap_light_ink = uniform(#ffffff) mod.widgets.SliderFader.draw_bg.cap_shadow_ink = uniform(#9aa4b8) mod.widgets.SliderFader.draw_bg.cap_tick_ink = uniform(#c0c3ca) // The puck's solid at `q`, a point in the cap's own frame (x along the // track, y across it), `hs` its half size and `r` its corner: the height // 0..1 in .x, its gradient per point in .yz, and in .w the footprint's // distance, negative inside. The curves are the preset's, handles resolved // by the bench: along, across and plan level at full (a round footprint // under a flat top), and the edge rising from the outline and rounding level // over the whole radius. Shape: corner and edge width the whole half size, // blend 0.02, 8 points tall. No ribs, no mark. The cut is a sphere (2) at // the middle (0), its radius 1.17 of the half length, its centre 1.38 // heights up. On the preset's 16 point cap the wall leaves the outline at 45 // degrees and the cut takes the top down to a fifth of the height at the // middle, a dish inside a rim; the height and the cut's centre are in // points, so a longer cap dishes deeper, as it does on the bench. let nm_puck_solid = fn(q: vec2, hs: vec2, r: float) -> vec4 { let plan = Cap.curve((q.x / hs.x + 1.0) * 0.5, 2.0, vec2(0.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0)) let fp = Cap.sd_cap(q, hs, r, plan) let along = Cap.curve((q.x / hs.x + 1.0) * 0.5, 2.0, vec2(0.0, 1.0), vec2(0.1667, 0.0), vec2(-0.1667, 0.0), vec2(1.0, 1.0), vec2(0.1667, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0)) let across = Cap.curve(abs(q.y) / hs.y, 2.0, vec2(0.0, 1.0), vec2(0.1667, 0.0), vec2(-0.1667, 0.0), vec2(1.0, 1.0), vec2(0.1667, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0)) // The edge is read no further out than the outline: past it the curve // has no slope, and the pixels the outline half covers would take the // flat top's light, a dotted bright line along the shaded foot. let edge = Cap.curve(Cap.edge_x(min(fp.x, 0.0), hs, 1.0), 2.0, vec2(0.0, 0.0), vec2(0.1667, 0.1667), vec2(-0.1667, 0.0), vec2(1.0, 1.0), vec2(0.1667, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0)) let f = Cap.field(q, hs, fp, along, across, edge, vec2(1.0, 0.0), vec4(1.0, 1.0, 0.02, 8.0), vec4(0.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.5, 1.0, 0.0), vec4(2.0, 0.0, 1.17, 1.38)) return vec4(f.x, f.y, f.z, fp.x) } // A colour drawn on a page of colour `ground`, handed back premultiplied so // the page itself is left as it is: the least cover that keeps every // channel, and the light it adds over what shows through. Exact over that // ground, so a shade keeps the slate-blue of the shadow ink; the engine's // `on_ground` covers by the darkest channel alone and greys it. let nm_on_page = fn(col: vec3, ground: vec3) -> vec4 { let dark = max(ground - col, vec3(0.0, 0.0, 0.0)) / max(ground, vec3(0.01, 0.01, 0.01)) let a = clamp(max(dark.x, max(dark.y, dark.z)), 0.0, 1.0) return vec4(max(col - ground * (1.0 - a), vec3(0.0, 0.0, 0.0)), a) } // The quad grows past the layout box by the cast shadow's reach (its throw, // five and a half points from this light, and its softest edge, the cap's 8 // point height), the focus ring's four points, and whatever of the cap is // wider than the control, so a cap at a stop never has its shadow or its // ring cut at the quad's edge. mod.widgets.SliderFader.draw_bg.vertex = fn() { let across = mix(self.rect_size.y, self.rect_size.x, self.is_vertical) let m = 14.0 + max(self.cap_px - across, 0.0) * 0.5 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.SliderFader.draw_bg.pixel = fn() { let v = self.is_vertical let sz = self.rect_size let along = mix(sz.x, sz.y, v) let across = mix(sz.y, sz.x, v) let m = 14.0 + max(self.cap_px - across, 0.0) * 0.5 let pq = self.pos * (sz + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let en = 1.0 - self.disabled let dis = mix(1.0, 0.35, self.disabled) // Track space: `ab.x` along the track from the value's zero (up the // screen when the fader stands), `ab.y` across it. let ab = vec2(mix(pq.x, sz.y - pq.y, v), mix(pq.y, pq.x, v)) let inset = self.inset_px let travel = max(along - inset * 2.0 - self.cap_px, 1.0) let foot = inset + self.cap_px * 0.5 let mid = across * 0.5 let ground = self.cap_ground.rgb let body = self.cap_body.rgb let light_ink = self.cap_light_ink.rgb let shadow_ink = self.cap_shadow_ink.rgb // The key light on screen for the cap's shading, and turned into track // space for everything laid out along the track. let l = normalize(self.cap_light) let lt = Cap.light(self.cap_light, v) // The ground: the scale beside the track, faded toward the page when // disabled as this sheet's ticks are. The track itself is this sheet's // pill well from one inset end to the other, worked out on screen so the // light falls on it as on every other well, and kept as what it changes // on the page, to go on after the tone below: through the roll-off its // light would come out at half strength. let tw = min(self.track_size, across) let tlen = max(along - inset * 2.0, 1.0) let at = foot + self.slide_pos * travel let hs = vec2(self.cap_px * 0.5, self.cap_px * 0.5) let wc = mix(vec2(inset + tlen * 0.5, mid), vec2(mid, sz.y - inset - tlen * 0.5), v) let wh = mix(vec2(tlen * 0.5, tw * 0.5), vec2(tw * 0.5, tlen * 0.5), v) let wa = Finish.cover(Material.sd_box(pq, wc, wh, tw * 0.5), px) let wv = (nm_groove(vec3(0.878, 0.898, 0.925), pq, wc, wh, px, dis) - ground) * wa var col = Cap.ticks(ground, ab, mid, foot, travel, 20.0, 1.0, 5.0, tw, 0.0, px, self.cap_tick_ink.rgb) col = mix(ground, col, dis) // The cap, worked out only where it, its shadow or its focus ring can // reach. Under the hand it lifts as this sheet's raised parts do: its // shadows deepen by three tenths and its face lightens a hair. Held, it // sinks half a point, so its shadow shortens, and its face darkens as // this sheet's held caps do. Disabled, its relief falls to a third as // this sheet's does: its shadows and its own modelling, and its rim // light goes, as this sheet's disabled faces lose their lit edge. let q = ab - vec2(at, mid) let r = min(hs.x, hs.y) let height = 8.0 let rise = height - 0.5 * self.drag let reach = rise * 0.7 * length(lt.xy) / max(lt.z, 0.25) + max(rise, 1.0) + 5.0 var capc = ground var cov = 0.0 var shd = 0.0 var halo = 0.0 var cap_d = 100.0 if max(abs(q.x) - hs.x, abs(q.y) - hs.y) < reach { let s = nm_puck_solid(q, hs, r) let n = Cap.normal(s.yz, height, v) let eye = Cap.view(q, hs, self.cap_reflection_perspective, v) capc = Cap.shade(n, l, eye, s.x, body, 0.0, 0.0, 1.0, self.cap_specular, self.cap_roughness, self.cap_gloss, self.cap_rim * en, self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_metal, self.cap_light_intensity, body, light_ink, shadow_ink, ground) if self.cap_spun > 0.001 { let grain = Cap.spun(q, hs, lt.xy) * self.cap_spun capc = Cap.in_shadow(Cap.in_light(capc, clamp(grain, 0.0, 1.0) * 0.55, light_ink), clamp(-grain, 0.0, 1.0) * 0.42, shadow_ink) } if self.cap_glow > 0.001 { capc = Cap.emit(capc, s.x, body, light_ink, self.cap_glow) } if self.disabled > 0.001 { // The cap as a flat disc would read, the relief laid back over // it at a third. let level = Cap.shade(vec3(0.0, 0.0, 1.0), l, vec3(0.0, 0.0, 1.0), 1.0, body, 0.0, 0.0, 1.0, self.cap_specular, self.cap_roughness, self.cap_gloss, 0.0, self.cap_ao, self.cap_ao_reach, self.cap_reflection, self.cap_metal, self.cap_light_intensity, body, light_ink, shadow_ink, ground) capc = mix(level, capc, dis) } cov = Cap.cover(s.w, px) // Under it: the cast shadow, thrown from the top outline the bench // gives for this cap, and the contact shadow round its foot. let cast_sh = Cap.shadow(q, hs, r, vec3(0.3529, 0.3529, 1.0), lt, rise, self.cap_shadow_blur, self.cap_shadow * (1.0 + 0.3 * max(self.hover, self.drag)) * dis, px) let contact_sh = Cap.contact(s.w, max(rise * 0.3, 1.0), self.cap_contact * dis) shd = clamp(cast_sh + contact_sh, 0.0, 1.0) halo = Cap.glow(s.w, hs, self.cap_glow) cap_d = s.w } col = mix(col, col * shadow_ink, shd * (1.0 - cov)) col = col + body * halo * 0.9 * (1.0 - cov) col = mix(col, capc, cov) // Toned, the page's colour toned alike. The face's lift under the hand, // its darkening when held and the well, where the cap leaves it showing, // go on after the tone, as displayed, so the roll-off does not swallow // them. Then the fader is laid over the page without painting it, and // this sheet's focus ring goes round the cap. let ev = pow(2.0, self.cap_exposure) let lift = (0.006 * self.hover - 0.012 * self.drag) * en * cov let shown = Cap.tone(col, ev, self.cap_rolloff) + vec3(lift, lift, lift) + wv * (1.0 - cov) let res = nm_on_page(shown, Cap.tone(ground, ev, self.cap_rolloff)) return Finish.over(res, nm_focus(cap_d, px, self.focus * en)) } // KNOBS. A raised disc standing in a sunken ring: a round well a little // wider than the disc, whose floor carries the value as a flat accent arc // in its outer part. The disc's two shadows fall into the ring and stop at // its edge, for the disc stands no higher than the page round it; the arc // lies over them, clean. A flat accent dot on the disc points at the value. let nm_cap = fn(dish: vec4, ring: vec4, pq: vec2, org: vec2, c: vec2, r: float, px: float, ang: float, hover: float, drag: float, focus: float, disabled: float) -> vec4 { // The shadows of a part a quarter as tall as the disc is wide, held // between 14 and 30 points. let o = nm_disc_e(dish, ring, pq, org, c, r, clamp(0.5 * r, 14.0, 30.0), dish.a, px, hover, drag, focus, disabled) let pr = clamp(0.07 * r, 1.5, 4.0) let pc = c + vec2(-sin(ang), cos(ang)) * (r - max(0.2 * r, 3.5)) let pa = Finish.cover(length(pq - pc) - pr, px) let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), disabled) return Finish.over(o, vec4(ink * pa, pa)) } // The ring a knob stands in: a round well of radius `rr` with the fields' // pale floor, its shade reaching in as far as the `w` points of floor that // show round the disc. let nm_dish_e = fn(pq: vec2, org: vec2, c: vec2, rr: float, w: float, px: float, disabled: float) -> vec4 { let d = length(pq - c) - rr var col = nm_well(vec3(0.918, 0.929, 0.957), pq, c, vec2(rr, rr), rr, d, px, 4.0 * w, mix(1.0, 0.35, disabled)) col = col + vec3(nm_grain(org + pq, px)) let a = Finish.cover(d, px) return vec4(col * a, a) } // The value on that floor: an accent arc half the ring wide in its outer // part, from `a0` to `a1`, shaded by the well as a slider's fill is. let nm_ring_e = fn(pq: vec2, org: vec2, c: vec2, rr: float, w: float, a0: float, a1: float, px: float, disabled: float) -> vec4 { let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), disabled) var col = nm_fill_e(ink, pq, c, vec2(rr, rr), rr, 1.5 * w, mix(1.0, 0.35, disabled)) col = col + vec3(nm_grain(org + pq, px)) let a = Finish.cover(nm_arc(pq, c, rr - 0.45 * w, a0, max(a1, a0 + 0.001), 0.5 * w), px) * step(0.0005, a1 - a0) return vec4(col * a, a) } // The panel knob: the ring in the biggest circle the box holds, pinned to // the bottom under its legend, the ring never under 4.5 points wide. mod.widgets.RotaryKnob.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let dia = min(self.rect_size.x, self.rect_size.y) let c = vec2(self.rect_size.x * 0.5, self.rect_size.y - dia * 0.5) let rr = dia * 0.5 - 1.0 let w = clamp(0.16 * rr, 4.5, 10.0) let one = PI / 180.0 let sweep = 2.0 * PI - self.gap * one let start = self.gap * one * 0.5 let val = start + sweep * self.slide_pos let org_a = start + sweep * self.origin_pos let a0 = mix(start, min(org_a, val), self.arc_origin) let a1 = mix(val, max(org_a, val), self.arc_origin) let dish = nm_dish_e(pq, org, c, rr, w, px, self.disabled) let ring = nm_ring_e(pq, org, c, rr, w, a0, a1, px, self.disabled) return nm_cap(dish, ring, pq, org, c, rr - w, px, val, self.hover, self.drag, self.focus, self.disabled) } mod.widgets.RotaryKnob.width = 44.0 mod.widgets.RotaryFlat.text_input.draw_bg.color = #00000000 mod.widgets.RotaryKnob.height = 62.0 // The dial: the same ring and disc, centred under the legend and the // readout, which stand in the 22 points above. mod.widgets.RotaryFlat.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let top = 22.0 let dia = min(self.rect_size.x, self.rect_size.y - top) let c = vec2(self.rect_size.x * 0.5, top + dia * 0.5) let rr = dia * 0.5 - 1.5 let w = clamp(0.16 * rr, 4.5, 10.0) let one = PI / 180.0 let sweep = 2.0 * PI - self.gap * one let start = self.gap * one * 0.5 let val = start + sweep * self.slide_pos let org_a = start + sweep * self.origin_pos let a0 = mix(start, min(org_a, val), self.arc_origin) let a1 = mix(val, max(org_a, val), self.arc_origin) let dish = nm_dish_e(pq, org, c, rr, w, px, self.disabled) let ring = nm_ring_e(pq, org, c, rr, w, a0, a1, px, self.disabled) return nm_cap(dish, ring, pq, org, c, rr - w, px, val, self.hover, self.drag, self.focus, self.disabled) } // PROGRESS. A pill well as thick as the bar asks, six points unless told // otherwise; the fill a flat pill of the accent inset by a point, with no // line along it. Without a value the bar holds one soft segment in the // middle of the well: a shader may not read the clock, so the sweep stands // still. A bar that asks for its stop wears a dot of the fill at the far // end of the well. mod.widgets.ProgressBarFlat.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let w = self.rect_size.x let t = max(min(self.thickness, self.rect_size.y), 2.0) let cy = Finish.snap(org.y + self.rect_size.y * 0.5, px) - org.y let x0 = w * self.start let x1 = w * self.end let gc = vec2((x0 + x1) * 0.5, cy) let gh = vec2((x1 - x0) * 0.5, t * 0.5) let gd = Material.sd_box(p, gc, gh, t * 0.5) var col = nm_well(vec3(0.878, 0.898, 0.925), p, gc, gh, t * 0.5, gd, px, max(14.0, 2.0 * t), 1.0) col = col + vec3(nm_grain(org + p, px)) let ga = Finish.cover(gd, px) * step(0.5, self.track) var acc = vec4(col * ga, ga) let fr = max(t * 0.5 - 1.0, t * 0.3) let xv = w * clamp(self.value, self.start, self.end) let det = step(0.0, self.value) let fa0 = mix(x0 + w * 0.3, x0 + 1.0, det) let fa1 = mix(x0 + w * 0.65, max(xv - 1.0, x0 + 1.0 + 2.0 * fr), det) let fd = Material.sd_box(p, vec2((fa0 + fa1) * 0.5, cy), vec2((fa1 - fa0) * 0.5, fr), fr) let fcol = mix(vec3(0.290, 0.486, 0.965), self.fill_color().rgb, step(0.5, self.intent)) // The still sweep fades in and out at both ends. let soft = mix(smoothstep(fa0, fa0 + w * 0.12, p.x) * smoothstep(fa1, fa1 - w * 0.12, p.x), 1.0, det) let show = mix(1.0, step(x0 + 0.5, xv), det) let fA = Finish.cover(fd, px) * soft * show acc = Finish.over(acc, vec4(fcol * fA, fA)) let stop_d = length(p - vec2(x1 - 1.0 - fr, cy)) - fr let stop_a = Finish.cover(stop_d, px) * step(0.5, self.stop_indicator) * step(0.5, self.track) acc = Finish.over(acc, vec4(fcol * stop_a, stop_a)) return acc * self.opacity } mod.widgets.ProgressBarFlat.height = 8.0 // Written as a uniform, since the face reads it and an app may set it. mod.widgets.ProgressBarFlat.draw_bg.thickness = uniform(6.0) // LEVEL METER. A pill well with a ladder of three point cells, one point // apart, lit in the accent, then warm past seven tenths and hot past // eighty-five hundredths; unlit cells keep an eighth of their colour. The // well is shaded where it stands in the window, so a standing meter takes // its light from the top left like everything round it. The lamp at the // end is a small well that fills flat when it latches. mod.widgets.LevelMeter.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let v = self.vertical let along = mix(self.rect_size.x, self.rect_size.y, v) let across = mix(self.rect_size.y, self.rect_size.x, v) let q = vec2(mix(p.x, self.rect_size.y - p.y, v), mix(p.y, p.x, v)) let lamp_room = self.lamp * (6.0 + self.lamp_gap) let bar = max(4.0, along - lamp_room) let r = min(across, bar) * 0.5 // The well in the window's frame: lying down it runs from the left, // standing up it rises from the bottom. let gc = vec2(mix(bar * 0.5, across * 0.5, v), mix(across * 0.5, self.rect_size.y - bar * 0.5, v)) let gh = vec2(mix(bar * 0.5, across * 0.5, v), mix(across * 0.5, bar * 0.5, v)) let gd = Material.sd_box(p, gc, gh, r) var col = nm_well(vec3(0.878, 0.898, 0.925), p, gc, gh, r, gd, px, 14.0, 1.0) let pad = 1.5 let span = max(1.0, bar - pad * 2.0) let share = clamp((q.x - pad) / span, 0.0, 1.0) var zone = vec3(0.290, 0.486, 0.965) zone = mix(zone, vec3(0.910, 0.635, 0.227), step(0.7, share)) zone = mix(zone, vec3(0.878, 0.325, 0.235), step(0.85, share)) let cell = step(0.25, fract((q.x - pad) / 4.0)) let inside = step(pad, q.x) * step(q.x, bar - pad) * step(abs(q.y - across * 0.5), across * 0.5 - pad) let lit = step(q.x, pad + span * self.level) col = mix(col, zone, cell * inside * mix(0.12, 1.0, lit)) // The high-water mark: a slate bar a point and a half wide. let hx = pad + span * self.hold let hm = step(abs(q.x - hx), 0.75) * inside * step(0.002, self.hold) col = mix(col, vec3(0.388, 0.416, 0.478), hm * 0.8) col = col + vec3(nm_grain(org + p, px)) let ga = Finish.cover(gd, px) var acc = vec4(col * ga, ga) // The lamp. let lq = vec2(along - 3.0, across * 0.5) let lc = vec2(mix(lq.x, lq.y, v), mix(lq.y, self.rect_size.y - lq.x, v)) let lr = min(3.0, across * 0.5) let ld = length(p - lc) - lr var lcol = nm_well(vec3(0.878, 0.898, 0.925), p, lc, vec2(lr, lr), lr, ld, px, 10.0, 1.0) lcol = mix(lcol, vec3(0.878, 0.325, 0.235), self.over) let la = Finish.cover(ld, px) * self.lamp acc = Finish.over(acc, vec4(lcol * la, la)) return acc * self.opacity } mod.widgets.LevelMeter.height = 8.0 mod.widgets.LevelMeterColumn.width = 8.0 // A raised part's shadows kept to the room round it: `g` points of it all // round the face of half size `h` about `c`, faded out over the last `f`, // so they end inside a strip's rim, or inside the quad that draws them, // and never at a hard edge. let nm_kept_f = fn(sh: vec4, p: vec2, c: vec2, h: vec2, r: float, g: float, f: float) -> vec4 { let dr = Material.sd_box(p, c, h + vec2(g, g), r + g) return sh * smoothstep(0.0, max(f, 0.01), -dr) } // A raised pill seated in a strip's well with `g` points of floor round // it: its face, and its two shadows kept to that floor, so neither the // light thrown up nor the shade thrown down climbs out over the rim onto // the page. `k` is how far it stands, 1 all the way up. let nm_seat_f = fn(p: vec2, c: vec2, h: vec2, r: float, g: float, px: float, org: vec2, k: float) -> vec4 { let d = Material.sd_box(p, c, h, r) let acc = nm_kept_f(nm_cast(p, c, h, r, 16.0, k), p, c, h, r, g, g * 0.75) let up = clamp(k, 0.0, 1.0) var col = mix(nm_ground(), nm_face(p, c, h, d, px, 0.0, up), up) col = col + vec3(nm_grain(org + p, px)) let a = Finish.cover(d, px) return Finish.over(acc, vec4(col * a, a)) } // SCROLL BAR. An 8 point well at half depth with a 6 point raised pill in // it: the pill's face, and its two shadows kept to the point of floor // round it. mod.widgets.ScrollBar.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let v = self.is_vertical let along = mix(self.rect_size.x, self.rect_size.y, v) let across = mix(self.rect_size.y, self.rect_size.x, v) let q = vec2(mix(p.x, p.y, v), mix(p.y, p.x, v)) let w = min(8.0, across) let mb = across * 0.5 let tc = vec2(along * 0.5, mb) let th = vec2(along * 0.5, w * 0.5) let td = Material.sd_box(q, tc, th, w * 0.5) var col = nm_well(vec3(0.898, 0.914, 0.937), q, tc, th, w * 0.5, td, px, 10.0, 0.5) col = col + vec3(nm_grain(self.rect_pos + p, px)) let ta = Finish.cover(td, px) var acc = vec4(col * ta, ta) let hw = max(w * 0.5 - 1.0, 1.0) let s0 = along * self.norm_scroll + 1.0 let s1 = along * (self.norm_scroll + self.norm_handle) - 1.0 let hc = vec2((s0 + s1) * 0.5, mb) let hh = vec2(max((s1 - s0) * 0.5, hw), hw) let hd = Material.sd_box(q, hc, hh, hw) let k = (1.0 + 0.3 * self.hover) * (1.0 - 0.5 * self.drag) acc = Finish.over(acc, nm_kept_f(nm_cast(q, hc, hh, hw, 8.0, k), q, hc, hh, hw, 1.0, 0.75)) var hcol = nm_face(q, hc, hh, hd, px, 0.006 + 0.008 * self.hover - 0.012 * self.drag, 1.0) hcol = hcol + vec3(nm_grain(self.rect_pos + p, px)) let ha = Finish.cover(hd, px) acc = Finish.over(acc, vec4(hcol * ha, ha)) return acc * self.opacity } mod.widgets.ScrollBar.bar_size = 10.0 // SEGMENTED CONTROL. A 28 point pill well; the current answer stands up in // it as a raised 24 point pill with its two shadows, kept to the two points // of floor round it, and its label in the accent ink. mod.widgets.SegmentedControlFlat.segment_height = 24.0 mod.widgets.SegmentedControlFlat.segment_padding = 12.0 mod.widgets.SegmentedControlFlat.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = self.rect_size * 0.5 let r = min(14.0, min(h.x, h.y)) let d = Material.sd_box(p, c, h, r) var col = nm_well(vec3(0.910, 0.925, 0.945), p, c, h, r, d, px, 24.0, 1.0) col = col + vec3(nm_grain(self.rect_pos + p, px)) let a = Finish.cover(d, px) return vec4(col * a, a) } mod.widgets.SegmentedControlFlat.draw_pill.vertex = fn() { let m = 4.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.SegmentedControlFlat.draw_pill.pixel = fn() { let m = 4.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = self.rect_size * 0.5 return nm_seat_f(pq, c, h, min(h.x, h.y), 2.0, px, self.rect_pos, 1.0) } mod.widgets.SegmentedControlFlat.draw_text.color = #636a7a mod.widgets.SegmentedControlFlat.intent = mod.widgets.BadgeIntent.Primary mod.widgets.SegmentedControlFlat.draw_text_selected.color = #3860d6 mod.widgets.SegmentedControlFlat.draw_text_selected.text_style = nm_medium // TABS. The strip is the same pill well; the tab in use stands up in it as // a raised pill two points in from the well's edge, its two shadows kept // to that floor, and the tab under the pointer rises part of the way. The // quiet tabs lie flat on the floor, with nothing drawn between them. mod.widgets.Tabs.height = 28.0 mod.widgets.Tabs.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = self.rect_size * 0.5 let r = min(14.0, min(h.x, h.y)) let d = Material.sd_box(p, c, h, r) var col = nm_well(vec3(0.910, 0.925, 0.945), p, c, h, r, d, px, 24.0, 1.0) col = col + vec3(nm_grain(self.rect_pos + p, px)) let a = Finish.cover(d, px) return vec4(col * a, a) } mod.widgets.Tabs.draw_tab.pixel = fn() { return vec4(0.0, 0.0, 0.0, 0.0) } mod.widgets.Tabs.draw_tab_hover.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = self.rect_size * 0.5 - vec2(2.0, 2.0) return nm_seat_f(p, c, h, min(h.x, h.y), 2.0, px, self.rect_pos, 0.4) } mod.widgets.Tabs.draw_tab_active.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = self.rect_size * 0.5 - vec2(2.0, 2.0) return nm_seat_f(p, c, h, min(h.x, h.y), 2.0, px, self.rect_pos, 1.0) } mod.widgets.Tabs.draw_sep.pixel = fn() { return vec4(0.0, 0.0, 0.0, 0.0) } mod.widgets.Tabs.draw_mark.color = #636a7a mod.widgets.Tabs.draw_text.color = #636a7a mod.widgets.Tabs.draw_text_active.color = #3860d6 mod.widgets.Tabs.draw_text_active.text_style = nm_medium // RADIO TABS. A pill flat on the page at rest that rises part of the way // under the pointer; the one in use stands all the way up with its two // shadows and its label in the accent ink, and a held one is pressed in. // The quad grows by six points, and the shadows fade out inside it. mod.widgets.RadioButtonTabFlat.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.RadioButtonTabFlat.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let lo = vec2(Finish.snap(org.x, px), Finish.snap(org.y, px)) - org let hi = vec2(Finish.snap(org.x + self.rect_size.x, px), Finish.snap(org.y + self.rect_size.y, px)) - org let c = (lo + hi) * 0.5 let h = (hi - lo) * 0.5 let r = min(h.x, h.y) let d = Material.sd_box(pq, c, h, r) let en = 1.0 - self.disabled let press = self.down * en let up = max(self.active, 0.4 * self.hover * en) * (1.0 - press) var acc = nm_kept_f(nm_cast(pq, c, h, r, 20.0, up * mix(1.0, 0.35, self.disabled)), pq, c, h, r, m - 0.5, 2.5) let rise = min(up * 2.0, 1.0) var col = mix(nm_ground(), nm_face(pq, c, h, d, px, 0.012 * self.hover * en, rise), rise) col = mix(col, nm_well(col, pq, c, h, r, d, px, 20.0, 0.8), press) col = col + vec3(nm_grain(org + pq, px)) let a = Finish.cover(d, px) acc = Finish.over(acc, vec4(col * a, a)) return Finish.over(acc, nm_focus(d, px, self.focus * en)) } mod.widgets.RadioButtonTabFlat.padding = mod.turtle.Inset{left: 0.0 right: 8.0 top: 5.0 bottom: 5.0} mod.widgets.RadioButtonTabFlat.draw_text.color = #636a7a mod.widgets.RadioButtonTabFlat.draw_text.color_active = #3860d6 // CHIPS. A small raised pill, pressed in while held, its shadows fading out // inside the six points the quad grows by. A chip whose role gives it a // solid fill, a chosen one or a filled one, wears that fill as its face, // shaded like the page's own faces; a pale tint keeps the page's face. A // keyed chip wears the focus mark. mod.widgets.ChipFlat.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.ChipFlat.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = self.rect_size * 0.5 let r = min(self.radius, min(h.x, h.y)) let d = Material.sd_box(pq, c, h, r) var acc = nm_kept_f(nm_cast(pq, c, h, r, 20.0, (1.0 + 0.3 * self.hover) * (1.0 - self.down)), pq, c, h, r, m - 0.5, 2.5) let lit = nm_face(pq, c, h, d, px, 0.018 * self.hover, 1.0 - self.down) // How much of the role's fill shows: none of a pale tint, all of a // saturated colour. let fill = self.color let chroma = max(fill.x, max(fill.y, fill.z)) - min(fill.x, min(fill.y, fill.z)) let solid = smoothstep(0.12, 0.3, chroma) * fill.w var col = mix(lit, fill.rgb + (lit - nm_ground()), solid) col = mix(col, nm_well(col - vec3(0.013, 0.013, 0.013), pq, c, h, r, d, px, 20.0, 0.75), self.down) col = col + vec3(nm_grain(self.rect_pos + pq, px)) let a = Finish.cover(d, px) acc = Finish.over(acc, vec4(col * a, a)) return Finish.over(acc, nm_focus(d, px, self.keyed)) } mod.widgets.ChipFlat.draw_text.color = #636a7a mod.widgets.ChipFlat.margin = mod.turtle.Inset{left: 4.0 right: 4.0 top: 4.0 bottom: 4.0} // SURFACES. A panel is the page raised by a fixed, shallow step whatever // its size, with a 12 point corner and the light line on its top edge; an // inset panel is a tray pressed a step into the page, with a lit lip. let nm_panel = fn(pq: vec2, size: vec2, org: vec2, px: float, face: vec3, r: float, raised: float, sunken: float, lift: float) -> vec4 { let c = size * 0.5 let h = size * 0.5 let rr = min(r, min(h.x, h.y)) let d = Material.sd_box(pq, c, h, rr) // Raised: a slate shadow three and a half points right and four and a // half down and a halo of light as far up and left, each blurred by three // and a half, both gone inside the // ten to twelve points a panel's quad grows by. Under the pointer the // shadow deepens a step. let dark = Finish.drop(pq, c, h, rr, vec2(3.5, 4.5), 3.5) * (0.65 + 0.1 * lift) * raised let light = Finish.drop(pq, c, h, rr, vec2(-3.5, -4.5), 3.5) * 1.0 * raised var acc = vec4(nm_slate_a() * dark, dark) acc = acc + vec4((vec3(1.0, 1.0, 1.0) - nm_ground()) * light, 0.0) // The face: a long, faint fall from the top left to the bottom right, and // a three point shoulder lit and shaded as a raised part's is, by the // same amounts whatever colour the panel is given. No line on any edge. let u = clamp((pq.x / max(size.x, 1.0) + pq.y / max(size.y, 1.0)) * 0.5, 0.0, 1.0) let g = (mix(0.01, -0.008, u) + 0.006 * lift) * raised var col = face + vec3(g, g, g) let sh = nm_shoulder_a(pq, c, h, max(rr, 3.0), d, max(3.0, 3.0 * px)) * raised col = col + (vec3(0.984, 0.988, 1.0) - nm_ground()) * max(sh, 0.0) * 0.7 col = col + (vec3(0.745, 0.776, 0.831) - nm_ground()) * max(-sh, 0.0) * 0.45 // The tray: a step down, the page's shadow falling in deep along its top // and left and light gathering along its bottom and right, with no lip. let tray = face - vec3(0.015, 0.015, 0.015) let dk = Finish.inset(pq, c, h, rr, vec2(2.4, 3.2), 3.6) let lt = Finish.inset(pq, c, h, rr, vec2(-1.6, -2.2), 2.8) var tcol = mix(tray, nm_slate_a(), clamp(dk * 0.7, 0.0, 1.0)) tcol = mix(tcol, vec3(1.0, 1.0, 1.0), clamp(lt * 0.6, 0.0, 1.0)) col = mix(col, tcol, sunken) col = col + vec3(nm_grain(org + pq, px)) let a = Finish.cover(d, px) return Finish.over(acc, vec4(col * a, a)) } mod.widgets.RoundedView.clip_x = false mod.widgets.RoundedView.clip_y = false mod.widgets.PanelView.draw_bg.vertex = fn() { let m = 12.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.PanelView.draw_bg.pixel = fn() { let m = 12.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let r = mix(12.0, 10.0, self.sunken) return nm_panel(pq, self.rect_size, self.rect_pos, px, self.color.rgb, r, 1.0 - self.sunken, self.sunken, 0.0) } mod.widgets.PanelView.draw_bg.color = #e9edf0 // A card is a raised panel too; a pressable one lifts under the pointer. // Elevated stands up and filled is a shallow tray; outlined lies flat on // the page with no line round it, and rises part of the way under the // pointer when it can be pressed. The shadows fade out inside the twelve // points the quad grows by. mod.widgets.Card.radius = 12.0 mod.widgets.Card.clip_x = false mod.widgets.Card.clip_y = false mod.widgets.Card.draw_bg.vertex = fn() { let m = 12.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.Card.draw_bg.pixel = fn() { let m = 12.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let lift = self.hover * (1.0 - self.down) let outlined = step(0.01, self.border_size) let rest = clamp(self.shadow_radius / 4.0, 0.0, 1.0) let raised = max(rest, 0.6 * lift) * (1.0 - self.down) let sunk = max((1.0 - rest) * (1.0 - outlined) * 0.6 * (1.0 - lift), 0.6 * self.down) let c = self.rect_size * 0.5 let r = min(self.border_radius, min(c.x, c.y)) return nm_kept_f(nm_panel(pq, self.rect_size, self.rect_pos, px, nm_ground(), self.border_radius, raised, sunk, lift), pq, c, c, r, m - 0.5, 3.5) } // The rounded view keeps whatever colour it is given; a view the colour of // the page is a surface of the page, and stands up off it as a panel does, // with the panel's own face and shadows, the shadows fading out inside the // ten points the quad grows by. A line is drawn only round a view that asks // for one. mod.widgets.RoundedView.draw_bg.vertex = fn() { let m = 10.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.RoundedView.draw_bg.pixel = fn() { let m = 10.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) var fill = self.color if self.color_2.x > -0.5 { let dir = if self.gradient_fill_horizontal > 0.5 self.pos.x else self.pos.y fill = mix(self.color, self.color_2, dir) } let lower = vec2(self.border_inset.x, self.border_inset.y) let upper = self.rect_size - vec2(self.border_inset.z, self.border_inset.w) let c = (lower + upper) * 0.5 let h = max((upper - lower) * 0.5, vec2(0.5, 0.5)) // The corner as the stock face draws it, so a view keeps its shape. let r = min(2.0 * max(1.0, self.border_radius), min(h.x, h.y)) let d = Material.sd_box(pq, c, h, r) let near = (1.0 - smoothstep(0.02, 0.06, abs(Finish.lum(fill.rgb) - 0.927))) * step(0.99, fill.w) let a = Finish.cover(d, px) var acc = vec4(fill.rgb * a, a) if near > 0.001 { acc = nm_kept_f(nm_panel(pq - lower, h * 2.0, self.rect_pos + lower, px, fill.rgb, r, near, 0.0, 0.0), pq, c, h, r, m - 0.5, 3.0) } acc = acc * fill.w let bd = Finish.band(d, 0.0, max(self.border_size, 0.0), px) * step(0.001, self.border_size) * self.border_color.w return Finish.over(acc, vec4(self.border_color.rgb * bd, bd)) } // LIST ROWS. Flat on their list: no shadow, no well and no line between // rows. Under the pointer a faint band of slate, held a breath deeper; the // chosen row a faint band of the accent's tone. A band is a tint laid over // whatever the list stands on, four points in from the row's ends and two // from its top and bottom, with a 10 point corner. mod.widgets.ListItem.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = max(self.rect_size * 0.5 - vec2(4.0, 2.0), vec2(1.0, 1.0)) let r = min(10.0, min(h.x, h.y)) let d = Material.sd_box(p, c, h, r) let en = 1.0 - self.disabled let cov = Finish.cover(d, px) let ca = 0.085 * self.selected * mix(1.0, 0.5, self.disabled) * cov let sa = (0.045 * self.hover + 0.035 * self.down) * en * cov let acc = vec4(vec3(0.290, 0.486, 0.965) * ca, ca) return Finish.over(acc, vec4(vec3(0.388, 0.427, 0.502) * sa, sa)) } mod.widgets.ListItem.draw_text.color = #4f5666 // THE OPEN LIST. A raised panel with a 12 point corner. It floats over // everything else, so its quad grows by sixteen points and the panel's // shadows fade out inside them. The row under the pointer is a faint band // of slate with an 8 point corner; the chosen row carries its label in the // accent ink and a check at its right end. mod.widgets.PopupMenuFlat.draw_bg.vertex = fn() { let m = 16.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.PopupMenuFlat.draw_bg.pixel = fn() { let m = 16.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let r = min(12.0, min(c.x, c.y)) return nm_kept_f(nm_panel(pq, self.rect_size, self.rect_pos, px, nm_ground(), 12.0, 1.0, 0.0, 0.0), pq, c, c, r, m - 0.5, 4.0) } mod.widgets.PopupMenuFlat.clip_x = false mod.widgets.PopupMenuFlat.clip_y = false mod.widgets.PopupMenuItem.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let h = max(self.rect_size * 0.5 - vec2(0.0, 1.0), vec2(1.0, 1.0)) let r = min(8.0, min(h.x, h.y)) let d = Material.sd_box(p, c, h, r) let sa = 0.045 * self.hover * (1.0 - self.disabled) * Finish.cover(d, px) let acc = vec4(vec3(0.388, 0.427, 0.502) * sa, sa) let cx = self.rect_size.x - 12.0 let cy = c.y let ck = Finish.cover(min(nm_seg(p, vec2(cx - 3.5, cy), vec2(cx - 1.0, cy + 2.5)), nm_seg(p, vec2(cx - 1.0, cy + 2.5), vec2(cx + 3.5, cy - 2.5))) - 0.75, px) * self.active return Finish.over(acc, vec4(vec3(0.290, 0.486, 0.965) * ck, ck)) } mod.widgets.PopupMenuItem.draw_text.color_active = #3860d6 // Templates that draw a face of their own on top of these roots keep the // plain quad: the grown one is only for the faces written here. mod.widgets.CloseButton.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.CopyButton.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.BurgerButton.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.ButtonFloating.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.ButtonFloatingSm.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.CheckBoxCustom.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.glass.CutButton.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } mod.widgets.glass.RadioButton.draw_bg.vertex = fn() { self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size) } // INSTRUMENTS. The displays, lamps and hardware switches are the same pale // material as the controls round them: a display is a window pressed into // the page, a lamp a small raised dome on it, and a switch a pill well with // a raised round key in it. None of them draws a line: every edge is light // or shade on a shoulder. // The well an instrument is cut into, a step under the page and a hair // cooler: the shade of a screen's face (`theme.color_screen`) and of a // switch's slot. let nm_well_shade_g = fn() -> vec3 { return vec3(0.886, 0.902, 0.925) } // A window pressed into the page, `base` its face: the page's shadow falls // in from the top left and light gathers at the bottom right, as deep as // the sheet's wells go and held to the window's own scale up to 40 points, // so a tall screen does not take a panel's shadow. `depth` 1 is the stock // recess of 4 points, 0 a face flush with the page. let nm_window_g = fn(base: vec3, p: vec2, c: vec2, h: vec2, r: float, d: float, px: float, depth: float) -> vec3 { let s = clamp(2.0 * min(h.x, h.y), 16.0, 40.0) return nm_well(base, p, c, h, r, d, px, s, 1.15 * clamp(depth, 0.0, 1.5)) } // An accent fill lying in a well: the page's shadow still falls in across // its top left and light still gathers at its bottom right, in the fill's // own colour, so a lit well sits as deep as an unlit one. `s` is the well's // own height. let nm_fill_well_g = fn(fill: vec3, p: vec2, c: vec2, h: vec2, r: float, s: float) -> vec3 { let dk = Finish.inset(p, c, h, r, vec2(0.04 * s, 0.06 * s), max(0.075 * s, 0.9)) let lt = Finish.inset(p, c, h, r, vec2(-0.03 * s, -0.05 * s), max(0.07 * s, 0.8)) let o = fill * (1.0 - 0.28 * dk) return mix(o, min(fill * 1.16, vec3(1.0, 1.0, 1.0)), lt * 0.45) } // A small dome of colour `face` over the box centred on `c` with half size // `h` and round ends: lighter toward the top left where the light falls, // deeper toward the bottom right, its shoulder rolling into light on the // one side and into shade on the other. Soft all over: no rim, no glint. let nm_dome_g = fn(face: vec3, p: vec2, c: vec2, h: vec2) -> vec3 { let ex = max(h.x - h.y, 0.0) let dx = p.x - c.x let q = vec2(dx - clamp(dx, 0.0 - ex, ex), p.y - c.y) / max(h.y, 0.5) // -1 at the top left, toward the light, to 1 at the bottom right. let g = clamp((q.x + q.y) * 0.7071, -1.0, 1.0) let shoulder = smoothstep(0.4, 1.0, length(q)) var col = min(face * (1.0 - 0.07 * g), vec3(1.0, 1.0, 1.0)) col = mix(col, face * 0.82, shoulder * smoothstep(-0.2, 0.9, g) * 0.6) col = mix(col, min(face + vec3(0.12, 0.12, 0.12), vec3(1.0, 1.0, 1.0)), shoulder * (1.0 - smoothstep(-0.9, 0.2, g)) * 0.5) return col } // SCREENS. A display is a window let into the page, not a pane of glass in // a bezel: no ring, no frame and no lip, only the well itself, its face the // screen's shade and its corner 10 points, and what it holds sits in it in // slate. `recess` still sets how deep it lies, and scan lines still draw. mod.widgets.ScreenView.draw_bg.border_radius = uniform(10.0) mod.widgets.ScreenView.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let lo = vec2(Finish.snap(org.x, px), Finish.snap(org.y, px)) - org let hi = vec2(Finish.snap(org.x + self.rect_size.x, px), Finish.snap(org.y + self.rect_size.y, px)) - org let c = (lo + hi) * 0.5 let h = (hi - lo) * 0.5 let r = min(self.border_radius, min(h.x, h.y)) let d = Material.sd_box(p, c, h, r) var face = self.color.rgb if self.scan > 0.0 { face = face * Finish.scan(p.y - (c.y - h.y), px, 3.0 * px, self.scan) } var col = nm_window_g(face, p, c, h, r, d, px, self.recess / 4.0) col = col + vec3(nm_grain(org + p, px)) let a = Finish.cover(d, px) * self.color.a return vec4(col * a, a) } // The glass keeps only its soft diagonal band, at the strength a page asks // for and none by default; the light line the stock laid inside its top // edge goes with the frame. mod.widgets.ScreenView.sheen = 0.0 mod.widgets.ScreenView.draw_glass.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let c = self.rect_size * 0.5 let d = Material.sd_box(p, c, c, min(self.face_radius, min(c.x, c.y))) let u = p / max(self.rect_size, vec2(1.0, 1.0)) let diag = u.x + u.y * 0.6 let band = smoothstep(0.0, 0.12, diag) * (1.0 - smoothstep(0.28, 0.5, diag)) let a = clamp(self.sheen * band, 0.0, 1.0) * Finish.cover(d + px, px) return vec4(a, a, a, a) } // THE NEEDLE METER. Its face is a window like a screen's, with a 10 point // corner and the scale printed on it in slate: a tick at every division, // the majors two device pixels wide and the minors one and paler, a faint // arc under them, and the red zone a band just outside the arc. The needle // is slate, a point wide, with a soft shadow on the face under it, and its // root stands under a small raised dome of the page that throws the // sheet's two shadows. No rim and no sheen. mod.widgets.NeedleMeter.draw_bg.border_radius = uniform(10.0) mod.widgets.NeedleMeter.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let lo = vec2(Finish.snap(org.x, px), Finish.snap(org.y, px)) - org let hi = vec2(Finish.snap(org.x + self.rect_size.x, px), Finish.snap(org.y + self.rect_size.y, px)) - org let c = (lo + hi) * 0.5 let hc = (hi - lo) * 0.5 let fr = min(self.border_radius, min(hc.x, hc.y)) let face_d = Material.sd_box(p, c, hc, fr) let pv = self.pivot() let rr = self.scale_radius() let q = p - pv let r = length(q) let ang = atan2(q.x, 0.0 - q.y) let sweep = self.sweep * 0.017453293 let t = ang / sweep + 0.5 let on_scale = step(-0.0001, t) * step(t, 1.0001) var col = self.color.rgb // The scale: a tick at every division over a faint arc. let arc = Finish.cover(abs(r - rr) - px * 0.5, px) * on_scale let n = max(floor(self.majors + 0.5), 1.0) * max(floor(self.minors + 0.5), 1.0) let k = floor(clamp(t, 0.0, 1.0) * n + 0.5) let ak = (k / n - 0.5) * sweep let perp = abs(q.x * cos(ak) + q.y * sin(ak)) let mn = max(floor(self.minors + 0.5), 1.0) let major = 1.0 - step(0.5, k - mn * floor(k / mn + 0.0001)) let len = mix(self.tick_minor, self.tick_major, major) let radial = max(rr - len - r, r - rr) let tick = Finish.cover(perp - px * mix(0.5, 1.0, major), px) * Finish.cover(radial, px) * step(0.0, -q.y * cos(ak) + q.x * sin(ak)) col = mix(col, self.color_scale.rgb, max(arc * 0.4, tick * mix(0.6, 1.0, major))) // The red zone: a band just outside the arc from `zone` on. if self.zone < 0.999 { let zone_t = Finish.cover((self.zone - t) * sweep * r, px) * Finish.cover((t - 1.0) * sweep * r, px) let zone_r = Finish.cover(max(rr + px - r, r - rr - 2.5), px) col = mix(col, self.color_zone.rgb, zone_t * zone_r * 0.85) } // The window over the printed face. col = nm_window_g(col, p, c, hc, fr, face_d, px, 1.0) // The needle, from under the pivot dome to just past the scale, and its // soft shadow a point down and to the right of it. let na = self.needle_angle(self.value) let nd = vec2(sin(na), 0.0 - cos(na)) let tail = max(self.pivot_size - px, 0.0) let sq = q - vec2(0.8, 1.2) let s_along = dot(sq, nd) let s_across = abs(sq.x * nd.y - sq.y * nd.x) let shade = (1.0 - smoothstep(0.0, 1.6, s_across)) * smoothstep(-1.0, 1.0, rr + 3.0 - s_along) * smoothstep(-1.0, 1.0, s_along + tail) col = col * (1.0 - 0.12 * shade) let along = dot(q, nd) let across = abs(q.x * nd.y - q.y * nd.x) let needle = Finish.cover(across - 0.5, px) * Finish.cover(along - (rr + 3.0), px) * Finish.cover(0.0 - along - tail, px) col = mix(col, self.color_needle.rgb, needle) // The pivot: a dome of the page over the needle's root, and its shadows. let pr = max(self.pivot_size, 1.0) let ph = vec2(pr, pr) let thrown = nm_cast(p, pv, ph, pr, max(2.0 * pr + 2.0, 8.0), 1.0) col = col * (1.0 - thrown.a) + thrown.rgb let pd = r - pr col = mix(col, nm_dome_g(nm_ground() + vec3(0.01, 0.01, 0.01), p, pv, ph), Finish.cover(pd, px)) col = col + vec3(nm_grain(org + p, px)) let a = Finish.cover(face_d, px) * self.color.a return vec4(col * a, a) * self.opacity } // LAMPS. A lamp is a small raised dome on the page: out, a dome of the page // itself with the faintest trace of the colour it would light; lit, the // same dome filled with its intent's colour; half lit, half way between. A // bar is the dome drawn out into a pill. Either throws the sheet's two // shadows, so the quad grows 6 points round the lens and the lamp keeps 3 // of them inside its own box as padding: two lamps set edge to edge stand // 6 points apart, and each one's shadow is gone before the other's dome. A // halo a page asks for is drawn as the stock draws it, in the room the lamp // then takes for it. mod.widgets.Lamp.padding = mod.turtle.Inset{left: 3.0 right: 3.0 top: 3.0 bottom: 3.0} mod.widgets.Lamp.draw_bg.bar_radius = uniform(16.0) mod.widgets.Lamp.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.Lamp.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) // The lens in the middle of the lamp's box, its thickness read back // from the room the box holds for a halo, as the stock reads it. let reach = clamp(self.reach, 0.0, 1.0) let t = self.rect_size.y / (1.0 + 2.0 * reach) let room = t * reach let c = self.rect_size * 0.5 let half_len = max((self.rect_size.x - 2.0 * room) * 0.5, t * 0.5) let hb = vec2(mix(t * 0.5, half_len, self.bar), t * 0.5) let corner = mix(t * 0.5, min(self.bar_radius, t * 0.5), self.bar) let d = Material.sd_box(pq, c, hb, corner) let lit = clamp(self.lit, 0.0, 1.0) let tone = self.intent_color() let ink = tone.rgb let glow = max(self.halo_at(d, t, px, self.halo * lit, reach), 0.0) var acc = self.halo_light(ink, glow) acc = Finish.over(acc, nm_cast(pq, c, hb, corner, max(1.25 * t, 6.0), 1.0)) let unlit = mix(nm_ground(), ink, self.lens_tint * 0.3) var col = nm_dome_g(mix(unlit, ink, lit), pq, c, hb) col = col + vec3(nm_grain(self.rect_pos + pq, px)) let a = Finish.cover(d, px) acc = Finish.over(acc, vec4(col * a, a)) return acc * self.opacity } // SWITCHES. Both hardware switches are a pill well the switch's height with // a raised round key in it, the key's shadows kept inside the well; on, the // well fills with the accent as a toggle's does. The rocker's key carries // the side in force, a slate O at the left while off and the I in the // accent ink at the right while on, and crosses on a click with the state's // own short ease; it is never dragged. The slide switch's key follows a // drag, and the end of the slot it has left holds a small mark: a quiet // slate dot while off, a white one on the fill while on. The quad grows 6 // points for the focus mark, and the label stands 8 points clear of the // well, as it does beside a toggle. // One switch: the pill `sz` at the start of the row, or at its end, its key // `inset` points in from the well's edge and `t` along its travel. `slide` // is 1 for the slide switch's mark in the slot, 0 for the rocker's legend // on the key. let nm_switch_g = fn(pq: vec2, size: vec2, org: vec2, px: float, sz: vec2, at_end: float, inset: float, t: float, hover: float, down: float, focus: float, active: float, disabled: float, error: float, slide: float) -> vec4 { let o0 = vec2((size.x - sz.x) * at_end, size.y * 0.5 - sz.y * 0.5) let lo = vec2(Finish.snap(org.x + o0.x, px), Finish.snap(org.y + o0.y, px)) - org let hi = vec2(Finish.snap(org.x + o0.x + sz.x, px), Finish.snap(org.y + o0.y + sz.y, px)) - org let tc = (lo + hi) * 0.5 let th = (hi - lo) * 0.5 let td = Material.sd_box(pq, tc, th, th.y) let ta = Finish.cover(td, px) let en = 1.0 - disabled let dis = mix(1.0, 0.35, disabled) // The well, the accent coming into it as the switch turns on. var col = nm_well(nm_well_shade_g(), pq, tc, th, th.y, td, px, 2.0 * th.y, (1.1 + 0.3 * hover) * dis) let fillc = mix(mix(vec3(0.290, 0.486, 0.965), vec3(0.741, 0.227, 0.192), error), vec3(0.725, 0.776, 0.902), disabled) col = mix(col, nm_fill_well_g(fillc, pq, tc, th, th.y, 2.0 * th.y), active) // The slide's mark, in the end of the slot the key has left. let free = vec2(mix(lo.x + 2.0 * th.x - th.y, lo.x + th.y, t), tc.y) let mark = Finish.cover(length(pq - free) - sz.y * 0.15, px) * slide * mix(0.3, 0.92, active) * dis col = mix(col, mix(vec3(0.388, 0.416, 0.478), vec3(1.0, 1.0, 1.0), active), mark) col = col + vec3(nm_grain(org + pq, px)) var acc = vec4(col * ta, ta) // The key, and what it throws on the well. let kr = max(th.y - inset, 2.0) let kc = vec2(lo.x + th.y + 2.0 * (th.x - th.y) * t, tc.y) let kh = vec2(kr, kr) let kd = length(pq - kc) - kr acc = Finish.over(acc, nm_cast(pq, kc, kh, kr, 2.0 * kr, (1.0 + 0.3 * hover) * dis) * ta) var kcol = nm_face(pq, kc, kh, kd, px, 0.018 * hover * en - 0.01 * down * en, en) // The rocker's legend on the key: the O fading out as the I comes in. let lq = pq - kc let ring = Finish.cover(abs(length(lq) - kr * 0.34) - 0.55, px) * (1.0 - active) let bar = Finish.cover(max(abs(lq.x) - 0.55, abs(lq.y) - kr * 0.38), px) * active let ink = mix(mix(vec3(0.388, 0.416, 0.478), vec3(0.220, 0.376, 0.839), active), vec3(0.725, 0.776, 0.902), disabled) kcol = mix(kcol, ink, (ring + bar) * (1.0 - slide)) kcol = kcol + vec3(nm_grain(org + pq, px)) let ka = Finish.cover(kd, px) acc = Finish.over(acc, vec4(kcol * ka, ka)) return Finish.over(acc, nm_focus(td, px, focus * en)) } mod.widgets.ToggleRocker.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.ToggleRocker.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let sz = vec2(self.size * self.pill_aspect, self.size) return nm_switch_g(pq, self.rect_size, self.rect_pos, px, sz, self.mark_at_end, self.border_size + self.knob_inset, self.active, self.hover, self.down, self.focus, self.active, self.disabled, self.error, 0.0) } mod.widgets.ToggleRocker.label_walk.margin.left = 44.0 mod.widgets.ToggleSlide.draw_bg.vertex = fn() { let m = 6.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.ToggleSlide.draw_bg.pixel = fn() { let m = 6.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let sz = vec2(self.size * self.pill_aspect, self.size) let t = mix(self.active, self.drag_pos, self.drag) return nm_switch_g(pq, self.rect_size, self.rect_pos, px, sz, self.mark_at_end, self.border_size + self.knob_inset, t, self.hover, self.down, self.focus, self.active, self.disabled, self.error, 1.0) } mod.widgets.ToggleSlide.label_walk.margin.left = 44.0 // THE PAGE. The window's own ground: the slate-white, a long gradient a // quarter as strong as a studio shot's, and the shared grain. mod.widgets.Window.show_bg = true mod.widgets.Window.draw_bg.pixel = fn() { let p = self.pos * self.rect_size let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let t = (self.pos.x + self.pos.y) * 0.5 let g = 0.008 - 0.016 * t return vec4(nm_ground() + vec3(g, g, g) + vec3(nm_grain(self.rect_pos + p, px)), 1.0) } // A ROW OF FIELDS IS ONE HEIGHT. Left to themselves the library's fields each // come out at a size of their own -- in the widget catalogue's toolbar the // search box, the number beside it and the drop down after it stood at three // different heights -- and a sheet that reshapes the text box alone widens // that gap instead of closing it. So the box metrics are stated ONCE here and // handed to each field below, and a field added to the library later joins // the row by being named here rather than by having these numbers copied // into it. // Twenty-six, the same row the buttons stand in. let field_height = 26.0 let field_pad_v = 5.0 let field_radius = 6.0 // No borders: the relief draws the edge. let field_border = 0.0 // The room a Fit field takes. A minimum height rather than a height, // because a line of text is shorter than a row, and the padding with it: // where a field is Fit, the padding IS its height, and two fields padded // differently part company again the moment the type grows. 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, and because writing a corner onto a // draw struct that has no corner would add a slot its shader never reads. 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: a number // with a stepper, a date, a time. The height is all these take. A padding // here would NOT hold the line off the box -- each of them measures its // parts against the turtle's whole rect and draws them from the content // origin, so the padding displaces the line instead of insetting it, and the // number came out sitting on the bottom edge with its arrows split around it. 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) // The number's box strokes its own hairline and declares no border size. mod.widgets.NumberField.draw_bg.border_radius = field_radius // The drag-number's corner is written into its own shader, so it takes the // room and keeps the box it draws. field_frame(mod.widgets.ValueInput) field_frame(mod.widgets.DateField) field_frame(mod.widgets.TimeField) // A picker is a date field in an anchor: the field inside fills the frame. field_frame(mod.widgets.DatePicker) field_frame(mod.widgets.DateRangePicker) // The chrome-less input a well holds must impose no height of its own. It is // a TextInput, so it inherits the minimum above, and a minimum as tall as the // whole field, applied inside a well already that tall, pushed the line out // through the bottom of the box. mod.widgets.WellInput.min_height = 0.0 mod.widgets.Window.pass.clear_color = #e9edf0 // ---------------------------------------------------------------- band // THE BAND is the sheet's own slider with two thumbs: the same track, the // same lit value and the same thumb as the Slider, the value lit between // the two thumbs, its quad grown as the slider's is so the thumbs keep // their shape. The widget's grab geometry is set to the thumbs' own, so // what is drawn is what is dragged. mod.widgets.RangeSliderFlat.clip_x = false mod.widgets.RangeSliderFlat.clip_y = false mod.widgets.RangeSliderFlat.track_inset = 1.0 mod.widgets.RangeSliderFlat.handle_size = 16.0 mod.widgets.RangeSliderFlat.draw_bg.vertex = fn() { let m = 8.0 self.vertex_pos = self.clip_and_transform_vertex(self.rect_pos - vec2(m, m), self.rect_size + vec2(2.0 * m, 2.0 * m)) } mod.widgets.RangeSliderFlat.draw_bg.pixel = fn() { let m = 8.0 let pq = self.pos * (self.rect_size + vec2(2.0 * m, 2.0 * m)) - vec2(m, m) let px = 1.0 / max(self.draw_pass.dpi_factor, 0.5) let org = self.rect_pos let dis = mix(1.0, 0.35, self.disabled) let gr = vec3(nm_grain(org + pq, px)) let top = self.label_px let cy = Finish.snap(org.y + top + (self.rect_size.y - top) * 0.5, px) - org.y let hr = 8.0 let x0 = hr + 1.0 let x1 = self.rect_size.x - hr - 1.0 let gc = vec2(self.rect_size.x * 0.5, cy) let gh = vec2(self.rect_size.x * 0.5 - 1.0, 4.0) let ga = Finish.cover(Material.sd_box(pq, gc, gh, 4.0), px) let col = nm_groove(vec3(0.878, 0.898, 0.925), pq, gc, gh, px, dis) + gr let ink = mix(vec3(0.290, 0.486, 0.965), vec3(0.725, 0.776, 0.902), self.disabled) let fcol = nm_fill_e(ink, pq, gc, gh, 4.0, 8.0, dis) + gr let fa = nm_span_e(pq, gc, gh, x0, x1, self.end_pos, self.start_pos, 1.0, px) let far = nm_disc_e(vec4(col * ga, ga), vec4(fcol * fa, fa), pq, org, vec2(mix(x0, x1, self.end_pos), cy), hr, 2.5 * hr, 1.0, px, self.hot_end, self.drag * self.hot_end, self.focus, self.disabled) return nm_disc_e(far, vec4(0.0, 0.0, 0.0, 0.0), pq, org, vec2(mix(x0, x1, self.start_pos), cy), hr, 2.5 * hr, 1.0, px, self.hot_start, self.drag * self.hot_start, self.focus, self.disabled) } true