meridian59/docs/themes.md
Adrien Laws c0b3c02085
feature: add configurable theme borders for chat and stats areas (theming part 5) (#1442)
## What

- Adds additional theming controls for the chat box and stats area
- Adds `ThemeSkipChatBoxFrame()` to complement the existing
`ThemeSkipStatsAreaFrame` (added in #1430)
- Adds a customizable border color, `ThemeBorderColor`, that can be used
to draw a border around the chat box and/or the stats area in place of
the skipped ornament
- Dark theme now uses the above to skip the chat box and stats area
ornamental frames and to draw a border specific to the dark theme
- Default theme is unchanged
- related to #1425 

## Why

- #1430 removed the stats area ornamental frame in dark theme, but that
left the stats area floating without a border
- The chat box had a border, but it used ornamental frame resources that
did not look good in dark theme
- The replacement border should be configurable so future themes can set
their own color

## How

### New functions

- `module/merintr/theme.c`
- `ThemeSkipChatBoxFrame` - returns true when the active theme skips the
`B*` chat box ornament. Same shape as the existing
`ThemeSkipStatsAreaFrame`
- `clientd3d/color.c`
- `ThemeBorderColor` - per-theme border color for the stats area and
chat box, or `CLR_INVALID` for the default border. Exported via
`client.def` so both the client and the interface module read it. One
shared color so the two borders match

### Borders

Both borders use the existing `DrawBorder`, converting the color to the
nearest palette index with `GetClosestPaletteIndex`.

- Chat box: drawn by `TextInputDrawBorder` in `clientd3d/textin.c`. No
new drawing code. The chat box already drew its own border, it just uses
`ThemeBorderColor` now and keeps its focus highlight (highlight color
while focused, theme color while not)
- Stats area: drawn by `InterfaceDrawElements` in
`module/merintr/drawint.c`. New code, since the stats area had no border
before

## Notes

- The border color is not a `COLOR_*` table entry. The table is for
user-customizable colors. This border is a fixed per-theme decision.
- The frame and the border are independent. Setting a border color
without skipping the frame draws both, which may not look good. Whether
to combine them is the theme author's choice.
- The chat box border sits a couple of pixels outside the text area,
leaving a thin gap that shows the window background. This is the
existing border geometry, not new to this change. In the default theme
the ornament filled that region, so the gap was hidden. With the
ornament skipped it is faintly visible. It is minor so I left it out of
scope for this change.

### Examples
#### Chat Box (before)
<img width="954" height="516" alt="image"
src="https://github.com/user-attachments/assets/99e270f7-9b9e-41b6-9077-3f55d65d4243"
/>

- default border clashes with dark theme and is not configurable
per-theme

#### Chat Box (after)
<img width="953" height="516" alt="image"
src="https://github.com/user-attachments/assets/48207cf5-be7b-4a3a-af3a-845f56f1b795"
/>

#### Stat Area (before)
<img width="363" height="600" alt="image"
src="https://github.com/user-attachments/assets/06b31f14-d1ee-47eb-b495-84cef38e7e0d"
/>

- floats without a border (see lower left)

#### Stat Area (after)
<img width="368" height="600" alt="image"
src="https://github.com/user-attachments/assets/a560acbd-afc9-42e3-b28a-677623e3115d"
/>
2026-06-26 13:55:02 -07:00

5.4 KiB

Themes

The client supports swappable UI themes selected from the Settings dialog under Interface Features. Theme swaps take effect without a restart.

Two themes ship today: Theme::Default (the original Meridian 59 palette) and Theme::Dark (a dark theme).

A theme covers two kinds of surface: colors and bitmaps.

Color theming

Theme definitions and the abstraction that hides them live in clientd3d/color.c and clientd3d/srvrstr.c. The rest of the client asks for a color or brush by name (GetColor(COLOR_X), GetBrush(COLOR_X)) and is theme-blind.

The active theme is stored in Config::theme (the Theme enum in clientd3d/config.h). Each theme keeps its customized colors in its own INI section (e.g. [Colors], [ColorsDark]) so switching themes does not clobber the other theme's saved values.

Bitmap surfaces

Bitmap theming is per-surface. A surface is themed when a _DARK (or other theme-specific) variant exists and the code path routes the ID through a resolver. Surfaces without a variant render the default art under every theme.

Three kinds of themeable bitmap surface exist:

  • Tiled backgrounds: the main window background and the inventory texture.
  • Wrapper ornaments: decorative corner + edge-repeater bitmaps drawn around the major UI regions.
  • Button and icon art: toolbar buttons, stat-tab buttons, mailbox icon.

The wrapper ornaments live in module/merintr/drawint.c. Five groups exist today:

Group Wraps Dark theme handling
E* Outer client-window edge Yes
M* Minimap Yes
S* Stats area outer panel No (solid-line border)
B* Chat edit box No (solid-line border)
I* Inventory area No (skipped for every theme)

Optional solid-line border for stats area and chat box

A theme can skip the S* and B* ornaments and draw a thin solid-line border in their place, for when the ornament artwork does not read well against the theme's sidebar fill.

Three theme switches control this: one to skip the stats area frame, one to skip the chat box frame, and a shared border color used for both. Returning no color keeps the default border.

The frame and the border are independent. Setting a border color without skipping the frame draws both, which may not look good. Whether to combine them is the theme author's choice.

Sidebar fill

The right sidebar (enchantments, portrait, stat bars) sits inside the main window. Two fill strategies exist for the sidebar area:

  • Show the main window background through any gaps between drawn elements.
  • Paint the sidebar with the inventory texture so it has its own fill.

Each theme picks one. The choice depends on whether the main window background reads well behind the portrait and stat bars. For example, the dark theme paints the inventory texture across the sidebar because the dark main window background lacks contrast against the portrait; the default theme leaves the main window background visible.

flowchart LR
    A[Caller paints<br/>a sidebar region] --> Q{active theme uses<br/>inventory fill?}
    Q -->|yes| F[Fill with inventory texture]
    Q -->|no| D[Return without painting]

    style A fill:#1971c2,color:#fff
    style Q fill:#b08000,color:#fff
    style F fill:#333,color:#fff
    style D fill:#333,color:#fff

Per-module bitmap resolvers

Each module owns its own bitmap IDs in its own resource.h. The ID values are not shared across modules, so each module ships its own resolver:

Module Resolver Themed bitmaps today
clientd3d MainThemeResourceId Main window background
module/merintr InterfaceThemeResourceId Inventory texture; window-edge and minimap wrapper ornaments

Other client modules (admin, char, chess, dm, mailnews) contain bitmaps but have no themed variants today.

A resolver takes a default-theme bitmap ID and returns the variant for the active theme. If the active theme has no variant for that ID, the resolver returns the input unchanged.

flowchart LR
    A[Caller in clientd3d] --> M[MainThemeResourceId]
    B[Caller in merintr] --> R[InterfaceThemeResourceId]
    M --> Q{variant for<br/>active theme?}
    R --> Q
    Q -->|yes| V[return themed ID]
    Q -->|no| D[return input ID]

    style A fill:#1971c2,color:#fff
    style B fill:#1971c2,color:#fff
    style M fill:#2f9e44,color:#fff
    style R fill:#2f9e44,color:#fff
    style Q fill:#b08000,color:#fff
    style V fill:#333,color:#fff
    style D fill:#333,color:#fff

Adding a new theme

The major components to touch:

  1. The Theme enum in clientd3d/config.h.
  2. The color tables and INI machinery in clientd3d/color.c.
  3. The per-theme menu bar color in ThemeMenuBarColor in clientd3d/color.c (optional). Skip to keep the system default.
  4. The per-theme title bar style in ThemeUsesDarkTitleBar in clientd3d/color.c (optional). Return true for light text on a dark title bar, false for dark text on a light title bar. Match this to the menu bar color for the most consistent look.
  5. Server message colors in clientd3d/srvrstr.c (optional).
  6. The Settings UI: localized string in clientd3d/client.rc and combo entry in clientd3d/preferences.c.
  7. Bitmap variants (optional): author _<NAME> BMP files and extend the per-module bitmap resolvers in clientd3d/color.c and module/merintr/theme.c.
  8. Theme capability switches in module/merintr/theme.c.