// Command xfwm4gen renders the Das OS xfwm4 window decorations. // // xfwm4 themes are a directory of image slices plus a themerc file. Drawing // ~90 slices by hand is error-prone, so they are generated from one palette: // // xfwm4gen -variant light -out build/DasOS/xfwm4 package main import ( "flag" "fmt" "image" "image/color" "image/png" "log" "math" "os" "path/filepath" "strings" ) const ( titleHeight = 32 frameWidth = 3 cornerRadius = 8.0 cornerWidth = 12 buttonWidth = 30 circleRadius = 11.0 glyphHalf = 4.0 strokeWidth = 1.5 samples = 4 // supersampling per axis, gives 16 samples per pixel ) type palette struct { titleBg, border, separator, glyph, text color.NRGBA } type variant struct { active, inactive palette hover, pressed color.NRGBA closeHover, closePressed, onClose color.NRGBA } var variants = map[string]variant{ "light": { active: palette{ titleBg: rgb(0xe9ecf1), border: rgb(0xb4bcc9), separator: rgb(0xd3d8e1), glyph: rgb(0x2e3440), text: rgb(0x1f2430), }, inactive: palette{ titleBg: rgb(0xf4f5f8), border: rgb(0xccd2db), separator: rgb(0xe2e5eb), glyph: rgb(0x9aa1ad), text: rgb(0x8a909c), }, hover: rgb(0xd5dae3), pressed: rgb(0xc3cad6), closeHover: rgb(0xe0464c), closePressed: rgb(0xc2353b), onClose: rgb(0xffffff), }, "dark": { active: palette{ titleBg: rgb(0x2b2f38), border: rgb(0x0e1014), separator: rgb(0x1d2026), glyph: rgb(0xe3e6ec), text: rgb(0xeceff4), }, inactive: palette{ titleBg: rgb(0x24272e), border: rgb(0x15171b), separator: rgb(0x1b1d22), glyph: rgb(0x7c8391), text: rgb(0x8a909c), }, hover: rgb(0x3b404b), pressed: rgb(0x4a505d), closeHover: rgb(0xe0464c), closePressed: rgb(0xc2353b), onClose: rgb(0xffffff), }, } func rgb(v uint32) color.NRGBA { return color.NRGBA{R: uint8(v >> 16), G: uint8(v >> 8), B: uint8(v), A: 0xff} } func hex(c color.NRGBA) string { return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B) } // shader returns the colour of a point in pixel coordinates; transparent outside the shape. type shader func(x, y float64) color.NRGBA // render rasterises a shader with supersampling, averaging in premultiplied space. func render(w, h int, fn shader) *image.NRGBA { img := image.NewNRGBA(image.Rect(0, 0, w, h)) const n = samples * samples for py := 0; py < h; py++ { for px := 0; px < w; px++ { var r, g, b, a float64 for sy := 0; sy < samples; sy++ { for sx := 0; sx < samples; sx++ { c := fn(float64(px)+(float64(sx)+0.5)/samples, float64(py)+(float64(sy)+0.5)/samples) ca := float64(c.A) / 255 r += float64(c.R) * ca g += float64(c.G) * ca b += float64(c.B) * ca a += ca } } if a == 0 { continue } img.SetNRGBA(px, py, color.NRGBA{ R: uint8(math.Round(r / a)), G: uint8(math.Round(g / a)), B: uint8(math.Round(b / a)), A: uint8(math.Round(a / n * 255)), }) } } return img } // titleRow is the colour of the title bar at height y: top border, fill, separator above the client. func titleRow(p palette, y float64) color.NRGBA { switch { case y < 1: return p.border case y >= titleHeight-1: return p.separator default: return p.titleBg } } // insideRounded reports whether (x, y) lies in a shape whose top-left corner is // rounded with radius r at origin (ox, oy) and which extends right and down. func insideRounded(x, y, ox, oy, r float64) bool { if x < ox || y < oy { return false } if x < ox+r && y < oy+r { return math.Hypot(x-(ox+r), y-(oy+r)) <= r } return true } func topLeft(p palette) shader { return func(x, y float64) color.NRGBA { switch { case !insideRounded(x, y, 0, 0, cornerRadius): return color.NRGBA{} case !insideRounded(x, y, 1, 1, cornerRadius-1): return p.border case x < frameWidth: return p.titleBg default: return titleRow(p, y) } } } func mirror(fn shader, w int) shader { return func(x, y float64) color.NRGBA { return fn(float64(w)-x, y) } } func leftEdge(p palette) shader { return func(x, _ float64) color.NRGBA { if x < 1 { return p.border } return p.titleBg } } func bottomEdge(p palette) shader { return func(_, y float64) color.NRGBA { if y >= frameWidth-1 { return p.border } return p.titleBg } } func bottomLeft(p palette) shader { return func(x, y float64) color.NRGBA { if x < 1 || y >= frameWidth-1 { return p.border } return p.titleBg } } type segment struct{ x1, y1, x2, y2 float64 } // glyph is a button symbol drawn around the button centre. type glyph struct { lines []segment ring float64 // stroked circle radius, 0 for none dot float64 // filled circle radius, 0 for none } func square(x1, y1, x2, y2 float64) []segment { return []segment{{x1, y1, x2, y1}, {x2, y1, x2, y2}, {x2, y2, x1, y2}, {x1, y2, x1, y1}} } var glyphs = func() map[string]glyph { const g = glyphHalf return map[string]glyph{ "close": {lines: []segment{{-g, -g, g, g}, {-g, g, g, -g}}}, "hide": {lines: []segment{{-g, 3, g, 3}}}, "maximize": {lines: square(-g, -g, g, g)}, "maximize-toggled": {lines: append(square(-g, -g+2.5, g-2.5, g), segment{-g + 2.5, -g, g, -g}, segment{g, -g, g, g - 2.5})}, "menu": {lines: []segment{{-g, -3, g, -3}, {-g, 0, g, 0}, {-g, 3, g, 3}}}, "shade": {lines: []segment{{-g, 2, 0, -2}, {0, -2, g, 2}}}, "shade-toggled": {lines: []segment{{-g, -2, 0, 2}, {0, 2, g, -2}}}, "stick": {ring: g}, "stick-toggled": {dot: g}, } }() func distToSegment(px, py float64, s segment) float64 { dx, dy := s.x2-s.x1, s.y2-s.y1 t := ((px-s.x1)*dx + (py-s.y1)*dy) / (dx*dx + dy*dy) t = math.Max(0, math.Min(1, t)) return math.Hypot(px-(s.x1+t*dx), py-(s.y1+t*dy)) } func (g glyph) covers(x, y float64) bool { for _, s := range g.lines { if distToSegment(x, y, s) <= strokeWidth/2 { return true } } r := math.Hypot(x, y) if g.ring > 0 && math.Abs(r-g.ring) <= strokeWidth/2 { return true } return g.dot > 0 && r <= g.dot } func button(p palette, g glyph, circle, ink color.NRGBA) shader { cx, cy := buttonWidth/2.0, titleHeight/2.0 return func(x, y float64) color.NRGBA { dx, dy := x-cx, y-cy switch { case g.covers(dx, dy): return ink case circle.A != 0 && math.Hypot(dx, dy) <= circleRadius: return circle default: return titleRow(p, y) } } } type piece struct { name string w, h int fn shader } func pieces(v variant) []piece { var out []piece for _, state := range []string{"active", "inactive"} { p := v.active if state == "inactive" { p = v.inactive } out = append(out, piece{"top-left-" + state, cornerWidth, titleHeight, topLeft(p)}, piece{"top-right-" + state, cornerWidth, titleHeight, mirror(topLeft(p), cornerWidth)}, piece{"left-" + state, frameWidth, 1, leftEdge(p)}, piece{"right-" + state, frameWidth, 1, mirror(leftEdge(p), frameWidth)}, piece{"bottom-" + state, 1, frameWidth, bottomEdge(p)}, piece{"bottom-left-" + state, frameWidth, frameWidth, bottomLeft(p)}, piece{"bottom-right-" + state, frameWidth, frameWidth, mirror(bottomLeft(p), frameWidth)}, ) for i := 1; i <= 5; i++ { out = append(out, piece{fmt.Sprintf("title-%d-%s", i, state), 1, titleHeight, func(_, y float64) color.NRGBA { return titleRow(p, y) }}) } } for name, g := range glyphs { hover, pressed, ink := v.hover, v.pressed, v.active.glyph if name == "close" { hover, pressed, ink = v.closeHover, v.closePressed, v.onClose } out = append(out, piece{name + "-active", buttonWidth, titleHeight, button(v.active, g, color.NRGBA{}, v.active.glyph)}, piece{name + "-inactive", buttonWidth, titleHeight, button(v.inactive, g, color.NRGBA{}, v.inactive.glyph)}, piece{name + "-prelight", buttonWidth, titleHeight, button(v.active, g, hover, ink)}, piece{name + "-pressed", buttonWidth, titleHeight, button(v.active, g, pressed, ink)}, ) } return out } func themerc(v variant) string { return strings.Join([]string{ "# Generated by xfwm4gen, do not edit.", "active_text_color=" + hex(v.active.text), "inactive_text_color=" + hex(v.inactive.text), "button_offset=4", "button_spacing=0", "full_width_title=true", "maximized_offset=0", "show_app_icon=true", "title_alignment=center", "title_horizontal_offset=0", "title_shadow_active=false", "title_shadow_inactive=false", "title_vertical_offset_active=0", "title_vertical_offset_inactive=0", "", }, "\n") } func writePNG(path string, img image.Image) error { f, err := os.Create(path) if err != nil { return err } if err := png.Encode(f, img); err != nil { f.Close() return fmt.Errorf("encode %s: %w", path, err) } return f.Close() } func generate(v variant, dir string) error { if err := os.MkdirAll(dir, 0o755); err != nil { return err } for _, p := range pieces(v) { if err := writePNG(filepath.Join(dir, p.name+".png"), render(p.w, p.h, p.fn)); err != nil { return err } } return os.WriteFile(filepath.Join(dir, "themerc"), []byte(themerc(v)), 0o644) } func main() { name := flag.String("variant", "light", "colour variant: light or dark") out := flag.String("out", "", "output directory (the theme's xfwm4 folder)") flag.Parse() v, ok := variants[*name] if !ok || *out == "" { flag.Usage() os.Exit(2) } if err := generate(v, *out); err != nil { log.Fatalf("xfwm4gen: %v", err) } }