mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-11-06 20:42:15 +00:00
wip: basic theming support
This commit is contained in:
245
ui/theme/material3.py
Normal file
245
ui/theme/material3.py
Normal file
@ -0,0 +1,245 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from materialyoucolor.blend import Blend
|
||||
from materialyoucolor.dynamiccolor.contrast_curve import ContrastCurve
|
||||
from materialyoucolor.dynamiccolor.dynamic_color import DynamicColor, FromPaletteOptions
|
||||
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
|
||||
from materialyoucolor.hct import Hct
|
||||
from materialyoucolor.palettes.tonal_palette import TonalPalette
|
||||
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
|
||||
from PySide6.QtGui import QColor, QPalette
|
||||
|
||||
from .shared import ThemeImpl, ThemeInfo, _TCustomPalette, _TScheme
|
||||
|
||||
|
||||
class _M3ThemeDataExtendedColorItem(TypedDict):
|
||||
name: str
|
||||
color: str
|
||||
description: str
|
||||
harmonized: bool
|
||||
|
||||
|
||||
_M3ThemeDataSchemes = TypedDict(
|
||||
"_M3ThemeDataSchemes",
|
||||
{
|
||||
"light": dict[str, str],
|
||||
"light-medium-contrast": dict[str, str],
|
||||
"light-high-contrast": dict[str, str],
|
||||
"dark": dict[str, str],
|
||||
"dark-medium-contrast": dict[str, str],
|
||||
"dark-high-contrast": dict[str, str],
|
||||
},
|
||||
)
|
||||
|
||||
_M3ThemeDataPalettes = TypedDict(
|
||||
"_M3ThemeDataPalettes",
|
||||
{
|
||||
"primary": dict[str, str],
|
||||
"secondary": dict[str, str],
|
||||
"tertiary": dict[str, str],
|
||||
"neutral": dict[str, str],
|
||||
"neutral-variant": dict[str, str],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class _M3ThemeData(TypedDict):
|
||||
name: str
|
||||
description: str
|
||||
seed: str
|
||||
coreColors: dict[str, str]
|
||||
extendedColors: list[_M3ThemeDataExtendedColorItem]
|
||||
schemes: _M3ThemeDataSchemes
|
||||
palettes: _M3ThemeDataPalettes
|
||||
|
||||
|
||||
def _hexToHct(hexColor: str) -> Hct:
|
||||
pureHexPart = hexColor[1:] if hexColor.startswith("#") else hexColor
|
||||
return Hct.from_int(int(f"0xff{pureHexPart}", 16))
|
||||
|
||||
|
||||
def _hctToQColor(hct: Hct) -> QColor:
|
||||
return QColor.fromRgba(hct.to_int())
|
||||
|
||||
|
||||
class Material3ThemeImpl(ThemeImpl):
|
||||
COLOR_ROLE_MAPPING: dict[QPalette.ColorRole, str] = {
|
||||
QPalette.ColorRole.Window: "surface",
|
||||
QPalette.ColorRole.WindowText: "onSurface",
|
||||
QPalette.ColorRole.Base: "surfaceContainer",
|
||||
QPalette.ColorRole.AlternateBase: "surfaceContainerHighest",
|
||||
QPalette.ColorRole.ToolTipBase: "secondaryContainer",
|
||||
QPalette.ColorRole.ToolTipText: "onSecondaryContainer",
|
||||
QPalette.ColorRole.PlaceholderText: "inverseSurface",
|
||||
QPalette.ColorRole.Text: "onSurface",
|
||||
QPalette.ColorRole.Button: "primaryContainer",
|
||||
QPalette.ColorRole.ButtonText: "onPrimaryContainer",
|
||||
QPalette.ColorRole.BrightText: "onSecondary",
|
||||
QPalette.ColorRole.Light: "surfaceContainerLowest",
|
||||
QPalette.ColorRole.Midlight: "surfaceContainerLow",
|
||||
QPalette.ColorRole.Dark: "inverseSurface",
|
||||
QPalette.ColorRole.Mid: "surfaceContainer",
|
||||
QPalette.ColorRole.Shadow: "shadow",
|
||||
QPalette.ColorRole.Highlight: "primary",
|
||||
QPalette.ColorRole.Accent: "primary",
|
||||
QPalette.ColorRole.HighlightedText: "onPrimary",
|
||||
QPalette.ColorRole.Link: "tertiary",
|
||||
QPalette.ColorRole.LinkVisited: "tertiaryContainer",
|
||||
}
|
||||
|
||||
def __init__(self, *, themeData: _M3ThemeData, scheme: _TScheme):
|
||||
self.themeData = themeData
|
||||
self.scheme: _TScheme = scheme
|
||||
|
||||
if self.themeData["schemes"].get(scheme) is None:
|
||||
raise ValueError(f"Invalid scheme: {scheme}")
|
||||
|
||||
def _findExtendedColor(
|
||||
self, colorName: str
|
||||
) -> _M3ThemeDataExtendedColorItem | None:
|
||||
return next(
|
||||
(it for it in self.themeData["extendedColors"] if it["name"] == colorName),
|
||||
None,
|
||||
)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return ThemeInfo(
|
||||
series="material3",
|
||||
name=self.themeData["name"],
|
||||
scheme=self.scheme,
|
||||
)
|
||||
|
||||
@property
|
||||
def qPalette(self) -> QPalette:
|
||||
qPalette = QPalette()
|
||||
|
||||
for role, name in self.COLOR_ROLE_MAPPING.items():
|
||||
color = QColor.fromString(self.themeData["schemes"][self.scheme][name])
|
||||
qPalette.setColor(role, color)
|
||||
|
||||
return qPalette
|
||||
|
||||
@property
|
||||
def customPalette(self) -> _TCustomPalette:
|
||||
primaryHct = _hexToHct(self.themeData["schemes"][self.scheme]["primary"])
|
||||
|
||||
successColorItem = self._findExtendedColor("Success")
|
||||
if successColorItem is None:
|
||||
raise Exception("Success color not found")
|
||||
successHct = _hexToHct(successColorItem["color"])
|
||||
|
||||
successHarmonizedHct = Hct.from_int(
|
||||
Blend.harmonize(successHct.to_int(), primaryHct.to_int())
|
||||
)
|
||||
|
||||
return {
|
||||
"primary": _hctToQColor(primaryHct),
|
||||
"success": _hctToQColor(successHarmonizedHct),
|
||||
"error": QColor.fromString(self.themeData["schemes"][self.scheme]["error"]),
|
||||
}
|
||||
|
||||
|
||||
class Material3DynamicThemeImpl(ThemeImpl):
|
||||
ACTIVE_COLOR_ROLE_MAPPING: dict[QPalette.ColorRole, DynamicColor] = {
|
||||
QPalette.ColorRole.Window: MaterialDynamicColors.surface,
|
||||
QPalette.ColorRole.WindowText: MaterialDynamicColors.onSurface,
|
||||
QPalette.ColorRole.Base: MaterialDynamicColors.surfaceContainer,
|
||||
QPalette.ColorRole.AlternateBase: MaterialDynamicColors.surfaceContainerHighest,
|
||||
QPalette.ColorRole.ToolTipBase: MaterialDynamicColors.secondaryContainer,
|
||||
QPalette.ColorRole.ToolTipText: MaterialDynamicColors.onSecondaryContainer,
|
||||
QPalette.ColorRole.PlaceholderText: MaterialDynamicColors.inverseSurface,
|
||||
QPalette.ColorRole.Text: MaterialDynamicColors.onSurface,
|
||||
QPalette.ColorRole.Button: MaterialDynamicColors.primaryContainer,
|
||||
QPalette.ColorRole.ButtonText: MaterialDynamicColors.onPrimaryContainer,
|
||||
QPalette.ColorRole.BrightText: MaterialDynamicColors.onSecondary,
|
||||
QPalette.ColorRole.Light: MaterialDynamicColors.surfaceContainerLowest,
|
||||
QPalette.ColorRole.Midlight: MaterialDynamicColors.surfaceContainerLow,
|
||||
QPalette.ColorRole.Dark: MaterialDynamicColors.inverseSurface,
|
||||
QPalette.ColorRole.Mid: MaterialDynamicColors.surfaceContainer,
|
||||
QPalette.ColorRole.Shadow: MaterialDynamicColors.shadow,
|
||||
QPalette.ColorRole.Highlight: MaterialDynamicColors.primary,
|
||||
QPalette.ColorRole.Accent: MaterialDynamicColors.primary,
|
||||
QPalette.ColorRole.HighlightedText: MaterialDynamicColors.onPrimary,
|
||||
QPalette.ColorRole.Link: MaterialDynamicColors.tertiary,
|
||||
QPalette.ColorRole.LinkVisited: MaterialDynamicColors.tertiaryContainer,
|
||||
}
|
||||
|
||||
EXTENDED_COLORS = {
|
||||
"success": "#00c555",
|
||||
}
|
||||
|
||||
def __init__(self, sourceColorHex: str, scheme: _TScheme, *, name: str):
|
||||
self.material3Scheme = SchemeTonalSpot(
|
||||
_hexToHct(sourceColorHex),
|
||||
is_dark=scheme == "dark",
|
||||
contrast_level=0.0,
|
||||
)
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return ThemeInfo(
|
||||
series="material3-dynamic",
|
||||
name=self.name,
|
||||
scheme="dark" if self.material3Scheme.is_dark else "light",
|
||||
)
|
||||
|
||||
@property
|
||||
def qPalette(self) -> QPalette:
|
||||
qPalette = QPalette()
|
||||
|
||||
for role, dynamicColor in self.ACTIVE_COLOR_ROLE_MAPPING.items():
|
||||
hct = dynamicColor.get_hct(self.material3Scheme)
|
||||
qColor = QColor.fromRgba(hct.to_int())
|
||||
qPalette.setColor(QPalette.ColorGroup.Active, role, qColor)
|
||||
|
||||
# TODO: disabled palette seems to work only after a theme reload, needs further investigation
|
||||
if role in [QPalette.ColorRole.Button, QPalette.ColorRole.ButtonText]:
|
||||
disabledHct = Hct.from_hct(hct.hue, 1.0, hct.tone)
|
||||
disabledQColor = QColor.fromRgba(disabledHct.to_int())
|
||||
qPalette.setColor(QPalette.ColorGroup.Disabled, role, disabledQColor)
|
||||
|
||||
return qPalette
|
||||
|
||||
@property
|
||||
def customPalette(self) -> _TCustomPalette:
|
||||
primaryHct = MaterialDynamicColors.primary.get_hct(self.material3Scheme)
|
||||
errorHct = MaterialDynamicColors.error.get_hct(self.material3Scheme)
|
||||
|
||||
extendedPalettes: dict[str, DynamicColor] = {}
|
||||
for colorName, colorHex in self.EXTENDED_COLORS.items():
|
||||
colorHct = _hexToHct(colorHex)
|
||||
colorHarmonized = Blend.harmonize(colorHct.to_int(), primaryHct.to_int())
|
||||
|
||||
colorTonalPalette = TonalPalette.from_int(colorHarmonized)
|
||||
|
||||
colorSurfacePaletteOptions = DynamicColor.from_palette(
|
||||
FromPaletteOptions(
|
||||
name=f"{colorName}_container",
|
||||
palette=lambda s: colorTonalPalette,
|
||||
tone=lambda s: 30 if s.is_dark else 90,
|
||||
is_background=True,
|
||||
background=lambda s: MaterialDynamicColors.highestSurface(s),
|
||||
contrast_curve=ContrastCurve(1, 1, 3, 4.5),
|
||||
)
|
||||
)
|
||||
|
||||
extendedPalettes[colorName] = DynamicColor.from_palette(
|
||||
FromPaletteOptions(
|
||||
name=colorName, # pyright: ignore[reportArgumentType]
|
||||
palette=lambda s: colorTonalPalette,
|
||||
tone=lambda s: 80 if s.is_dark else 40, # pyright: ignore[reportArgumentType]
|
||||
is_background=False, # pyright: ignore[reportArgumentType]
|
||||
background=lambda s: MaterialDynamicColors.highestSurface(s),
|
||||
contrast_curve=ContrastCurve(3, 4.5, 7, 7),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"primary": _hctToQColor(primaryHct),
|
||||
"success": _hctToQColor(
|
||||
extendedPalettes["success"].get_hct(self.material3Scheme)
|
||||
),
|
||||
"error": _hctToQColor(errorHct),
|
||||
}
|
||||
Reference in New Issue
Block a user