Added support for live switching between light and dark themes

This commit is contained in:
Alexei Anoshenko 2026-07-09 20:18:29 +03:00
parent 85e244babf
commit 38c914e2ea
17 changed files with 169 additions and 74 deletions

View File

@ -56,25 +56,11 @@ function sessionInfo() {
message += ",pixel-ratio=" + pixelRatio;
}
/*
if (localStorage.length > 0) {
message += ",storage="
lead = "_{"
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i)
let value = localStorage.getItem(key)
key = key.replaceAll(/\\/g, "\\\\")
key = key.replaceAll(/\"/g, "\\\"")
key = key.replaceAll(/\'/g, "\\\'")
value = value.replaceAll(/\\/g, "\\\\")
value = value.replaceAll(/\"/g, "\\\"")
value = value.replaceAll(/\'/g, "\\\'")
message += lead + "\"" + key + "\"=\"" + value + "\""
lead = ","
}
message += "}"
if (CSS.supports('color', 'light-dark(red, blue)')) {
message += ",light-dark=1";
} else {
message += ",light-dark=0";
}
*/
return message + "}";
}

View File

@ -9,6 +9,7 @@
}
:root {
color-scheme: light dark;
--tooltip-arrow-size: 6px;
--tooltip-background: white;
--tooltip-text-color: black;

View File

@ -203,10 +203,10 @@ func backgroundCSS(properties Properties, session Session) string {
}
if buffer.Len() > 0 {
backgroundColor, _ := colorProperty(properties, BackgroundColor, session)
if backgroundColor != 0 {
lightColor, darkColor, ok := colorProperty(properties, BackgroundColor, session)
if ok && (lightColor != 0 || darkColor != 0) {
buffer.WriteRune(' ')
buffer.WriteString(backgroundColor.cssString())
writeColorCSS(buffer, lightColor, darkColor, session)
}
return buffer.String()
}

View File

@ -765,22 +765,32 @@ func (border *borderProperty) deleteTag(tag PropertyName) bool {
func (border *borderProperty) ViewBorders(session Session) ViewBorders {
defStyle, _ := valueToEnum(border.getRaw(Style), BorderStyle, session, NoneLine)
defWidth, _ := sizeProperty(border, Width, session)
defColor, _ := colorProperty(border, ColorTag, session)
defaultStyle, _ := valueToEnum(border.getRaw(Style), BorderStyle, session, NoneLine)
defaultWidth, _ := sizeProperty(border, Width, session)
defaultLightColor, defaultDarkColor, _ := colorProperty(border, ColorTag, session)
getBorder := func(prefix PropertyName) ViewBorder {
var result ViewBorder
var ok bool
if result.Style, ok = valueToEnum(border.getRaw(prefix+Style), BorderStyle, session, NoneLine); !ok {
result.Style = defStyle
result.Style = defaultStyle
}
if result.Width, ok = sizeProperty(border, prefix+Width, session); !ok {
result.Width = defWidth
result.Width = defaultWidth
}
if result.Color, ok = colorProperty(border, prefix+ColorTag, session); !ok {
result.Color = defColor
lightColor, darkColor, ok := colorProperty(border, prefix+ColorTag, session)
if !ok {
lightColor = defaultLightColor
darkColor = defaultDarkColor
}
if session.DarkTheme() {
result.Color = darkColor
} else {
result.Color = lightColor
}
return result
}
@ -850,7 +860,7 @@ func (border *borderProperty) cssWidthValue(session Session) string {
func (border *borderProperty) cssColorValue(session Session) string {
var builder cssValueBuilder
border.cssColor(&builder, session)
border.cssColor(&builder, session) // TODO !!!
return builder.finish()
}

View File

@ -197,3 +197,17 @@ func stringToColor(text string) (Color, error) {
return 0, errors.New(`Invalid color format: "` + text + `"`)
}
func writeColorCSS(buffer *strings.Builder, lightColor, darkColor Color, session Session) {
if session.lightDarkSupport() {
buffer.WriteString("light-dark(")
buffer.WriteString(lightColor.cssString())
buffer.WriteRune(',')
buffer.WriteString(darkColor.cssString())
buffer.WriteRune(')')
} else if session.DarkTheme() {
buffer.WriteString(darkColor.cssString())
} else {
buffer.WriteString(lightColor.cssString())
}
}

View File

@ -186,13 +186,19 @@ func (picker *colorPickerData) handleCommand(self View, command PropertyName, da
// If it is not specified then a value from the first argument (view) is returned.
func GetColorPickerValue(view View, subviewID ...string) Color {
if view = getSubview(view, subviewID); view != nil {
if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok {
return value
if lightColor, darkColor, ok := colorProperty(view, ColorPickerValue, view.Session()); ok {
if view.Session().DarkTheme() {
return darkColor
}
return lightColor
}
for _, tag := range []PropertyName{ColorPickerValue, Value, ColorTag} {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToColor(value, view.Session()); ok {
return result
if lightColor, darkColor, ok := valueToColor(value, view.Session()); ok {
if view.Session().DarkTheme() {
return darkColor
}
return lightColor
}
}
}

View File

@ -129,7 +129,10 @@ func getColumnSeparatorProperty(properties Properties) ColumnSeparatorProperty {
func (separator *columnSeparatorProperty) ViewBorder(session Session) ViewBorder {
style, _ := valueToEnum(separator.getRaw(Style), BorderStyle, session, NoneLine)
width, _ := sizeProperty(separator, Width, session)
color, _ := colorProperty(separator, ColorTag, session)
color, darkColor, _ := colorProperty(separator, ColorTag, session)
if session.DarkTheme() {
color = darkColor
}
return ViewBorder{
Style: style,
@ -139,28 +142,29 @@ func (separator *columnSeparatorProperty) ViewBorder(session Session) ViewBorder
}
func (separator *columnSeparatorProperty) cssValue(session Session) string {
value := separator.ViewBorder(session)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if value.Width.Type != Auto && value.Width.Type != SizeInFraction &&
(value.Width.Value > 0 || value.Width.Type == SizeFunction) {
buffer.WriteString(value.Width.cssString("", session))
width, ok := sizeProperty(separator, Width, session)
if ok && width.Type != Auto && width.Type != SizeInFraction && (width.Value > 0 || width.Type == SizeFunction) {
buffer.WriteString(width.cssString("", session))
}
styles := enumProperties[BorderStyle].cssValues
if value.Style > 0 && value.Style < len(styles) {
style, _ := valueToEnum(separator.getRaw(Style), BorderStyle, session, NoneLine)
if style > 0 && style < len(styles) {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(styles[value.Style])
buffer.WriteString(styles[style])
}
if value.Color != 0 {
lightColor, darkColor, ok := colorProperty(separator, ColorTag, session)
if ok && (lightColor != 0 || darkColor != 0) {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(value.Color.cssString())
writeColorCSS(buffer, lightColor, darkColor, session)
}
return buffer.String()

View File

@ -52,6 +52,7 @@ var disabledStyles = []string{
type cssBuilder interface {
add(key, value string)
addWriter(key string, writer func(buffer *strings.Builder))
addValues(key, separator string, values ...string)
}
@ -94,6 +95,19 @@ func (builder *viewCSSBuilder) add(key, value string) {
}
}
func (builder *viewCSSBuilder) addWriter(key string, writer func(buffer *strings.Builder)) {
if builder.buffer == nil {
builder.buffer = allocStringBuilder()
} else if builder.buffer.Len() > 0 {
builder.buffer.WriteRune(' ')
}
builder.buffer.WriteString(key)
builder.buffer.WriteString(": ")
writer(builder.buffer)
builder.buffer.WriteRune(';')
}
func (builder *viewCSSBuilder) addValues(key, separator string, values ...string) {
if len(values) == 0 {
return
@ -136,6 +150,13 @@ func (builder *cssValueBuilder) add(key, value string) {
}
}
func (builder *cssValueBuilder) addWriter(key string, writer func(buffer *strings.Builder)) {
if builder.buffer == nil {
builder.buffer = allocStringBuilder()
}
writer(builder.buffer)
}
func (builder *cssValueBuilder) addValues(key, separator string, values ...string) {
if len(values) > 0 {
if builder.buffer == nil {
@ -272,6 +293,20 @@ func (builder *cssStyleBuilder) add(key, value string) {
}
}
func (builder *cssStyleBuilder) addWriter(key string, writer func(buffer *strings.Builder)) {
if builder.buffer == nil {
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
}
builder.buffer.WriteString(`\t`)
builder.buffer.WriteString(key)
builder.buffer.WriteString(": ")
writer(builder.buffer)
builder.buffer.WriteString(`;\n`)
}
func (builder *cssStyleBuilder) addValues(key, separator string, values ...string) {
if len(values) == 0 {
return

View File

@ -76,8 +76,11 @@ func outlineSet(properties Properties, tag PropertyName, value any) []PropertyNa
func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
style, _ := valueToEnum(outline.getRaw(Style), BorderStyle, session, NoneLine)
width, _ := sizeProperty(outline, Width, session)
color, _ := colorProperty(outline, ColorTag, session)
return ViewOutline{Style: style, Width: width, Color: color}
lightColor, darkColor, _ := colorProperty(outline, ColorTag, session)
if session.DarkTheme() {
return ViewOutline{Style: style, Width: width, Color: darkColor}
}
return ViewOutline{Style: style, Width: width, Color: lightColor}
}
// ViewOutline describes parameters of a view border

View File

@ -1427,8 +1427,8 @@ func (popup *popupData) createLayerView() GridLayout {
layerParams[ClickEvent] = popup.cancel
}
if color, ok := colorProperty(popup, OutsideColor, session); ok {
layerParams[BackgroundColor] = color
if value := popup.getRaw(OutsideColor); value != nil {
layerParams[BackgroundColor] = value
}
if value := popup.getRaw(OutsideFilter); value != nil {

View File

@ -81,24 +81,28 @@ func angleProperty(properties Properties, tag PropertyName, session Session) (An
return AngleUnit{Type: 0, Value: 0}, false
}
func valueToColor(value any, session Session) (Color, bool) {
func valueToColor(value any, session Session) (Color, Color, bool) {
if value != nil {
switch value := value.(type) {
case Color:
return value, true
return value, value, true
case string:
if ok, constName := isConstantName(value); ok {
return session.Color(constName)
lightColor, ok1 := session.getColor(constName, false)
darkColor, ok2 := session.getColor(constName, true)
return lightColor, darkColor, ok1 && ok2
}
if color, ok := StringToColor(value); ok {
return color, color, true
}
return StringToColor(value)
}
}
return Color(0), false
return Color(0), Color(0), false
}
func colorProperty(properties Properties, tag PropertyName, session Session) (Color, bool) {
func colorProperty(properties Properties, tag PropertyName, session Session) (Color, Color, bool) {
return valueToColor(properties.getRaw(tag), session)
}

View File

@ -154,6 +154,8 @@ type Session interface {
getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string
getColor(tag string, darkMode bool) (Color, bool)
lightDarkSupport() bool
resolveConstants(value string) (string, bool)
checkboxOffImage(accentColor Color) string
checkboxOnImage(accentColor Color) string
@ -215,6 +217,7 @@ type sessionData struct {
currentTheme Theme
darkTheme bool
touchScreen bool
lightDark bool
screenWidth int
screenHeight int
textDirection int
@ -771,6 +774,10 @@ func (session *sessionData) handleSessionInfo(params DataObject) {
session.pixelRatio = f
}
}
if value, ok := params.PropertyValue("light-dark"); ok {
session.lightDark = (value == "1" || value == "true")
}
}
func (session *sessionData) handleEvent(command string, data DataObject) {

View File

@ -124,12 +124,15 @@ func (session *sessionData) getCurrentTheme() Theme {
return defaultTheme
}
// Color return the color with "tag" name or 0 if it is not exists
func (session *sessionData) Color(tag string) (Color, bool) {
func (session *sessionData) lightDarkSupport() bool {
return session.lightDark
}
func (session *sessionData) getColor(tag string, darkMode bool) (Color, bool) {
tags := []string{tag}
theme := session.getCurrentTheme()
for {
result := theme.color(tag, session.darkTheme)
result := theme.color(tag, darkMode)
if result == "" {
ErrorLogF(`"%v" color not found`, tag)
return 0, false
@ -154,6 +157,11 @@ func (session *sessionData) Color(tag string) (Color, bool) {
}
}
// Color return the color with "tag" name or 0 if it is not exists
func (session *sessionData) Color(tag string) (Color, bool) {
return session.getColor(tag, session.darkTheme)
}
func (session *sessionData) ImageConstant(tag string) (string, bool) {
tags := []string{tag}
theme := session.getCurrentTheme()

View File

@ -205,13 +205,13 @@ func (shadow *shadowPropertyData) init() {
}
func (shadow *shadowPropertyData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session)
lightColor, darkColor, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session)
if color.Alpha() == 0 ||
if (lightColor.Alpha() == 0 && darkColor.Alpha() == 0) ||
((offsetX.Type == Auto || offsetX.Value == 0) &&
(offsetY.Type == Auto || offsetY.Value == 0) &&
(blurRadius.Type == Auto || blurRadius.Value == 0) &&
@ -232,17 +232,17 @@ func (shadow *shadowPropertyData) cssStyle(buffer *strings.Builder, session Sess
buffer.WriteByte(' ')
buffer.WriteString(spreadRadius.cssString("0", session))
buffer.WriteByte(' ')
buffer.WriteString(color.cssString())
writeColorCSS(buffer, lightColor, darkColor, session)
return true
}
func (shadow *shadowPropertyData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session)
lightColor, darkColor, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
if color.Alpha() == 0 ||
if (lightColor.Alpha() == 0 && darkColor.Alpha() == 0) ||
((offsetX.Type == Auto || offsetX.Value == 0) &&
(offsetY.Type == Auto || offsetY.Value == 0) &&
(blurRadius.Type == Auto || blurRadius.Value == 0)) {
@ -256,18 +256,18 @@ func (shadow *shadowPropertyData) cssTextStyle(buffer *strings.Builder, session
buffer.WriteByte(' ')
buffer.WriteString(blurRadius.cssString("0", session))
buffer.WriteByte(' ')
buffer.WriteString(color.cssString())
writeColorCSS(buffer, lightColor, darkColor, session)
return true
}
func (shadow *shadowPropertyData) visible(session Session) bool {
color, _ := colorProperty(shadow, ColorTag, session)
lightColor, darkColor, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session)
if color.Alpha() == 0 ||
if (lightColor.Alpha() == 0 && darkColor.Alpha() == 0) ||
((offsetX.Type == Auto || offsetX.Value == 0) &&
(offsetY.Type == Auto || offsetY.Value == 0) &&
(blurRadius.Type == Auto || blurRadius.Value == 0) &&

View File

@ -1019,8 +1019,12 @@ func (view *viewData) propertyChanged(tag PropertyName) {
AccentColor: string(AccentColor),
}
if cssTag, ok := colorTags[tag]; ok {
if color, ok := colorProperty(view, tag, session); ok {
session.updateCSSProperty(htmlID, cssTag, color.cssString())
if lightColor, darkColor, ok := colorProperty(view, tag, session); ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writeColorCSS(buffer, lightColor, darkColor, session)
session.updateCSSProperty(htmlID, cssTag, buffer.String())
} else {
session.updateCSSProperty(htmlID, cssTag, "")
}

View File

@ -256,6 +256,7 @@ func writeViewStyleCSS(style Properties, builder cssBuilder, session Session, ig
property PropertyName
cssTag string
}
colorProperties := []propertyCss{
//{BackgroundColor, string(BackgroundColor)},
{TextColor, "color"},
@ -263,9 +264,13 @@ func writeViewStyleCSS(style Properties, builder cssBuilder, session Session, ig
{CaretColor, string(CaretColor)},
{AccentColor, string(AccentColor)},
}
for _, p := range colorProperties {
if color, ok := colorProperty(style, p.property, session); ok && color != 0 {
builder.add(p.cssTag, color.cssString())
lightColor, darkColor, ok := colorProperty(style, p.property, session)
if ok && (lightColor != 0 || darkColor != 0) {
builder.addWriter(p.cssTag, func(buffer *strings.Builder) {
writeColorCSS(buffer, lightColor, darkColor, session)
})
}
}
@ -280,9 +285,11 @@ func writeViewStyleCSS(style Properties, builder cssBuilder, session Session, ig
if background := backgroundCSS(style, session); background != "" {
builder.add("background", background)
} else {
backgroundColor, _ := colorProperty(style, BackgroundColor, session)
if backgroundColor != 0 {
builder.add("background-color", backgroundColor.cssString())
lightColor, darkColor, ok := colorProperty(style, BackgroundColor, session)
if ok && (lightColor != 0 || darkColor != 0) {
builder.addWriter("background-color", func(buffer *strings.Builder) {
writeColorCSS(buffer, lightColor, darkColor, session)
})
}
}

View File

@ -748,12 +748,18 @@ func floatStyledProperty(view View, subviewID []string, tag PropertyName, defaul
func colorStyledProperty(view View, subviewID []string, tag PropertyName, inherit bool) Color {
if view = getSubview(view, subviewID); view != nil {
if value, ok := colorProperty(view, tag, view.Session()); ok {
return value
if lightColor, darkColor, ok := colorProperty(view, tag, view.Session()); ok {
if view.Session().DarkTheme() {
return darkColor
}
return lightColor
}
if value := valueFromStyle(view, tag); value != nil {
if color, ok := valueToColor(value, view.Session()); ok {
return color
if lightColor, darkColor, ok := valueToColor(value, view.Session()); ok {
if view.Session().DarkTheme() {
return darkColor
}
return lightColor
}
}
if inherit {