Skip to main content

Layout Definition & Switching

Overview

FlorisBoard uses a flexible JSON-based layout system that allows dynamic loading, caching, and switching between different keyboard layouts for various languages and input modes.

Introduction

The layout system is one of FlorisBoard's most powerful features, enabling support for 100+ keyboard layouts across multiple languages. Layouts are defined in JSON files, loaded asynchronously, cached for performance, and can be extended through the extension system.

Key Concepts

Layout Types

FlorisBoard supports multiple layout types for different input scenarios:

enum class LayoutType(val id: String) {
CHARACTERS(LayoutTypeId.CHARACTERS), // Main character layout (QWERTY, AZERTY, etc.)
CHARACTERS_MOD(LayoutTypeId.CHARACTERS_MOD), // Modifier row for character layout
EXTENSION(LayoutTypeId.EXTENSION), // Extension layouts
NUMERIC(LayoutTypeId.NUMERIC), // Numeric keypad
NUMERIC_ADVANCED(LayoutTypeId.NUMERIC_ADVANCED), // Advanced numeric with symbols
NUMERIC_ROW(LayoutTypeId.NUMERIC_ROW), // Number row above main layout
PHONE(LayoutTypeId.PHONE), // Phone number pad
PHONE2(LayoutTypeId.PHONE2), // Alternative phone layout
SYMBOLS(LayoutTypeId.SYMBOLS), // Symbol layout
SYMBOLS_MOD(LayoutTypeId.SYMBOLS_MOD), // Modifier row for symbols
SYMBOLS2(LayoutTypeId.SYMBOLS2), // Secondary symbol layout
SYMBOLS2_MOD(LayoutTypeId.SYMBOLS2_MOD); // Modifier for secondary symbols
}

Layout Arrangement

A layout arrangement is a 2D array of key definitions:

typealias LayoutArrangement = List<List<AbstractKeyData>>

Each layout is organized as:

  • Rows: Horizontal rows of keys
  • Keys: Individual key definitions with properties like code, label, type, popup keys

LayoutManager

The LayoutManager is responsible for:

  • Loading: Asynchronously loading layouts from JSON files
  • Caching: Caching loaded layouts to avoid repeated parsing
  • Merging: Combining main, modifier, and extension layouts
  • Popup Mappings: Loading long-press popup key mappings
class LayoutManager(context: Context) {
private val layoutCache: HashMap<LTN, DeferredResult<CachedLayout>> = hashMapOf()
private val layoutCacheGuard: Mutex = Mutex(locked = false)
private val popupMappingCache: HashMap<ExtensionComponentName, DeferredResult<CachedPopupMapping>> = hashMapOf()
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

fun computeKeyboardAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
): Deferred<TextKeyboard>
}

Layout Components

@Serializable
data class LayoutArrangementComponent(
override val id: String,
override val label: String,
override val authors: List<String>,
val direction: String, // "ltr" or "rtl"
val modifier: ExtensionComponentName? = null, // Optional modifier layout
val arrangementFile: String? = null, // Path to layout JSON
) : ExtensionComponent {
fun arrangementFile(type: LayoutType) = arrangementFile ?: "layouts/${type.id}/$id.json"
}

Implementation Details

Layout JSON Structure

Basic Character Layout

[
[
{ "$": "auto_text_key", "code": 113, "label": "q" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 106, "label": "j" },
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 112, "label": "p" }
],
...
]

Modifier Layout

[
[
{ "code": -11, "label": "shift", "type": "modifier" },
{ "code": 0, "type": "placeholder" },
{ "code": -7, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "$": "variation_selector",
"default": { "code": 44, "label": ",", "groupId": 1 },
"email": { "code": 64, "label": "@", "groupId": 1 },
"uri": { "code": 47, "label": "/", "groupId": 1 }
},
{ "code": -227, "label": "language_switch", "type": "system_gui" },
{ "code": -212, "label": "ime_ui_mode_media", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 46, "label": ".", "groupId": 2 },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]

Layout Loading Process

private fun loadLayoutAsync(ltn: LTN?, allowNullLTN: Boolean) = ioScope.runCatchingAsync {
if (ltn == null) return@runCatchingAsync null

layoutCacheGuard.withLock {
val cached = layoutCache[ltn]
if (cached != null) {
flogDebug(LogTopic.LAYOUT_MANAGER) { "Using cache for '${ltn.name}'" }
return@withLock cached
} else {
flogDebug(LogTopic.LAYOUT_MANAGER) { "Loading '${ltn.name}'" }
val meta = keyboardManager.resources.layouts.value?.get(ltn.type)?.get(ltn.name)
?: error("No indexed entry found for ${ltn.type} - ${ltn.name}")
val ext = extensionManager.getExtensionById(ltn.name.extensionId)
?: error("Extension ${ltn.name.extensionId} not found")
val path = meta.arrangementFile(ltn.type)
val layout = async {
runCatching {
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
val arrangement = loadJsonAsset<LayoutArrangement>(jsonStr).getOrThrow()
CachedLayout(ltn.type, ltn.name, meta, arrangement)
}
}
layoutCache[ltn] = layout
return@withLock layout
}
}.await().getOrThrow()
}

Layout Merging

FlorisBoard merges three types of layouts:

  1. Main Layout: The primary character/symbol layout
  2. Modifier Layout: Bottom row with space, enter, mode switches
  3. Extension Layout: Optional number row or other extensions
fun computeKeyboardAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
): Deferred<TextKeyboard> = ioScope.async {
var main: LTN? = null
var modifier: LTN? = null
var extension: LTN? = null

when (keyboardMode) {
KeyboardMode.CHARACTERS -> {
if (prefs.keyboard.numberRow.get()) {
extension = LTN(LayoutType.NUMERIC_ROW, subtype.layoutMap.numericRow)
}
main = LTN(LayoutType.CHARACTERS, subtype.layoutMap.characters)
modifier = LTN(LayoutType.CHARACTERS_MOD, extCoreLayout("default"))
}
KeyboardMode.SYMBOLS -> {
extension = LTN(LayoutType.NUMERIC_ROW, subtype.layoutMap.numericRow)
main = LTN(LayoutType.SYMBOLS, subtype.layoutMap.symbols)
modifier = LTN(LayoutType.SYMBOLS_MOD, extCoreLayout("default"))
}
// ... other modes
}

return@async mergeLayouts(keyboardMode, subtype, main, modifier, extension)
}

Extension Metadata

{
"$": "ime.extension.keyboard",
"meta": {
"id": "org.florisboard.layouts",
"version": "0.1.0",
"title": "Default layouts",
"description": "Default layouts which are always available.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"license": "apache-2.0"
},
"layouts": {
"characters": [
{
"id": "arabic",
"label": "Arabic",
"authors": [ "HeiWiper" ],
"direction": "rtl",
"modifier": "org.florisboard.layouts:arabic"
},
{
"id": "qwerty",
"label": "QWERTY",
"authors": [ "patrickgold" ],
"direction": "ltr"
}
]
}
}

Code Examples

Creating a Custom Layout

[
[
{ "$": "auto_text_key", "code": 97, "label": "a", "popup": {
"relevant": [
{ "code": 224, "label": "à" },
{ "code": 225, "label": "á" },
{ "code": 226, "label": "â" }
]
}},
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 99, "label": "c" }
],
[
{ "code": 32, "label": "space" },
{ "code": 10, "label": "enter", "type": "enter_editing" }
]
]

Key Types and Special Keys

{
"code": -11,
"label": "shift",
"type": "modifier"
}

{
"code": -7,
"label": "delete",
"type": "enter_editing"
}

{
"code": -202,
"label": "view_symbols",
"type": "system_gui"
}

{
"code": 0,
"type": "placeholder"
}

Conditional Keys (Selectors)

{
"$": "case_selector",
"lower": { "code": 59, "label": ";" },
"upper": { "code": 58, "label": ":" }
}

{
"$": "variation_selector",
"default": { "code": 44, "label": "," },
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" }
}

Japanese Kana Layout

{
"$": "kana_selector",
"hira": { "code": 12396, "label": "ぬ" },
"kata": { "$": "char_width_selector",
"full": { "code": 12492, "label": "ヌ" },
"half": { "code": 65415, "label": "ヌ" }
}
}

Best Practices

1. Use Selectors for Context-Aware Keys

{
"$": "variation_selector",
"default": { "code": 46, "label": "." },
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" }
}

2. Organize Popup Keys Logically

{
"code": 101,
"label": "e",
"popup": {
"relevant": [
{ "code": 232, "label": "è" },
{ "code": 233, "label": "é" },
{ "code": 234, "label": "ê" },
{ "code": 235, "label": "ë" }
]
}
}

3. Use Placeholders for Flexible Layouts

{ "code": 0, "type": "placeholder" }

Placeholders expand to fill available space, allowing flexible key sizing.

4. Specify Direction for RTL Languages

{
"id": "arabic",
"label": "Arabic",
"direction": "rtl",
"modifier": "org.florisboard.layouts:arabic"
}
{ "code": 44, "label": ",", "groupId": 1 },
{ "code": 46, "label": ".", "groupId": 2 },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }

Groups control key sizing and spacing.

Common Patterns

Multi-Language Support

val subtype = Subtype(
id = System.currentTimeMillis(),
primaryLocale = FlorisLocale.from("fr", "FR"),
layoutMap = SubtypeLayoutMap(
characters = extCoreLayout("french/azerty"),
symbols = extCoreLayout("symbols"),
numeric = extCoreLayout("numeric")
)
)

Dynamic Layout Switching

keyboardManager.activeState.keyboardMode = when (editorInfo.inputType) {
InputType.TYPE_CLASS_NUMBER -> KeyboardMode.NUMERIC
InputType.TYPE_CLASS_PHONE -> KeyboardMode.PHONE
else -> KeyboardMode.CHARACTERS
}

Custom Modifier Layouts

{
"id": "custom_layout",
"label": "Custom",
"direction": "ltr",
"modifier": "org.florisboard.layouts:custom_modifier"
}

Troubleshooting

Layout Not Loading

Problem: Custom layout doesn't appear in keyboard.

Solutions:

  • Verify JSON syntax is valid
  • Check extension.json includes layout metadata
  • Ensure layout file is in correct directory
  • Verify extension is properly installed
  • Check logs for parsing errors

Keys Not Displaying Correctly

Problem: Keys show wrong characters or labels.

Solutions:

  • Verify Unicode code points are correct
  • Check font supports the characters
  • Ensure proper encoding (UTF-8)
  • Test with different themes

Problem: Long-press doesn't show popup keys.

Solutions:

  • Verify popup mapping is loaded
  • Check popup key definitions in JSON
  • Ensure long-press duration is configured
  • Verify key has popup property

Layout Caching Issues

Problem: Layout changes don't appear after modification.

Solutions:

  • Clear app cache
  • Force reload extension
  • Restart keyboard service
  • Check cache invalidation logic

Next Steps


Note: This documentation is continuously being improved. Contributions are welcome!