MMTextKit
0.1.0-alphaindexedVertical typesetting editor supporting mixed Mongolian/Manchu and CJK/Kana/Hangul/Latin, with correct vertical rendering, cursor/selection, selection handles, undo/redo, multi-column flow, theming and context-menu hooks.
Vertical typesetting editor supporting mixed Mongolian/Manchu and CJK/Kana/Hangul/Latin, with correct vertical rendering, cursor/selection, selection handles, undo/redo, multi-column flow, theming and context-menu hooks.
A Compose Multiplatform text editor component for vertical typesetting — Mongolian, Manchu, Chinese, Japanese, Korean, and Latin mixed layout, rendered correctly in a single column or multi-column vertical flow.
Compose Multiplatform 竖排文字编辑器组件,支持蒙古文、满文、汉字、日文、韩文、英文混排,单列或多列竖排显示。
| Platform | Status |
|---|---|
| Android | ✅ |
| iOS | ✅ |
| Desktop (JVM) | ✅ |
The library module packaging is in progress. For now, copy the source into your project or use a local Maven publication.
库模块打包进行中,目前可将源码复制到项目中使用,或通过本地 Maven 发布引入。
var value by remember { mutableStateOf(TextFieldValue("汉字\nᠮᠣᠩᠭᠣᠯ ᠪᠢᠴᠢᠭ")) }
VTextField(
value = value,
onValueChange = { value = it },
modifier = Modifier.fillMaxSize(),
)
// In commonMain resources: composeApp/src/commonMain/composeResources/font/NotoSansMongolian_Regular.ttf
val mongolianFont = FontFamily(Font(Res.font.NotoSansMongolian_Regular))
VTextField(
value = value,
onValueChange = { value = it },
verticalFontFamily = mongolianFont,
style = TextStyle(fontSize = 24.sp, color = Color.Black),
modifier = Modifier.fillMaxSize(),
)
// maxColumns = 1 → single column, scrolls vertically on overflow
VTextField(
value = value,
onValueChange = { value = it },
verticalFontFamily = mongolianFont,
maxColumns = 1,
modifier = Modifier.height(200.dp),
)
The library fires onShowContextMenu on long-press, double-tap, drag-select end, and handle drag end. Implement your own menu UI:
库在长按、双击、拖选结束、把手拖动结束时触发 onShowContextMenu,由调用方实现菜单 UI:
var showMenu by remember { mutableStateOf(false) }
var menuPos by remember { mutableStateOf(Offset.Zero) }
Box {
VTextField(
value = value,
onValueChange = { value = it },
verticalFontFamily = mongolianFont,
modifier = Modifier.fillMaxSize(),
onShowContextMenu = { position, hasSelection ->
menuPos = position
showMenu = true
},
)
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
offset = DpOffset(menuPos.x.dp, menuPos.y.dp),
) {
// Copy, Cut, Paste, Select All ...
}
}
The library does not manage keyboard dismissal — handle it at the app level. Two common approaches, both following Android/iOS platform conventions:
库不管键盘收起,在 app 层处理。下面两种方式均为 Android / iOS 平台惯例:
Standard on both Android and iOS. Wrap the editor in a Box that clears focus on tap:
Android / iOS 通用标准交互,在外层 Box 点击时清除焦点即可收起键盘:
val focusManager = LocalFocusManager.current
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) { detectTapGestures { focusManager.clearFocus() } }
) {
VTextField(...)
}
This is the iOS standard inputAccessoryView pattern (Done button above the keyboard, as seen in Notes, Mail, WeChat). On Android it behaves identically. Use WindowInsets.ime.getBottom(density) as an upward offset to stick the toolbar above the keyboard:
这是 iOS 标准的 inputAccessoryView 交互(键盘上方 Done 按钮,备忘录、邮件、微信均如此),Android 上行为一致。用 WindowInsets.ime.getBottom(density) 作为向上 offset 把工具栏贴在键盘上方:
Use
WindowInsets.ime.getBottom(density)as the upward so the toolbar sticks to the keyboard. adds inner padding and won't push the component above the keyboard when it's aligned to the bottom. Desktop has no soft keyboard (), so the toolbar won't show.
VTextFieldVTextFieldColorsVTextFieldDefaults.colors(
cursor: Color = MaterialTheme.colorScheme.primary,
selectionHandle: Color = MaterialTheme.colorScheme.primary,
selectionBackground: Color = LocalTextSelectionColors.current.backgroundColor,
)
Correct rendering of Mongolian and Manchu script requires a font with proper OpenType vertical features (vert, vrt2).
蒙古文/满文的正确渲染依赖支持 OpenType 竖排特性(vert、vrt2)的字体。
Android requires a bundled font. Other platforms are recommended to bundle one for best results.
Android 必须自带字体,其他平台建议自带以获得最佳效果。
Noto Sans Mongolian — Apache 2.0 license, free for commercial use, covers Mongolian, Manchu, Sibe, Todo, and more.
Noto Sans Mongolian — Apache 2.0 协议,免费商用,覆盖蒙古文、满文、锡伯文、托忒文等。
Download NotoSansMongolian-Regular.ttf and place it in:
composeApp/src/commonMain/composeResources/font/NotoSansMongolian_Regular.ttf
Use it:
// In a @Composable
val mongolianFont = FontFamily(Font(Res.font.NotoSansMongolian_Regular))
VTextField(
value = value,
onValueChange = { value = it },
verticalFontFamily = mongolianFont,
)
verticalFontFamilyis applied only to Mongolian / Manchu code point ranges. All other characters (CJK, Latin, Emoji, etc.) use the font fromstyle.
verticalFontFamily仅应用于蒙古文/满文码点区间,其他字符(汉字、拉丁文、Emoji 等)使用style中的字体。
Mongolian and Manchu script is stored "lying on its side" in Unicode — characters are encoded for horizontal rendering but displayed vertically. MMTextKit uses Compose's TextMeasurer to measure text horizontally, then rotates each "line" 90° to form a vertical column.
蒙古文/满文在 Unicode 中是"侧卧"存储的——字符按横排编码,但需要竖排显示。MMTextKit 使用 Compose 的 TextMeasurer 横向测量文字,再将每"行"旋转 90° 作为竖排的一"列"。
Upright characters (CJK, Kana, Hangul, Emoji): rotated back −90° after the column rotation, so they appear upright.
Mongolian / Manchu characters: follow the column rotation, preserving their natural vertical flow and ligatures.
直立字符(汉字、假名、韩文、Emoji):在列旋转后再反向旋转 −90°,保持直立。
蒙古文/满文字符:随列旋转,保持自然的竖排连写形态。
MIT © 2026 lzdev42
maxColumns — limit visible columns, scroll horizontally for overflowenabled / readOnly / selectableVTextFieldColors (follows MaterialTheme by default)onShowContextMenu — implement your own menu UIval focusManager = LocalFocusManager.current
val density = LocalDensity.current
var isFocused by remember { mutableStateOf(false) }
Box(Modifier.fillMaxSize()) {
VTextField(
value = value,
onValueChange = { value = it },
modifier = Modifier
.fillMaxSize()
.onFocusChanged { isFocused = it.isFocused },
// Optional: IME action Done also dismisses keyboard
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
)
// Toolbar floating above the keyboard
// Use IME insets bottom as upward offset so the toolbar sticks to the keyboard
// Desktop has no soft keyboard, imeBottom is always 0, toolbar won't show
val imeBottomPx = WindowInsets.ime.getBottom(density)
val imeBottomDp = with(density) { imeBottomPx.toDp() }
if (isFocused && imeBottomPx > 0) {
Surface(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.offset(y = -imeBottomDp),
color = MaterialTheme.colorScheme.surfaceVariant,
) {
Box(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterEnd,
) {
TextButton(onClick = { focusManager.clearFocus() }) {
Text("完成")
}
}
}
}
}
offsetModifier.imePadding()imeBottom == 0用 WindowInsets.ime.getBottom(density) 作为向上 offset,让 toolbar 贴在键盘上方。Modifier.imePadding() 加的是内边距,组件 align(BottomCenter) 时不会把组件推到键盘上方。桌面端无软键盘(imeBottom == 0),toolbar 不会显示。
| Parameter | Type | Default | Description |
|---|
value | TextFieldValue | — | Current text and selection state / 当前文本与选区 |
onValueChange | (TextFieldValue) -> Unit | — | Called on text or selection change / 文本或选区变化回调 |
verticalFontFamily | FontFamily | FontFamily.Default | Font for Mongolian / Manchu script only / 仅用于蒙古文/满文区段的字体 |
style | TextStyle | TextStyle.Default | Text style (size, color, etc.) / 文字样式 |
colors | VTextFieldColors | VTextFieldDefaults.colors() | Cursor / selection / handle colors / 光标、选区、把手颜色 |
placeholder | String | "" | Shown when text is empty / 文字为空时显示 |
placeholderStyle | TextStyle? | null | Placeholder text style; falls back to style.color.copy(alpha=0.4f) when null / 占位提示样式,null 时用半透明默认 |
maxColumns | Int | Int.MAX_VALUE | Max visible columns; overflow scrolls horizontally / 最多显示几列,超出横向滚动 |
enabled | Boolean | true | If false, no interaction / 为 false 时不可交互 |
readOnly | Boolean | false | Selectable but not editable / 可选取但不可编辑 |
selectable | Boolean | true | Whether text can be selected / 是否允许选取 |
keyboardOptions | KeyboardOptions | KeyboardOptions.Default | Keyboard config (type, capitalization, autoCorrect, imeAction) / 键盘配置(类型、大写、自动纠错、IME action) |
keyboardActions | KeyboardActions | KeyboardActions.Default | IME action callbacks (e.g. onDone to dismiss keyboard) / IME action 回调(如 onDone 收起键盘) |
onTextLayout | (TextLayoutResult) -> Unit | {} | Layout result callback (horizontal measurement before rotation) / 布局完成回调(旋转前的横排测量结果) |
interactionSource | MutableInteractionSource? | null | Interaction state source, forwarded to BasicTextField / 交互状态源,透传给 BasicTextField |
cursorBrush | Brush? | null | Custom cursor brush (supports gradients); falls back to colors.cursor when null / 自定义光标画笔(支持渐变),为 null 时用 colors.cursor |
onShowContextMenu | ((Offset, Boolean) -> Unit)? | null | Context menu trigger; Boolean = hasSelection / 菜单触发回调,Boolean 表示是否有选区 |
ascentTrim | Float | 0.15f | Trim ascent to reduce inter-column spacing / 削减 Ascent 调整列间距 |
baselineShift | Float | 0f | Baseline shift / 基线偏移 |
fixedWidth | Boolean | true | All columns same width (max line height) / 等宽列 |
| Parameter | Reason |
|---|
label / leadingIcon / trailingIcon / supportingText / prefix / suffix / shape | VTextField is a BasicTextField equivalent; Material decorations should be wrapped by the caller / VTextField 定位为 BasicTextField 等价物,装饰由调用方外层包装 |
isError | No Material decoration to reflect error state; use style.color instead / 无 Material 装饰框,错误状态用 style.color 表达 |
visualTransformation | Conflicts with rotated+upright rendering pipeline / 与旋转+直立补偿渲染管线冲突 |
TextFieldState (new API) | CMP 1.11 support still evolving; migration would break existing callers / CMP 支持仍在迭代,迁移会破坏调用方 |
| Platform | Built-in Mongolian font | Quality |
|---|
| Android | ❌ None | Renders as boxes without a custom font |
| iOS 12+ | ✅ Mongolian Baiti | Partial — Manchu / Sibe incomplete |
| macOS | ✅ Mongolian Baiti | Partial |
| Windows | ✅ Mongolian Baiti | Partial |
Surfaced from shared tags and platforms — no rankings paid for.