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>
112 lines
4.8 KiB
Kotlin
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)
|
|
)
|
|
}
|
|
}
|
|
}
|