""" Themes: a palette, and a drawing style for the pad. Most themes only change colours. Two change how the pad is drawn: Blueprint an engineering drawing: white linework on cyanotype blue, a faint grid, unknown keys as dashed hidden lines, chain lines through the knob centres, and a title block in the corner Sketch pencil on paper: lines that wander a little and are gone over twice, as a hand would, in a handwriting font Nord and Catppuccin use their published palettes, both MIT licensed. """ from dataclasses import dataclass, field, replace from pathlib import Path @dataclass(frozen=True) class Metrics: """ The pad's proportions, in board units. The view scales the whole board to fit, so only the ratios between these matter. Knobs North's values come from its Figma file: each one is the design's measurement divided by the design's 291.67 unit key and multiplied by our 96, so the proportions are exactly the designer's. """ key: float = 96.0 # a key tile, square gap_x: float = 16.0 # between key columns gap_y: float = 16.0 # between key rows margin: float = 30.0 # board edge to anything key_radius: float = 5.0 r_out: float = 54.0 # outer reach of a knob's arcs r_in: float = 40.0 r_knob: float = 30.0 # the knob body row_h: float = 22.0 # one legend row row_gap: float = 12.0 # knob bottom to first legend row row_sep: float = 0.0 # hairline between rows, 0 for none dial_w: float = 160.0 # legend block width legend_radius: float = 3.0 rule_x: float = 0.0 # vertical rule inside the legend block label_x: float = 22.0 # where a legend's text starts byte_pt: float = 10.0 # action byte type size label_pt: float = 13.0 # key legend type size row_pt: float = 12.0 # knob legend type size row_byte_pt: float = 10.0 badge: float = 13.0 # the changed / unknown corner flag # Where a key's legend sits, as fractions of the tile: left, top, right, # bottom. Empty keeps the fixed insets that suit a 96 unit key. label_band: tuple = () byte_inset: tuple = () # same, for the action byte @dataclass(frozen=True) class Theme: key: str name: str dark: bool # palette, as hex window: str # behind everything board: str # the pad's face ink: str # lines and legends ink_dim: str # secondary text, unknown outlines hatch: str # hatching, grid, quiet lines accent: str # selection and unwritten changes accent_text: str # text on an accent filled button good: str bad: str line: str # widget borders field: str # text boxes # drawing style unknown: str = "hatch" # hatch, or "hidden": dashed outline grid: bool = False # a faint drawing grid behind the pad title_block: bool = False # sheet border and title block centre_lines: bool = False # chain lines through knob centres wobble: float = 0.0 # how far a pencil line wanders, board units caps: bool = False # legends in capitals, like drawing lettering font: str = "" # legend font; empty means the system font mono_legends: bool = False # draw legends in the monospace font badge: str = "tape" # pending marker: "tape" strip or "star" legend_box: bool = False # a border around each knob's three rows query_badge: bool = False # a "?" in the corner of an unknown key byte_prefix: bool = False # knob rows read 0x13 rather than 13 badge_glyph: str = "" # a character stamped in the corner flag byte_ink: str = "" # action byte grey; empty means ink_dim well: str = "" # gradient's far colour; empty means flat glow: float = 0.0 # neon bloom on accent strokes, board units knob_art: str = "" # "figma" draws the outline from vectors.py button_radius: int = 4 # window buttons and fields pill_buttons: bool = False # footer buttons outlined rather than bare outline_primary: bool = False # the write button outlined, not filled ui_pt: int = 0 # base type size; 0 keeps Qt's own title_pt: int = 18 # the inspector's heading mono_status: bool = False # footer status in the mono face metrics: Metrics = field(default_factory=Metrics) ui_font: str = "" # window chrome; empty means the system font mono_font: str = "" # window chrome, monospace parts notes: tuple = field(default=()) SILKSCREEN = Theme( "silkscreen", "Silkscreen", True, window="#17191b", board="#202326", ink="#d8d4cb", ink_dim="#72767b", hatch="#34383c", accent="#eb8a2f", accent_text="#1b1206", good="#8db36b", bad="#e0584f", line="#3a3e42", field="#121416") WHITE_BOARD = Theme( "white-board", "White board", False, window="#e4e2dc", board="#f7f6f2", ink="#232323", ink_dim="#86847e", hatch="#d6d2c8", accent="#cf6a17", accent_text="#ffffff", good="#3f7d20", bad="#c0392b", line="#cbc7bd", field="#ffffff") BLUEPRINT = Theme( "blueprint", "Blueprint", True, window="#123b6d", board="#123b6d", ink="#f2f5f8", ink_dim="#93b3d8", hatch="#285a92", accent="#ffd166", accent_text="#0b2545", good="#a8e6a1", bad="#ff9a8f", line="#3d6aa3", field="#0d2e57", unknown="hidden", grid=True, title_block=True, centre_lines=True, caps=True, mono_legends=True) SKETCH = Theme( "sketch", "Sketch", False, window="#f3efe6", board="#f3efe6", ink="#2e2b28", ink_dim="#8c857a", hatch="#b9b1a3", accent="#c8553d", accent_text="#ffffff", good="#4f7a36", bad="#b03a2e", line="#cfc6b6", field="#fbf9f4", wobble=1.6, font="Architects Daughter") NORD = Theme( "nord", "Nord", True, window="#2e3440", board="#3b4252", ink="#eceff4", ink_dim="#7b88a1", hatch="#434c5e", accent="#88c0d0", accent_text="#2e3440", good="#a3be8c", bad="#bf616a", line="#4c566a", field="#292e39") MOCHA = Theme( "catppuccin-mocha", "Catppuccin Mocha", True, window="#181825", board="#1e1e2e", ink="#cdd6f4", ink_dim="#7f849c", hatch="#313244", accent="#cba6f7", accent_text="#1e1e2e", good="#a6e3a1", bad="#f38ba8", line="#45475a", field="#11111b") LATTE = Theme( "catppuccin-latte", "Catppuccin Latte", False, window="#e6e9ef", board="#eff1f5", ink="#4c4f69", ink_dim="#8c8fa1", hatch="#ccd0da", accent="#8839ef", accent_text="#eff1f5", good="#40a02b", bad="#d20f39", line="#bcc0cc", field="#dce0e8") # Designed in Figma by a member of the community. The palette is Silkscreen's # own window and board with a hotter orange and neutral rather than warm greys, # so what makes it its own theme is mostly the drawing switches below. # :D -Oaken _F = 96.0 / 291.67 # Scale factor from the Figma file: its key tile is 291.67 units, ours is 96. KNOBS_NORTH = Theme( "knobs-north", "Knobs North", True, window="#17191b", board="#202326", ink="#ffffff", ink_dim="#b5b5b5", hatch="#2f2f2f", accent="#ff7700", accent_text="#ffffff", good="#87df9a", bad="#e0584f", line="#6e6e6e", field="#202326", badge="star", badge_glyph="*", legend_box=True, query_badge=True, byte_prefix=True, byte_ink="#505050", # tile and knob gradients, both #202326 falling to #131619 in the file well="#131619", glow=35 * _F, knob_art="figma", # "glow" value here was set way too low, making it not visible. Fixed. -Oaken font="Harmattan", mono_font="JetBrains Mono NL", ui_font="Harmattan", button_radius=7, pill_buttons=True, outline_primary=True, ui_pt=18, # ui_pt=15 was a little small, bumping slightly to 18pt. -Oaken title_pt=28, mono_status=True, metrics=Metrics( key=96.0, gap_x=88.0 * _F, gap_y=60.58 * _F, margin=74.0 * _F, key_radius=20.0 * _F, r_out=128.58 * _F, r_in=110.0 * _F, r_knob=92.5 * _F, row_h=75.0 * _F, row_gap=40.0 * _F, row_sep=2.0 * _F, dial_w=512.0 * _F, legend_radius=15.0 * _F, rule_x=73.0 * _F, label_x=94.0 * _F, byte_pt=24.0 * _F, label_pt=55.0 * _F, row_pt=40.0 * _F, row_byte_pt=20.0 * _F, badge=64.29 * _F, # The design sets line height to exactly the type size; Qt's is # looser, so the band is taller than the design's 100/300 while # staying centred on the same line. # Values changed to allow 2 lines to render per key with increased font size. Elipsis will show on second line instead of a third line. -Oaken label_band=(38 / 300, 0.10, 37 / 300, 0.01), # label_band=(38 / 300, 0.18, 37 / 300, 0.18) <-- old byte_inset=(19 / 300, 10 / 300)), notes=("Designed in Figma by a member of the community.", "Knobs along the top. North. Or east if you rotate...")) # Small tweak -Oaken # --------------------------------------------------------------- palettes # # A palette is colour and nothing else. Anything that decides how the pad is # drawn - proportions, badges, knob art, typefaces - belongs to the style. PALETTE_FIELDS = ("window", "board", "ink", "ink_dim", "hatch", "accent", "accent_text", "good", "bad", "line", "field", "well", "byte_ink", "dark") def _palette(theme, **over): """Pull a palette out of a theme written the old way.""" out = {f: getattr(theme, f) for f in PALETTE_FIELDS} out.update(over) return out PALETTES = { "graphite": ("Graphite", _palette(SILKSCREEN)), "paper": ("Paper", _palette(WHITE_BOARD)), "nord": ("Nord", _palette(NORD)), "mocha": ("Catppuccin Mocha", _palette(MOCHA)), "latte": ("Catppuccin Latte", _palette(LATTE)), "neon": ("Neon", _palette(KNOBS_NORTH)), "blueprint": ("Blueprint", _palette(BLUEPRINT)), "graph": ("Graph paper", _palette(SKETCH)), } # ----------------------------------------------------------------- styles # # recolour lists the palettes a style is offered with. Blueprint and Sketch # are left alone: a blueprint that isn't blue, or a pencil sketch that isn't # on paper, is not the same idea. OPEN = ("graphite", "paper", "nord", "mocha", "latte", "neon") STYLES = { "silkscreen": ("Silkscreen", SILKSCREEN, OPEN), "knobs-north": ("Knobs North", KNOBS_NORTH, OPEN), "blueprint": ("Blueprint", BLUEPRINT, ("blueprint",)), "sketch": ("Hand drawn", SKETCH, ("graph",)), } # Keys as they were before styles and palettes were separated, so a saved # setting from an older build still finds its theme. LEGACY = { "silkscreen": ("silkscreen", "graphite"), "white-board": ("silkscreen", "paper"), "nord": ("silkscreen", "nord"), "catppuccin-mocha": ("silkscreen", "mocha"), "catppuccin-latte": ("silkscreen", "latte"), "blueprint": ("blueprint", "blueprint"), "sketch": ("sketch", "graph"), "knobs-north": ("knobs-north", "neon"), } _LEGACY_BY_PAIR = {v: k for k, v in LEGACY.items()} def compose(style_key, palette_key): """One style wearing one palette.""" style_name, base, _allowed = STYLES[style_key] palette_name, colours = PALETTES[palette_key] key = _LEGACY_BY_PAIR.get((style_key, palette_key), f"{style_key}/{palette_key}") name = (style_name if len(STYLES[style_key][2]) == 1 else f"{style_name} / {palette_name}") return replace(base, key=key, name=name, **colours) THEMES = {} for _s, (_sn, _base, _allowed) in STYLES.items(): for _p in _allowed: _t = compose(_s, _p) THEMES[_t.key] = _t DEFAULT = "silkscreen" def parts(key): """(style, palette) for a theme key, old style or new.""" if key in LEGACY: return LEGACY[key] if "/" in key: s, p = key.split("/", 1) if s in STYLES and p in PALETTES: return s, p return LEGACY[DEFAULT] FONTS = Path(__file__).resolve().parent / "fonts" def get(key): if key in THEMES: return THEMES[key] style, palette = parts(key or DEFAULT) if palette not in STYLES[style][2]: palette = STYLES[style][2][0] return THEMES[compose(style, palette).key] def mix(a, b, t): """Blend two hex colours; t = 0 gives a, 1 gives b.""" a, b = a.lstrip("#"), b.lstrip("#") ca = [int(a[i:i + 2], 16) for i in (0, 2, 4)] cb = [int(b[i:i + 2], 16) for i in (0, 2, 4)] return "#" + "".join(f"{round(x + (y - x) * t):02x}" for x, y in zip(ca, cb)) def load_fonts(): """Register the fonts shipped with the app. Safe to call more than once.""" from PySide6.QtGui import QFontDatabase from . import padview for f in sorted(FONTS.glob("*.ttf")): QFontDatabase.addApplicationFont(str(f)) padview._FAMILIES = None