Files
zed/styles/src/theme/create_theme.ts
2023-08-02 12:15:39 -07:00

292 lines
7.0 KiB
TypeScript

import { Scale, Color } from "chroma-js"
import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
import {
ThemeConfig,
ThemeAppearance,
ThemeConfigInputColors,
} from "./theme_config"
import { get_ramps } from "./ramps"
export interface Theme {
name: string
is_light: boolean
/**
* App background, other elements that should sit directly on top of the background.
*/
lowest: Layer
/**
* Panels, tabs, other UI surfaces that sit on top of the background.
*/
middle: Layer
/**
* Editors like code buffers, conversation editors, etc.
*/
highest: Layer
ramps: RampSet
popover_shadow: Shadow
modal_shadow: Shadow
players: Players
syntax?: Partial<ThemeSyntax>
}
export interface Meta {
name: string
author: string
url: string
license: License
}
export interface License {
SPDX: SPDXExpression
}
// License name -> License text
export interface Licenses {
[key: string]: string
}
// FIXME: Add support for the SPDX expression syntax
export type SPDXExpression = "MIT"
export interface Player {
cursor: string
selection: string
}
export interface Players {
"0": Player
"1": Player
"2": Player
"3": Player
"4": Player
"5": Player
"6": Player
"7": Player
}
export interface Shadow {
blur: number
color: string
offset: number[]
}
export type StyleSets = keyof Layer
export interface Layer {
base: StyleSet
variant: StyleSet
on: StyleSet
accent: StyleSet
positive: StyleSet
warning: StyleSet
negative: StyleSet
}
export interface RampSet {
neutral: Scale
red: Scale
orange: Scale
yellow: Scale
green: Scale
cyan: Scale
blue: Scale
violet: Scale
magenta: Scale
}
export type Styles = keyof StyleSet
export interface StyleSet {
default: Style
active: Style
disabled: Style
hovered: Style
pressed: Style
inverted: Style
}
export interface Style {
background: string
border: string
foreground: string
}
export function create_theme(theme: ThemeConfig): Theme {
const {
name,
appearance,
input_color,
override: { syntax },
} = theme
const is_light = appearance === ThemeAppearance.Light
const color_ramps: ThemeConfigInputColors = input_color
// Chromajs scales from 0 to 1 flipped if is_light is true
const ramps = get_ramps(is_light, color_ramps)
const lowest = lowest_layer(ramps)
const middle = middle_layer(ramps)
const highest = highest_layer(ramps)
const popover_shadow = {
blur: 4,
color: ramps
.neutral(is_light ? 7 : 0)
.darken()
.alpha(0.2)
.hex(), // TODO used blend previously. Replace with something else
offset: [1, 2],
}
const modal_shadow = {
blur: 16,
color: ramps
.neutral(is_light ? 7 : 0)
.darken()
.alpha(0.2)
.hex(), // TODO used blend previously. Replace with something else
offset: [0, 2],
}
const players = {
"0": player(ramps.blue),
"1": player(ramps.green),
"2": player(ramps.magenta),
"3": player(ramps.orange),
"4": player(ramps.violet),
"5": player(ramps.cyan),
"6": player(ramps.red),
"7": player(ramps.yellow),
}
return {
name,
is_light,
ramps,
lowest,
middle,
highest,
popover_shadow,
modal_shadow,
players,
syntax,
}
}
function player(ramp: Scale): Player {
return {
selection: ramp(0.5).alpha(0.24).hex(),
cursor: ramp(0.5).hex(),
}
}
function lowest_layer(ramps: RampSet): Layer {
return {
base: build_style_set(ramps.neutral, 0.2, 1),
variant: build_style_set(ramps.neutral, 0.2, 0.7),
on: build_style_set(ramps.neutral, 0.1, 1),
accent: build_style_set(ramps.blue, 0.1, 0.5),
positive: build_style_set(ramps.green, 0.1, 0.5),
warning: build_style_set(ramps.yellow, 0.1, 0.5),
negative: build_style_set(ramps.red, 0.1, 0.5),
}
}
function middle_layer(ramps: RampSet): Layer {
return {
base: build_style_set(ramps.neutral, 0.1, 1),
variant: build_style_set(ramps.neutral, 0.1, 0.7),
on: build_style_set(ramps.neutral, 0, 1),
accent: build_style_set(ramps.blue, 0.1, 0.5),
positive: build_style_set(ramps.green, 0.1, 0.5),
warning: build_style_set(ramps.yellow, 0.1, 0.5),
negative: build_style_set(ramps.red, 0.1, 0.5),
}
}
function highest_layer(ramps: RampSet): Layer {
return {
base: build_style_set(ramps.neutral, 0, 1),
variant: build_style_set(ramps.neutral, 0, 0.7),
on: build_style_set(ramps.neutral, 0.1, 1),
accent: build_style_set(ramps.blue, 0.1, 0.5),
positive: build_style_set(ramps.green, 0.1, 0.5),
warning: build_style_set(ramps.yellow, 0.1, 0.5),
negative: build_style_set(ramps.red, 0.1, 0.5),
}
}
function build_style_set(
ramp: Scale,
background_base: number,
foreground_base: number,
step = 0.08
): StyleSet {
const style_definitions = build_style_definition(
background_base,
foreground_base,
step
)
function color_string(index_or_color: number | Color): string {
if (typeof index_or_color === "number") {
return ramp(index_or_color).hex()
} else {
return index_or_color.hex()
}
}
function build_style(style: Styles): Style {
return {
background: color_string(style_definitions.background[style]),
border: color_string(style_definitions.border[style]),
foreground: color_string(style_definitions.foreground[style]),
}
}
return {
default: build_style("default"),
hovered: build_style("hovered"),
pressed: build_style("pressed"),
active: build_style("active"),
disabled: build_style("disabled"),
inverted: build_style("inverted"),
}
}
function build_style_definition(bg_base: number, fg_base: number, step = 0.08) {
return {
background: {
default: bg_base,
hovered: bg_base + step,
pressed: bg_base + step * 1.5,
active: bg_base + step * 2.2,
disabled: bg_base,
inverted: fg_base + step * 6,
},
border: {
default: bg_base + step * 1,
hovered: bg_base + step,
pressed: bg_base + step,
active: bg_base + step * 3,
disabled: bg_base + step * 0.5,
inverted: bg_base - step * 3,
},
foreground: {
default: fg_base,
hovered: fg_base,
pressed: fg_base,
active: fg_base + step * 6,
disabled: bg_base + step * 4,
inverted: bg_base + step * 2,
},
}
}