From 38c914e2eae0dab9c4aac94a72203f8ec4a13352 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Thu, 9 Jul 2026 20:18:29 +0300 Subject: [PATCH] Added support for live switching between light and dark themes --- app_scripts.js | 22 ++++------------------ app_styles.css | 1 + background.go | 6 +++--- border.go | 26 ++++++++++++++++++-------- color.go | 14 ++++++++++++++ colorPicker.go | 14 ++++++++++---- columnSeparator.go | 22 +++++++++++++--------- cssBuilder.go | 35 +++++++++++++++++++++++++++++++++++ outline.go | 7 +++++-- popup.go | 4 ++-- propertyGet.go | 16 ++++++++++------ session.go | 7 +++++++ sessionTheme.go | 14 +++++++++++--- shadow.go | 16 ++++++++-------- view.go | 8 ++++++-- viewStyle.go | 17 ++++++++++++----- viewUtils.go | 14 ++++++++++---- 17 files changed, 169 insertions(+), 74 deletions(-) diff --git a/app_scripts.js b/app_scripts.js index 54b4f76..2ac2675 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -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 + "}"; } diff --git a/app_styles.css b/app_styles.css index dee0cde..2ab76d8 100644 --- a/app_styles.css +++ b/app_styles.css @@ -9,6 +9,7 @@ } :root { + color-scheme: light dark; --tooltip-arrow-size: 6px; --tooltip-background: white; --tooltip-text-color: black; diff --git a/background.go b/background.go index 28386f0..65917fe 100644 --- a/background.go +++ b/background.go @@ -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() } diff --git a/border.go b/border.go index e36c3b8..a6950b6 100644 --- a/border.go +++ b/border.go @@ -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() } diff --git a/color.go b/color.go index fcb9249..a3dcec2 100644 --- a/color.go +++ b/color.go @@ -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()) + } +} diff --git a/colorPicker.go b/colorPicker.go index ad2e81e..44d19b5 100644 --- a/colorPicker.go +++ b/colorPicker.go @@ -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 } } } diff --git a/columnSeparator.go b/columnSeparator.go index ffd7ef3..4037764 100644 --- a/columnSeparator.go +++ b/columnSeparator.go @@ -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() diff --git a/cssBuilder.go b/cssBuilder.go index 52a4824..7df91df 100644 --- a/cssBuilder.go +++ b/cssBuilder.go @@ -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 diff --git a/outline.go b/outline.go index f5fa00a..8ad8736 100644 --- a/outline.go +++ b/outline.go @@ -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 diff --git a/popup.go b/popup.go index 5ddd8db..2f485cb 100644 --- a/popup.go +++ b/popup.go @@ -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 { diff --git a/propertyGet.go b/propertyGet.go index f7475b9..a7d3785 100644 --- a/propertyGet.go +++ b/propertyGet.go @@ -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) } diff --git a/session.go b/session.go index 03d6d22..85708b1 100644 --- a/session.go +++ b/session.go @@ -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) { diff --git a/sessionTheme.go b/sessionTheme.go index ecaee53..654ddaa 100644 --- a/sessionTheme.go +++ b/sessionTheme.go @@ -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() diff --git a/shadow.go b/shadow.go index 6458a84..0853799 100644 --- a/shadow.go +++ b/shadow.go @@ -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) && diff --git a/view.go b/view.go index 79a6e2c..3d6e5e5 100644 --- a/view.go +++ b/view.go @@ -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, "") } diff --git a/viewStyle.go b/viewStyle.go index 2dd96d8..23a5e0c 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -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) + }) } } diff --git a/viewUtils.go b/viewUtils.go index 0966513..22eef6a 100644 --- a/viewUtils.go +++ b/viewUtils.go @@ -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 {