#863: CalendarStyle.kt

projectforge-business/src/main/kotlin/org/projectforge/business/calendar/CalendarStyle.kt Type: Kotlin class (styling utility)
Purpose: Computes background and text colors for calendar events based on a configurable base color
Source path: projectforge-business/src/main/kotlin/org/projectforge/business/calendar/CalendarStyle.kt 161 lines · 103 code · 38 comments · 20 blank

CalendarStyle takes a base background color (hex string), computes a matching text color, and derives a semi-transparent background variation for calendar event rendering. The text color is calculated server-side using HSB color-space manipulation to ensure adequate contrast. Results are cached in a ColorCache (extending AbstractCache with 1-hour expiry) to avoid recomputing the same conversions repeatedly. Two color schemes are supported: the default (modern) mode with HSB-derived colors and a CLASSIC mode that preserves the original v6 behavior of black-on-white or white-on-dark.

Constructor

ParameterTypeDefaultDescription
baseBackgroundColor String? null A hex color string (e.g., "#06790e"). Defaults to "#777" if null.

Properties

bgColor: String

The base background color. Initialized from the constructor parameter, falling back to "#777" (a neutral gray). This is stored as the canonical hex value and should not be used directly for rendering — use getBackgroundColor() instead.

Public Methods

getTextColor(colorScheme: CalendarEventColorScheme?): String

Returns the computed text color for the given color scheme. Delegates to colorCache.getTextColor(bgColor, colorScheme).

getBackgroundColor(calendarEventColorScheme: CalendarEventColorScheme?): String

Returns the background color adjusted for the color scheme:

Companion Object — Color Computation Engine

Internal class RGB(r: Int, g: Int, b: Int)

Simple data class holding RGB channel values (0–255).

Internal class ColorCache : AbstractCache()

Cache with 1-hour TTL that stores computed text colors keyed by hex string, avoiding redundant HSB conversions. Maintains separate maps for the standard and classic color schemes. Thread-safe via synchronized blocks on the per-scheme map.

Color Conversion Functions

FunctionSignaturePurpose
validateHexCode (color: String): Boolean Validates whether a string matches a 3-digit (#[a-f0-9]{3}) or 6-digit (#[a-f0-9]{6}) hex color pattern.
hexToRGB (color: String?): RGB Converts a hex string to an RGB object. Expands shorthand hex ("#abc""#aabbcc"). Returns RGB(0,0,0) on failure.
hexToColor (color: String?): Color Converts a hex string to a java.awt.Color. Handles shorthand expansion.
getTextColor (backgroundColor: String?, colorScheme: CalendarEventColorScheme?): String Public static entry point delegating to the cache.
calculateTextColor (backgroundColor: String?, colorScheme: CalendarEventColorScheme?): String Core algorithm. For CLASSIC: returns "#fff" if dark, "#444" otherwise. For modern: shifts the HSB brightness down by 0.6 (clamped to 0.3 minimum) to produce a darker variant of the background color.
brightness(rgb: RGB): Int Weighted brightness formula: (R*299 + G*587 + B*114) / 1000 (perceptual luminance).
brightness(color: String?): Int Overload that calls hexToRGB first, then computes brightness.
dark(color: String?): Boolean Returns true if brightness is below 180 (i.e., the color is dark).

Color Algorithm Details

For the modern scheme, the text color is computed by:

  1. Converting the background hex to java.awt.Color.
  2. Extracting HSB (Hue, Saturation, Brightness) values via Color.RGBtoHSB().
  3. Subtracting 0.6 from brightness, clamping the lower bound to 0.3. This produces a noticeably darker shade of the same hue.
  4. Converting back to RGB via Color.HSBtoRGB() and formatting as "#rrggbb".
  5. Comment-out code had additional saturation boosting for near-gray colors that was disabled.

The CLASSIC scheme is simpler: dark backgrounds get white text ("#fff"), light backgrounds get dark gray text ("#444"), with the threshold at brightness 180.

Git History

Design Rationale

Server-side color computation is a deliberate architectural decision. By calculating text and background colors on the server (rather than the React frontend), the same color values are available for both FullCalendar event rendering and form select dropdowns, avoiding inconsistencies. The HSB-based algorithm produces colors that are perceptually related to the background (same hue, adjusted brightness) rather than simply inverting — this creates a more cohesive visual design. The ColorCache avoids recalculating the same color combinations repeatedly (each calendar's style is used for many events), and the 1-hour expiry via AbstractCache allows theme changes to propagate without restart. The CLASSIC mode was added retroactively (commit 5a2c98523) as a user option for those preferring the simpler high-contrast approach from earlier versions.