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 = 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 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) ) } } }