Files
wIRC-desktop/src/main/kotlin/ui/AppFonts.kt
T
weishauptandClaude Opus 5.5 efad80e141 Add font picker (Segoe UI, Tahoma, Tw Cen MT, Nunito, Quicksand, Varela Round), bump to 0.1.10
Each channel window's settings panel gets a Font list, and wIRC main gets a Font
button that opens the same list for the server messages box. Every name is drawn
in its own font as a preview. The choice is saved per window in window_prefs.json
(fontId) and applies to the scrollback, user list, input box and title.

- Segoe UI, Tahoma and Tw Cen MT are Microsoft-licensed, so they aren't bundled:
  they load from the Windows font folders at runtime and are only listed when
  present (Tw Cen MT comes with Office).
- Nunito, Quicksand and Varela Round are SIL OFL fonts, bundled in
  resources/fonts with their license files.
- A missing or unloadable font falls back to Monospace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:18:31 -04:00

112 lines
4.8 KiB
Kotlin

package ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font
import java.io.File
/**
* Fonts offered in each window's settings. Stored in window_prefs.json by [id], so the ids
* must never change once shipped.
*
* Two kinds:
* - Windows fonts (Segoe UI, Tahoma, Tw Cen MT) are Microsoft-licensed, so they are NOT
* bundled. They are loaded straight out of the Windows font folders at runtime and only
* listed when the file is actually there (Tw Cen MT only exists if Office is installed).
* - Rounded fonts (Nunito, Quicksand, Varela Round) are SIL OFL licensed and bundled in
* src/main/resources/fonts with their license files.
*
* Anything missing or failing to load falls back to Monospace, so a font uninstalled after
* being picked can never crash a window.
*/
data class FontOption(val id: String, val label: String, val family: FontFamily)
object AppFonts {
const val DEFAULT_ID = "monospace"
private val fontDirs: List<File> = buildList {
add(File(System.getenv("WINDIR") ?: "C:\\Windows", "Fonts"))
System.getenv("LOCALAPPDATA")?.let { add(File(it, "Microsoft\\Windows\\Fonts")) }
}
/** Case-insensitive lookup across the system and per-user font folders. */
private fun findSystemFont(name: String): File? {
for (dir in fontDirs) {
val direct = File(dir, name)
if (direct.isFile) return direct
dir.listFiles()?.firstOrNull { it.name.equals(name, ignoreCase = true) }?.let { return it }
}
return null
}
private fun systemFamily(regular: String, bold: String?): FontFamily? {
val reg = findSystemFont(regular) ?: return null
return try {
val fonts = mutableListOf(Font(reg, FontWeight.Normal))
bold?.let { findSystemFont(it) }?.let { fonts.add(Font(it, FontWeight.Bold)) }
FontFamily(fonts)
} catch (e: Exception) {
null
}
}
private fun bundledFamily(regular: String, bold: String?): FontFamily? = try {
val fonts = mutableListOf(Font("fonts/$regular", FontWeight.Normal))
bold?.let { fonts.add(Font("fonts/$it", FontWeight.Bold)) }
FontFamily(fonts)
} catch (e: Exception) {
null
}
/** Built once at startup; the font folders don't change while wIRC is running. */
val options: List<FontOption> by lazy {
buildList {
add(FontOption(DEFAULT_ID, "Monospace", FontFamily.Monospace))
systemFamily("segoeui.ttf", "segoeuib.ttf")?.let { add(FontOption("segoe-ui", "Segoe UI", it)) }
systemFamily("tahoma.ttf", "tahomabd.ttf")?.let { add(FontOption("tahoma", "Tahoma", it)) }
systemFamily("TCM_____.TTF", "TCB_____.TTF")?.let { add(FontOption("tw-cen-mt", "Tw Cen MT", it)) }
bundledFamily("Nunito-Regular.ttf", "Nunito-Bold.ttf")?.let { add(FontOption("nunito", "Nunito (rounded)", it)) }
bundledFamily("Quicksand-Regular.ttf", "Quicksand-Bold.ttf")?.let { add(FontOption("quicksand", "Quicksand (rounded)", it)) }
bundledFamily("VarelaRound-Regular.ttf", null)?.let { add(FontOption("varela-round", "Varela Round (rounded)", it)) }
}
}
fun familyFor(id: String): FontFamily =
options.firstOrNull { it.id == id }?.family ?: FontFamily.Monospace
}
/**
* The font list used in both the channel-window settings and wIRC main's Font dialog. Each
* name is drawn in its own font as a live preview; plain square-cornered rows, no dropdown.
*/
@Composable
fun FontPickerList(selectedId: String, textColor: Color, onPick: (String) -> Unit) {
val effectiveId = if (AppFonts.options.any { it.id == selectedId }) selectedId else AppFonts.DEFAULT_ID
Column {
AppFonts.options.forEach { option ->
Text(
text = option.label,
color = textColor,
fontFamily = option.family,
fontSize = 15.sp,
modifier = Modifier
.fillMaxWidth()
.background(if (option.id == effectiveId) Color.White.copy(alpha = 0.18f) else Color.Transparent)
.clickable { onPick(option.id) }
.padding(horizontal = 6.dp, vertical = 4.dp)
)
}
}
}