English · Türkçe
A declarative, cross-platform UI library for C++20 in the style of Jetpack Compose: the same mental model (composable functions, state, recomposition, modifiers), the same names where C++ allows, and Material 3 components that look and behave like Compose's. It runs on Linux, Windows, Android and the web from one code base.
Version 0.1.1. The API follows Compose closely, but it may still change. Licensed under the Apache License 2.0.
#include <kompoz/kompoz.hpp>
using namespace kompoz;
void Counter() {
auto count = remember([] { return mutableStateOf(0); });
Column(Modifier().padding(16), verticalArrangement = Arrangement::spacedBy(8)) {
Text("Count: " + std::to_string(count.get()), style = typography().titleLarge);
Button([count] { count.set(count.get() + 1); }) { Text("Increment"); };
};
}
kompoz::Application kompoz::createApplication(int, char**) {
return {.title = "Demo", .content = [] { MaterialTheme(lightColorScheme(), Counter); }};
}- Runtime: composable functions with smart recomposition (only what read a changed state runs again;
unchanged children are skipped),
remember/rememberSaveable, effects,KompozitionLocal, C++20 coroutines (LaunchedEffect,delay,Flow/StateFlow),ViewModel, navigation with type-safe routes and deep links, lifecycle. - Layout: incremental (a node is measured again only when something in it changed),
Box/Row/Column,FlowRow,FlexBox,Grid, customLayoutandSubkompozLayout, intrinsics, window insets, right to left. - Lists and scrolling:
LazyColumn/LazyRow/lazy grids/staggered grids, pagers, carousels, nested scroll, Android's fling curve and overscroll stretch, snapping, the mouse wheel and touchpads. - Material 3 (Compose Material 3 1.4, including the Expressive components): Scaffold and app bars,
navigation bars, rails and drawers,
NavigationSuiteScaffold, buttons and button groups, cards, chips, lists, sliders, switches, tabs, text fields, date and time pickers, dialogs, menus, sheets, snackbars, tooltips, search bars, progress and loading indicators, carousels,MaterialThemewith dynamic color. - Drawing and text:
Canvas/DrawScope, brushes, paths, any shape as a clip or outline, blurred shadows,graphicsLayer, vector images,AsyncImage(JPEG, PNG, WebP, over HTTP), HarfBuzz text shaping, bidirectional text,AnnotatedString, selection, emoji and system fallback fonts. - Input: touch, multi-touch gestures, mouse (hover, right click, text selection by mouse), keyboard focus and shortcuts, the software keyboard with suggestions, drag and drop, the predictive back gesture.
- Accessibility: a semantics tree that TalkBack reads on Android, and screen readers read through AT-SPI on Linux (Orca) and UI Automation on Windows (Narrator, NVDA): labels, roles, states, actions, custom actions, live regions.
- Platform integration: the system's dark mode; dynamic color, from a source the app (or its user) picks: the
system's palette (the wallpaper's on Android 12+, the desktop's accent color on Linux and Windows) or the wallpaper itself (Linux
desktops, Windows, Android 8.1+); system font scale; the system language (the library's own texts in English or
Turkish,
LocalStrings, and right-to-left layouts); clipboard, haptics, URIs, the IME. - Tools: headless UI tests (
KompozTestRule), previews (KOMPOZ_PREVIEW), rebuild-and-restart on save that keeps the app's state (scripts/dev.sh), scripted screenshots, frame profiling, packaging for every platform (scripts/package.sh).
The full list, with the status of each Compose API, is in docs/components.md.
| Compose | Kompoz |
|---|---|
@Kompozable + compiler plugin |
Every composable takes a default CallSite site = CallSite::current() parameter (std::source_location). The call site plus the order within the parent is the identity |
| Restartable / skippable composable | kompozable(site, args..., [=]{ ... }): runs again on its own when a state it read changes; skipped when its arguments are equal |
Inline composable (Column, Row) |
KompozableScope scope(site);: gives an identity without a scope of its own (for composables that take a content lambda) |
remember { }, remember(key) { } |
remember([]{...}), remember(key, []{...}) |
mutableStateOf |
mutableStateOf(x) → MutableState<T> (a copyable handle; capture it by value in callbacks) |
key(id) { } |
key(id, [&]{...}) |
DisposableEffect, SideEffect |
The same names |
KompozitionLocal |
KompozitionLocal<T>, KompozitionLocalProvider({Local.provides(v)}, ...) |
animate*AsState |
animateFloatAsState, animateColorAsState (only their own scope runs each frame) |
LaunchedEffect, rememberCoroutineScope, delay |
C++20 coroutines: LaunchedEffect(key, [=]() -> Task { co_await delay(1s); ... }) |
Animatable, tween, spring, infiniteRepeatable |
co_await anim.animateTo(1.f, tween(300)), the same spec names |
AnimatedVisibility, AnimatedContent, Crossfade |
The same names; fadeIn() + expandVertically(), scaleIn(), togetherWith |
NavHost(nav, "home") { kompozable("detail/{id}") { e -> } }, nav.navigate("detail/3") |
NavHost(nav, "home", m, [&](NavGraphBuilder& g) { g.kompozable("detail/{id}", [&](const NavBackStackEntry& e) { e.arguments().getInt("id"); }); }), nav.navigate("detail/3", {.popUpTo = "home", .saveState = true, .restoreState = true}) |
@Serializable data class Profile(val id: String, val tab: Int? = null), kompozable<Profile> { e -> e.toRoute<Profile>() }, nav.navigate(Profile("42")) |
struct Profile { std::string id; std::optional<int> tab; KOMPOZ_ROUTE(Profile, id, tab) }; (required fields are the path, std::optionals query parameters), g.kompozable<Profile>([&](const NavBackStackEntry& e) { e.toRoute<Profile>(); }), nav.navigate(Profile{.id = "42"}), navigation<Login>(Username{}, …), hasRoute<T>(), getBackStackEntry<T>(), navDeepLink<T>("kompoz://profile") |
kompozable("item/{n}", deepLinks = listOf(navDeepLink { uriPattern = "kompoz://item/{n}" })) { … } |
g.kompozable("item/{n}", {navDeepLink("kompoz://item/{n}")}, [&](const NavBackStackEntry& e) { … }); nav.navigate("kompoz://item/3"), nav.handleDeepLink(...) |
navigation(startDestination = "username", route = "login") { kompozable("username") { … } } |
g.navigation("username", "login", [&](NavGraphBuilder& login) { login.kompozable("username", [&](const NavBackStackEntry& e) { auto vm = viewModel<LoginViewModel>(*e.parent()); }); }) |
class SearchViewModel(private val state: SavedStateHandle) : ViewModel(), state.getStateFlow("q", ""), state["q"] = q |
explicit SearchViewModel(SavedStateHandle s) : state(s), query(s.getStateFlow<std::string>("q", "")) {}, state.set("q", q) (viewModel<SearchViewModel>() passes the handle; it survives the process being ended) |
rememberSaveable { mutableStateOf(0) } |
rememberSaveable([] { return mutableStateOf(0); }) (also when Android ends the process and brings it back) |
Saver(save = { … }, restore = { … }) |
template <> struct kompoz::Saver<Point> { static SavedValue save(const Point&); static std::optional<Point> restore(const SavedValue&); }; |
AsyncImage(model = url, contentDescription = "…", placeholder = …, contentScale = ContentScale.Crop) (Coil) |
AsyncImage(url, "…", Modifier().size(120), {.placeholder = [] { … }, .error = [] { … }, .contentScale = ContentScale::Crop, .crossfadeMillis = 200}); (an asset path or http(s); JPEG, PNG, WebP; decoded at the box's size; downloads are kept on disk) |
val vm: MyViewModel = viewModel() |
auto vm = viewModel<MyViewModel>(); (class MyViewModel : public ViewModel, viewModelScope(), onCleared()) |
SharedTransitionLayout { AnimatedContent(x) { Modifier.sharedElement(rememberSharedContentState(key), this@AnimatedContent) } } |
SharedTransitionLayout(m, [&](const SharedTransitionScope& s) { AnimatedContent<T>(x, [&](const AnimatedContentScope& scope, const T& v) { Box(Modifier().sharedElement(s.rememberSharedContentState("key"), scope)); }); }) |
derivedStateOf, mutableStateListOf, rememberUpdatedState, produceState |
The same names (state.hpp, coroutines.hpp) |
LazyColumn { items(list, key) { ... } }, stickyHeader { } |
LazyColumn(modifier) { stickyHeader([] { ... }); items(data, key, content); }; |
LazyColumn(reverseLayout = true), listState.animateScrollToItem(0), scrollState.animateScrollTo(0) |
LazyColumn(modifier, state = list, reverseLayout = true) { ... };, scope.launch([list]() -> Task { co_await list.animateScrollToItem(0); }), co_await scroll.animateScrollTo(0) |
LazyVerticalGrid(GridCells.Adaptive(96)), HorizontalPager/VerticalPager |
LazyVerticalGrid(GridCells::Adaptive(96), modifier) { items(...); };, HorizontalPager(rememberPagerState(0, count), modifier) [=](int page) {...}; |
val grid = rememberLazyGridState(); LazyVerticalGrid(GridCells.Fixed(3), state = grid), grid.animateScrollToItem(0), grid.layoutInfo.visibleItemsInfo |
auto grid = rememberLazyGridState(); LazyVerticalGrid(GridCells::Fixed(3), Modifier(), state = grid) { … };, co_await grid.animateScrollToItem(0), grid.layoutInfo().visibleItemsInfo |
HorizontalMultiBrowseCarousel(state = rememberCarouselState { n }, preferredItemWidth = 186.dp) { i -> … } |
HorizontalMultiBrowseCarousel(rememberCarouselState(0, [&] { return n; }), 186, Modifier().height(221), {.itemSpacing = 8}, [&](int i) { … }) |
Modifier.nestedScroll(connection), TopAppBarDefaults.enterAlwaysScrollBehavior() |
Modifier().nestedScroll(rememberNestedScrollConnection({.onPreScroll = [](Offset available, NestedScrollSource) { ... }})); auto s = TopAppBarDefaults::enterAlwaysScrollBehavior(); Scaffold(Modifier().nestedScroll(s.nestedScrollConnection), topBar = [&] { TopAppBar(..., scrollBehavior = s); }) [=](PaddingValues padding) { ... };, LargeTopAppBar |
BoxWithConstraints, onSizeChanged, animateContentSize, updateTransition, FlowRow |
The same names |
Layout(content) { measurables, constraints -> }, Modifier.layout { } |
Layout(modifier, content, [](const std::vector<Measurable>& m, const Constraints& c) { ... return Size{w, h}; }), Modifier().layout(...), height(IntrinsicSize::Min) |
FontFamily.Serif, FontFamily(Font(R.font.x)) |
{.fontFamily = FontFamily::Serif}, FontFamily::fromFiles({{"fonts/x.ttf"}, {"fonts/x-bold.ttf", FontWeight::Bold}}) |
buildAnnotatedString { withStyle(SpanStyle(...)) { append(...) } } |
buildAnnotatedString([&](AnnotatedString::Builder& b) { b.withStyle({.fontWeight = FontWeight::Bold}, [&] { b.append("bold"); }); }), withLink; TextOverflow, FontStyle::Italic, LocalDensity (the system's font scale) |
Canvas, drawBehind, drawWithContent, drawWithCache |
Canvas(modifier, [](DrawScope& d) { d.drawCircle(color); }) |
Brush.verticalGradient(...), Path |
Brush::verticalGradient({a, b}), background(brush, shape), d.drawPath(path, brush, Stroke(3)) |
Modifier.graphicsLayer { rotationZ = ... } |
graphicsLayer({.rotationZ = 45}), or a lambda read while drawing: graphicsLayer([=](GraphicsLayerScope& l) { ... }) |
TriStateCheckbox(state = ToggleableState.Indeterminate, onClick), Text(…, autoSize = TextAutoSize.StepBased(12.sp, 48.sp)) |
TriStateCheckbox(ToggleableState::Indeterminate, onClick), Text("…", maxLines = 1, autoSize = TextAutoSize::StepBased(12, 48)) |
SubkompozLayout { constraints -> val m = subkompoz("main") { … }.map { it.measure(constraints) }; layout(w, h) { … } } |
SubkompozLayout(Modifier(), [](SubkompozMeasureScope& s, const Constraints& c) { auto m = s.subkompoz(std::string("main"), [] { … }); …; return Size{w, h}; }) |
Modifier.clickable(interactionSource = s, indication = null) { … } + Modifier.indication(s, ripple()) |
Modifier().clickable([] { … }, {.interactionSource = s, .indication = false}) + Modifier().indication(s, ripple()) |
Modifier.clickable(indication = ripple(bounded = false, radius = 24.dp)) { … }, KompozitionLocalProvider(LocalIndication provides …) |
Modifier().clickable([] { … }, {.indication = ripple(false, 24)}), KompozitionLocalProvider({LocalIndication.provides(false)}, …) (every clickable has Material's ripple by default) |
val source = remember { MutableInteractionSource() }, val pressed by source.collectIsPressedAsState(), Modifier.clickable(source, indication = null) { … } |
auto source = remember([] { return MutableInteractionSource(); });, bool pressed = source.collectIsPressedAsState();, Modifier().clickable([] { … }, {.interactionSource = source, .indication = false}) (Button(…, interactionSource = source) { … };, Slider(v, set, interactionSource = source), source.interactions().collect(…), pagerState.interactionSource().collectIsDraggedAsState()) |
val state = remember { AnchoredDraggableState(DragValue.Start) }, state.updateAnchors(DraggableAnchors { Start at 0f; End at w }), Modifier.offset { IntOffset(state.requireOffset().roundToInt(), 0) }.anchoredDraggable(state, Orientation.Horizontal) |
auto state = rememberAnchoredDraggableState(DragValue::Start);, state.updateAnchors({{DragValue::Start, 0}, {DragValue::End, w}});, Modifier().offset([state] { return Offset{state.requireOffset(), 0}; }).anchoredDraggable(state, Orientation::Horizontal) (anchored_draggable.hpp) |
val measurer = rememberTextMeasurer(), drawText(measurer.measure("%42", style), topLeft = …), Text(text, onTextLayout = { r -> cut = r.hasVisualOverflow }) |
auto measurer = rememberTextMeasurer();, d.drawText(measurer.measure("%42", {.fontSize = 12}), {x, y}), Text(text, Modifier(), {.maxLines = 2}, [cut](const TextLayoutResult& r) { cut.set(r.hasVisualOverflow()); }) (text_measure.hpp) |
val v = velocityTracker.calculateVelocity(), offset.animateDecay(v.x, rememberSplineBasedDecay()), offset.animateTo(0f, spring(), initialVelocity = v.x), offset.updateBounds(0f, max) |
Offset v = tracker.calculateVelocity();, co_await offset.animateDecay(v.x, splineBasedDecay());, co_await offset.animateTo(0.f, spring(), v.x);, offset.updateBounds(0.f, max); |
AnimatedContent(count, transitionSpec = { if (targetState > initialState) slideInVertically { it } togetherWith slideOutVertically { -it } else … using SizeTransform(clip = false) }), keyframes { durationMillis = 400; 1.3f at 100 } |
AnimatedContent<int>(count, content, Modifier(), [](const int& from, const int& to) { return togetherWith(slideInVertically(to > from ? 1 : -1), slideOutVertically(to > from ? -1 : 1)).withSizeTransform(SizeTransform{.clip = false}); }), keyframes(400, {{100, 1.3f}}) |
AnimatedContent(state, contentAlignment = Alignment.Center, contentKey = { it::class }) { … } |
AnimatedContent<Screen>(state, content, Modifier(), defaultContentTransform(), {.contentAlignment = Alignment::Center, .contentKey = [](const Screen& s) { return uint64_t(s.index()); }}) |
AnimatedVisibility(open, enter = EnterTransition.None, exit = ExitTransition.None) { Box(Modifier.animateEnterExit(enter = slideInVertically { it }, exit = slideOutVertically { it })) } |
AnimatedVisibility(open, Modifier(), EnterTransition::None, ExitTransition::None, [&](const AnimatedVisibilityScope& s) { Box(s.animateEnterExit(slideInVertically(1), slideOutVertically(1))); }) |
Modifier.onGloballyPositioned { c -> offset = c.positionInRoot(); visible = c.boundsInRoot().height > 0 } |
Modifier().onGloballyPositioned([=](const LayoutCoordinates& c) { offset.set(c.positionInRoot()); visible.set(c.boundsInRoot().height > 0); }) |
Row(Modifier.selectable(selected, onClick = { … }, role = Role.RadioButton)) { RadioButton(selected, onClick = null) }, Modifier.matchParentSize() |
Row(Modifier().selectable(selected, [=] { … }, SemanticsRole::RadioButton)) { RadioButton(selected, nullptr); };, Modifier().matchParentSize() |
scope.launch { if (snackbarHostState.showSnackbar("Deleted", actionLabel = "Undo") == SnackbarResult.ActionPerformed) undo() } |
scope.launch([s]() -> Task { if (co_await s.showSnackbarAsync("Deleted", "Undo") == SnackbarResult::ActionPerformed) undo(); }); |
val sheetState = rememberModalBottomSheetState(), ModalBottomSheet(onDismissRequest = { show = false }, sheetState = sheetState) { … }, scope.launch { sheetState.hide() }.invokeOnCompletion { show = false } |
auto sheetState = rememberModalBottomSheetState();, ModalBottomSheet([show] { show.set(false); }, sheetState) { … };, scope.launch([=]() -> Task { co_await sheetState.hide(); show.set(false); }); |
Modifier.pointerHoverIcon(PointerIcon.Hand) |
Modifier().pointerHoverIcon(PointerIcon::Hand) (Cursor::Move, ResizeHorizontal…) |
CenterAlignedTopAppBar(title = { … }), Card(onClick = { … }) { … }, Scaffold(floatingActionButtonPosition = FabPosition.Center) |
CenterAlignedTopAppBar(title = [] { … }), Card([] { … }) { … };, Scaffold(floatingActionButton = …, floatingActionButtonPosition = FabPosition::Center) [=](PaddingValues padding) { … }; |
LazyRow(state = s, flingBehavior = rememberSnapFlingBehavior(s)) { … } |
LazyRow(Modifier(), state = s, flingBehavior = rememberSnapFlingBehavior(s)) { … }; |
KompozitionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { … }, Modifier.absoluteOffset(x) |
KompozitionLocalProvider({LocalLayoutDirection.provides(LayoutDirection::Rtl)}, [&] { … }), Modifier().absoluteOffset(x, 0) |
IconToggleButton(checked, onCheckedChange = { checked = it }) { Icon(…) }, OutlinedIconButton(onClick) { … } |
IconToggleButton(checked.get(), [checked](bool on) { checked.set(on); }) { Icon(…); }; (FilledIconToggleButton, FilledTonalIconToggleButton, OutlinedIconToggleButton), OutlinedIconButton([] { … }) { … }; |
OutlinedTextField(value, onValueChange, label = { Text("Amount") }, prefix = { Text("$") }, suffix = { Text("kg") }) |
OutlinedTextField(value, set, label = "Amount", prefix = "$", suffix = "kg") |
Modifier.zIndex(1f), placeable.place(x, y, zIndex) |
Modifier().zIndex(1), p.place(x, y, 1) |
Modifier.dragAndDropSource { DragAndDropTransferData(...) }, Modifier.dragAndDropTarget(shouldStartDragAndDrop, target) |
Modifier().dragAndDropSource([](Offset) { return DragAndDropTransferData{.text = "…", .localState = item}; }), dragAndDropTarget([](const DragAndDropEvent& e) { return e.hasMimeType("text/plain"); }, {.onDrop = [](const DragAndDropEvent& e) { …; return true; }}) |
| Material 3 components | ListItem({.headline = ..., .supporting = ...}), FilterChip(selected, onClick, label), Slider(value, onChange), PrimaryTabRow/Tab, BadgedBox, SegmentedButton, BottomAppBar (material.hpp) |
TextField/OutlinedTextField, KeyboardOptions, FocusRequester |
OutlinedTextField(value, onChange, modifier, singleLine = true, label = "Email", keyboardOptions = KeyboardOptions{.keyboardType = KeyboardType::Email, .imeAction = ImeAction::Next}); as in Compose, a field without singleLine has several lines (minLines/maxLines); OutlinedTextField(state) with rememberTextFieldState() (state.edit([](TextFieldBuffer& b) { b.selectAll(); }), inputTransformation = InputTransformation::maxLength(10), outputTransformation = OutputTransformation{[](TextFieldBuffer& b) { b.insert(3, " "); }}, state.undoState().undo()); Modifier().focusRequester(r), r.requestFocus(), focusManager().clearFocus(), Modifier().focusProperties({.canFocus = false, .next = r}) |
LocalUriHandler.current.openUri(url), LocalSoftwareKeyboardController.current?.hide(), LocalHapticFeedback.current.performHapticFeedback(...), LocalClipboardManager |
uriHandler().openUri(url), softwareKeyboardController().hide(), hapticFeedback().performHapticFeedback(HapticFeedbackType::LongPress), clipboardManager().setText(...) (taken in composition, used in callbacks) |
ModalNavigationDrawer, NavigationRail, the window's size |
ModalNavigationDrawer(drawerContent, rememberDrawerState()) { … };; a rail in wide windows from LocalWindowSize.current() |
Dialog, AlertDialog, DropdownMenu, ModalBottomSheet, BackHandler |
The same names; if (open) AlertDialog(onDismiss, title = ..., confirmButton = ...). Popups are composed where they are called in the tree (their state and composition locals are kept); the host places them on top |
BottomSheetScaffold(sheetContent = { ... }, scaffoldState = state, sheetPeekHeight = 128.dp) { padding -> ... } |
BottomSheetScaffold([&] { ... }, Modifier(), scaffoldState = state, sheetPeekHeight = 128) [=](PaddingValues padding) { ... };, co_await state.bottomSheetState.expand() |
NavigationSuiteScaffold(navigationSuiteItems = { item(selected, onClick, icon, label) }) { content }, currentWindowAdaptiveInfo().windowSizeClass |
NavigationSuiteScaffold([&](NavigationSuiteScope& nav) { nav.item(selected, onClick, Icons::Filled::Home, "Home"); }, Modifier(), {}, content), currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass |
Modifier.imePadding(), WindowInsets.statusBars, consumeWindowInsets |
Modifier().padding(padding).consumeWindowInsets(padding).imePadding(), WindowInsets::statusBars(), windowInsetsPadding(WindowInsets::systemBars()); SystemBarsAppearance(!dark) for the system bars' icons |
dynamicLightColorScheme(context) (Material You) |
MaterialTheme(isDynamicColorAvailable() ? dynamicLightColorScheme() : lightColorScheme(), ...): the wallpaper's colors on Android 12+, the desktop's accent color on Linux (XDG portal) and Windows; TonalPalette::fromSeed(color) anywhere; wallpaperLightColorScheme() from the wallpaper itself (Linux desktops, Windows, Android 8.1+), TonalPalette::fromImage(bitmap) from any picture |
Modifier.semantics { contentDescription = ...; role = Role.Button } |
Modifier().semantics({.contentDescription = "...", .role = SemanticsRole::Button}); TalkBack reads this tree on Android |
Modifier.semantics(mergeDescendants = true) {}, Modifier.clearAndSetSemantics { contentDescription = "Rating: 4 of 5" }, Modifier.semantics { hideFromAccessibility() } |
Modifier().semantics({.mergeDescendants = true}), Modifier().clearAndSetSemantics({.contentDescription = "Rating: 4 of 5"}), Modifier().hideFromAccessibility() |
Modifier.animateItem(), SwipeToDismissBox, Modifier.draggable |
The same names; SwipeToDismissBox(rememberSwipeToDismissBoxState(confirm), background, content) |
Modifier.focusable(), onKeyEvent { } |
Modifier().onKeyEvent([](const KeyEvent& e) { return e.key == Key::Enter && e.type == KeyEventType::KeyDown; }).focusable(); focus moves with Tab and the arrow keys, Enter/Space click |
SearchBar(inputField = { SearchBarDefaults.InputField(...) }, expanded, onExpandedChange) { } |
SearchBar([&] { SearchBarDefaults::InputField({.query = q, .onQueryChange = ..., .expanded = open, ...}); }, open, setOpen, modifier, [&] { ...suggestions... }); DockedSearchBar, which opens in place, takes the same arguments; BasicTextField |
DatePickerDialog(onDismiss, confirmButton = { }) { DatePicker(state) } |
auto state = rememberDatePickerState(); DatePickerDialog(close, [=] { TextButton([=] { pick(state.selectedDate()); }) { ... }; }, cancel, [&] { DatePicker(state, Modifier(), {.strings = DatePickerStrings::turkish()}); }); dates are std::chrono::year_month_day |
DateRangePicker(state) |
auto state = rememberDateRangePickerState(); DateRangePicker(state, Modifier(), {.strings = DatePickerStrings::turkish()}); state.selectedStartDate() / selectedEndDate(); start and end tabs, each like DatePicker |
TimePickerDialog(onDismiss, confirmButton = { }) { TimePicker(state) } |
auto state = rememberTimePickerState(9, 30); TimePickerDialog(close, {.title = ..., .confirmButton = ..., .modeToggleButton = ...}, [&] { TimePicker(state); }); TimeInput(state) to type it |
TooltipBox(positionProvider, tooltip = { PlainTooltip { Text(..) } }, state) { IconButton(..) } |
TooltipBox([] { PlainTooltip([] { Text("Add to favorites"); }); }, rememberTooltipState(), Modifier(), [&] { IconButton(...) { ... }; }); RichTooltip({.title, .text, .action}) |
ExposedDropdownMenuBox(expanded, onExpandedChange) { TextField(readOnly = true, modifier = Modifier.menuAnchor()); ExposedDropdownMenu(..) } |
ExposedDropdownMenuBox(open, setOpen, Modifier()) { TextField(choice, nullptr, Modifier().fillMaxWidth(), readOnly = true); ExposedDropdownMenu(open, close) { DropdownMenuItem(...); }; };; the box itself is the anchor |
RangeSlider(value = 20f..80f, onValueChange, valueRange = 0f..100f) |
RangeSlider(std::pair(20.f, 80.f), [=](std::pair<float, float> v) { ... }, valueFrom = 0, valueTo = 100, steps = 9) |
PrimaryScrollableTabRow(selectedTabIndex) { Tab(...) }, SecondaryTabRow |
PrimaryScrollableTabRow(i, Modifier(), [&] { Tab(...); }, {.edgePadding = 16}); SecondaryTabRow(i, Modifier(), [&] { Tab(...); }) |
LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2)) { items(..) }, LazyHorizontalGrid(rows = GridCells.Fixed(3)) |
LazyVerticalStaggeredGrid(StaggeredGridCells::Fixed(2), Modifier().fillMaxSize(), verticalItemSpacing = 8) { items(...); };; LazyHorizontalGrid(GridCells::Fixed(3), Modifier().height(180)) { ... }; |
Modifier.scrollable(rememberScrollableState { delta -> ... }, Orientation.Horizontal) |
auto s = rememberScrollableState([=](float delta) { ...; return delta; }); Modifier().scrollable(s, Orientation::Horizontal); Modifier().scale(2).rotate(45) |
PermanentNavigationDrawer(drawerContent = { PermanentDrawerSheet { } }) { }, DismissibleNavigationDrawer |
PermanentNavigationDrawer([&] { PermanentDrawerSheet(Modifier()) { items(); }; }, Modifier()) { page(); };; DismissibleNavigationDrawer([&] { DismissibleDrawerSheet(Modifier()) { items(); }; }, drawerState) { page(); }; |
LaunchedEffect(Unit) { snapshotFlow { listState.firstVisibleItemIndex }.collect { } } |
LaunchedEffect(0, [=]() -> Task { co_await snapshotFlow([=] { return list.firstVisibleItemIndex(); }).collect([](int i) { ... }); }) |
val results = query.debounce(300).flatMapLatest { repo.search(it) }.stateIn(viewModelScope, Eagerly, emptyList()) |
StateFlow<Results> results = query.debounce(300ms).flatMapLatest([](const std::string& q) { return repo.search(q); }).stateIn(viewModelScope(), {}); (flow<T>([](FlowCollector<T> emit) -> Task { co_await emit(v); }), map, filter, take, combine…) |
MutableSharedFlow<Event>(), stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initial) |
MutableSharedFlow<Event> events; (events.tryEmit(e) from any thread), flow.stateIn(viewModelScope(), SharingStarted::WhileSubscribed(5000ms), initial), shareIn(scope, SharingStarted::Lazily, 1) |
val nearEnd by remember { derivedStateOf { state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == total - 1 } }, items(n, key = …, contentType = …) |
auto nearEnd = remember([state] { return derivedStateOf([state] { auto v = state.layoutInfo().visibleItemsInfo; return !v.empty() && v.back().index == total - 1; }); });, l.items(n, key, [](int) { return size_t(1); }, …) |
val uiState by viewModel.uiState.collectAsState() |
UiState uiState = viewModel->uiState.collectAsState(); (MutableStateFlow<UiState>, setValue/update from any thread) |
val items by vm.items.collectAsStateWithLifecycle(), LifecycleResumeEffect(key) { player.play(); onPauseOrDispose { player.pause() } }, LifecycleEventEffect(Lifecycle.Event.ON_START) { … } |
auto items = collectAsStateWithLifecycle(vm->items);, LifecycleResumeEffect(key, [=] { player.play(); return [=] { player.pause(); }; });, LifecycleEventEffect(LifecycleEvent::OnStart, [&] { … }); (currentLifecycleOwner()->lifecycle(): the app's or the navigation entry's) |
SelectionContainer { Text(..) } |
SelectionContainer(Modifier(), [] { Text("..."); }): one selection across texts, handles, Copy, dragging with the mouse, Ctrl+C |
DisableSelection { Text(..) } |
DisableSelection([] { Text("..."); }) |
PullToRefreshBox(isRefreshing, onRefresh) { LazyColumn { } } |
PullToRefreshBox(refreshing, [=] { reload(); }, Modifier().fillMaxSize(), [&] { LazyColumn(...); }) |
Modifier.transformable(state), detectTransformGestures |
auto t = rememberTransformableState([=](float zoom, Offset pan, float rotation) { ... }); Modifier().transformable(t); PointerEvent::changes carries every finger |
pointerInput(key) { detectTapGestures(...) } |
pointerInput(key, [=](PointerInputScope& s) -> Task { co_await detectTapGestures(s, {.onTap = ...}); }) |
Modifier.padding(..).background(..) |
Modifier().padding(..).background(..) (the order matters, as in Compose) |
| Named arguments | Named arguments: Text("Title", fontSize = 24, color = ...) (options structs for the components not yet converted) |
@Preview |
KOMPOZ_PREVIEW(Name, .width = 360, .height = 200) { ... }, shown with KOMPOZ_PREVIEW=Name (see Development) |
createComposeRule(), onNodeWithText(..).assertIsDisplayed() |
kompoz::testing::KompozTestRule (UI tests without a window): rule.onNodeWithText("OK").assertIsDisplayed().performClick(); a failure throws testing::AssertionError |
Modifier.rekompozHighlighter() (snippets) |
Modifier().rekompozHighlighter(): a border showing how many times the composable that made the node ran |
Which Compose APIs came first was decided by how often the android/snippets repository uses them; see docs/components.md.
It works as in Compose:
- Every function written with
kompozable(...)is a scope. EachMutableStateandKompozitionLocalits body reads while it runs is recorded. - When a state changes, only the scopes that read it run again; their parents don't.
- When a parent runs, children whose arguments are equal to last time and that read nothing that changed are skipped: their slots and nodes stay as they are.
- When a
KompozitionLocalProviderstarts giving a different value, the scopes below it that read it run again (whatever was skipped in between). - Animations run only their own scope each frame; values read while drawing or laying out (a
graphicsLayerlambda,Modifier.offset { },InfiniteTransition::animateFloat) don't recompose at all.
void TodoRow(const Todo& todo, MutableState<std::vector<Todo>> todos, CallSite site = CallSite::current()) {
kompozable(site, todo, todos, [=] { // skipped while the list is redrawn, if todo and todos are the same
...
});
}Rules:
- The body lambda captures with
[=]: the scope may later run on its own, without its parent. - To be skipped, the arguments must compare with
==. AMutableStatecompares by identity, aModifierby its elements' values. An argument that can't be compared (std::function, ...) makes the composable restartable but never skipped. Textis skippable. A block (Column(…) { … };,Box,Row,Button,Card,Surface,Dialog,DropdownMenu...) runs in a scope of its own: when only what it read changes, it alone runs again. Components still taking a content lambda (ListItem,LazyColumn...) are like Compose's inline functions: state read inside them belongs to the enclosing scope.- Blocks and event callbacks (
onClick, ...) can run after the function that made them returned, so they capture local variables by value: a block writtenColumn(…) { … };does ([=]).
Layout is incremental as well: a node keeps its size and its children's places when it is measured with the same constraints and nothing in or below it changed; only what was composed again, or read a state that changed while it was measured, is measured again.
KOMPOZ_TRACE=1 logs how many scopes ran and how many were skipped in each recomposition. The showcase's
"Recomposition'ları göster" switch turns on the highlighter from the snippets.
app code (composable functions)
│
┌──────▼─────────────────────────────── kompoz_core (platform independent, no dependencies)
│ runtime slot table (a tree of groups), scopes and read tracking, remember, state, effects, KompozitionLocal
│ state derivedStateOf, mutableStateListOf, rememberUpdatedState
│ coroutines Task, Dispatcher (delay/awaitFrame on the frame clock), LaunchedEffect, CoroutineScope
│ flow Flow, StateFlow/SharedFlow (written from any thread), collect, collectAsState
│ lifecycle Lifecycle/LifecycleOwner (the app's, navigation entries'), lifecycle effects
│ layout LayoutNode tree, Constraints, the Modifier chain, incremental measure → place → draw
│ graphics Color, HCT and tonal palettes, Rect, ImageVector, ImageBitmap, Brush, Path, Shape, Font/FontFamily
│ draw DrawScope, Canvas, drawBehind/drawWithContent/drawWithCache, graphicsLayer
│ text AnnotatedString, multi-style line breaking, bidi (right-to-left text)
│ gestures pointerInput (a coroutine per node), detectTap/Drag/TransformGestures
│ host KompozHost: composition + layout + popup layer + hit testing, focus, scrolling, flings (dp)
│ nested_scroll NestedScrollConnection/Dispatcher: scrolls and flings chained to the parents
│ lazy, pager LazyColumn/LazyRow/grids (only what is in view, plus a margin), pagers, carousels
│ animation specs, Animatable, animate*AsState, transitions, animateContentSize;
│ animated: AnimatedVisibility/AnimatedContent/Crossfade, SharedTransitionLayout
│ widgets foundation and basic components, TextField, Popup, BackHandler, previews
│ material Scaffold and bars, components (chips, sliders, tabs...), overlays (dialogs, menus,
│ sheets), navigation (drawers, rails, suites), adaptive layouts
└──────┬─────────────────────────────── GraphicsContext / TextMeasurer interfaces
┌──────▼─────────────────────────────── platform backend
│ sdl3 window, input, IME/soft keyboard, HarfBuzz + FreeType glyph atlas, images, the desktop's accent
│ color (Linux)
│ android the Java side: keyboard (InputConnection), accessibility, insets, dynamic color, back gesture
│ gpu Kompoz's own GPU renderer, what everything is drawn with: one core over thin layers for Vulkan,
│ Metal, WebGPU and OpenGL ES 3 (WebGL 2 on the web); KOMPOZ_GPU picks one, the platform's best by default
└──────────────────────────────────────
A new platform backend implements GraphicsContext and TextMeasurer (two functions, plus two optional calls for
gradients and paths), gives input events to KompozHost, and calls frame() and draw(); redrawing only what
changed through a DisplayList is optional. Kompoz's own platforms (platform/) are only the window system's side:
the app core and the GPU renderer are the same on all of them.
Requirements: a C++20 compiler (GCC 13+, Clang 17+; tested with GCC 16 and Clang 22; GCC 13 crashes on some coroutine
lambdas, an internal compiler error that GCC 14 doesn't have), CMake 3.24+,
FreeType and libpng, and on Linux the headers of Wayland and xkbcommon or of X11 (see the setup guide). Optional: HarfBuzz (text shaping: Arabic joining, Indic scripts, ligatures, emoji sequences;
without it text is drawn character by character), and on Linux libcurl, libwebp and libturbojpeg 3.0+ for
AsyncImage (HTTP, WebP, JPEG). On Linux, libdbus is loaded at run time, when present, for the desktop's accent color.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/showcase
./build/kompoz_testsThe core and the tests alone (no platform): -DKOMPOZ_BUILD_APP=OFF; without the examples: -DKOMPOZ_BUILD_EXAMPLES=OFF.
On desktops that have lld (and when no other linker was chosen) the build links with it: about 1 second instead of
GNU ld's 25 for the showcase.
Add it as a subdirectory (a clone or a git submodule) and link its platform layer, kompoz::app, which brings the entry point:
cmake_minimum_required(VERSION 3.24)
project(myapp LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(KOMPOZ_BUILD_EXAMPLES OFF)
set(KOMPOZ_BUILD_TESTS OFF)
add_subdirectory(kompoz) # a clone or a git submodule
add_executable(myapp main.cpp) # the example at the top
target_link_libraries(myapp PRIVATE kompoz::app)
kompoz_copy_assets(myapp) # the fonts and the licenses next to the binaryOr install Kompoz once and find it as a package, on Linux, macOS or Windows. Built with the system's FreeType,
HarfBuzz and libpng, it finds them there again; built with its own (scripts/fetch-deps.sh: Windows, or
-DKOMPOZ_VENDORED_DEPS=ON), it installs them beside it:
cmake -S kompoz -B kompoz/build -DCMAKE_BUILD_TYPE=Release -DKOMPOZ_BUILD_EXAMPLES=OFF -DKOMPOZ_BUILD_TESTS=OFF
cmake --build kompoz/build -j
cmake --install kompoz/build --prefix ~/.localfind_package(kompoz 0.1 REQUIRED) # -DCMAKE_PREFIX_PATH=~/.local, if that isn't searched already
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE kompoz::app)
kompoz_copy_assets(myapp)With vcpkg or Conan, from a clone of the repository (its overlay port and its recipe build that clone, with the
package manager's FreeType, HarfBuzz and libpng), then find_package(kompoz) as above:
vcpkg install kompoz --overlay-ports=kompoz/packaging/vcpkg/ports
conan create kompoz/packaging/conan --build=missing -s compiler.cppstd=20On Linux and macOS AsyncImage decodes JPEG with libjpeg-turbo (2.x or 3.x), WebP with libwebp and downloads with
libcurl (Windows, Android and the web have their own). Each is used when found; -DKOMPOZ_WITH_JPEG=ON (or WEBP,
CURL) requires it, OFF leaves it out. vcpkg's features jpeg, webp, curl and Conan's options with_jpeg,
with_webp, with_curl (all on by default) are the same.
main.cpp defines kompoz::createApplication (see the example at the top); the backend owns main. For Android and
the web, start from the showcase's setup: its android/ Gradle project and the Emscripten options in
examples/showcase/CMakeLists.txt.
Layout of the repository:
| Path | What |
|---|---|
include/kompoz/, src/ |
the platform-independent core (#include <kompoz/kompoz.hpp>) |
platform/app/ |
the app core (frames, drawing, scripts, screenshots) and the platform interface it runs on |
platform/gpu/ |
the GPU renderer and its Vulkan, OpenGL ES, Metal and WebGPU backends |
platform/common/ |
what every platform shares: fonts, images, files and logging, the wallpaper's colors |
platform/linux/, platform/windows/ |
the desktops' own: Linux's Wayland and X11 platforms (wayland/, x11/), Windows' Win32 platform, screen readers (AT-SPI, UI Automation), the accent color |
platform/android/ |
Android's platform: its native side (the frames, input and surfaces, the keyboard, performance hints) and its Java side in java/ (the activity and its view, keyboard, accessibility, insets, intents) |
platform/headless/ |
the platform without a screen (KOMPOZ_HEADLESS=1): tests, scripted runs, CI |
platform/macos/ |
macOS's platform (AppKit, Metal) |
platform/web/ |
the web's platform and the page the app is drawn in |
assets/ |
what every app ships: the bundled fonts (fonts/) and the licenses (licenses/) |
tests/ |
the headless tests |
examples/showcase/ |
the showcase app: a folder per screen (home/, list/, components/, material/, places/), each screen's sections in files of their own, showcase.hpp for what they share; android/ is the same app for Android, assets/ its images |
scripts/ |
fetch-deps.sh (the vendored dependencies), dev.sh, package.sh |
docs/ |
components.md (every API and its status, the build log), screenshots |
The showcase's texts are in Turkish, and it gives the library's own texts in Turkish too (LocalStrings).
scripts/dev.shWhen a source file is saved, the showcase is rebuilt and started again; the screen it was on, scroll positions and
rememberSaveable values come back (through KOMPOZ_STATE_FILE, as Android brings an app back after ending its
process). A build that fails leaves the running app alone. An edit to a showcase file is on the screen in about 7
seconds (Debug with sanitizers, lld). Arguments: the target, the build directory and what the app is started with
(scripts/dev.sh showcase build kompoz://components). Closing the window or Ctrl+C ends it.
As Compose's @Preview: a composable on its own, without the rest of the app, at its size.
KOMPOZ_PREVIEW(CounterCardPreview, .width = 400, .height = 220) {
MaterialTheme(lightColorScheme(), [] { CounterCard(); });
}KOMPOZ_PREVIEW=CounterCardPreview scripts/dev.shThe window shows only that preview and comes back on every save; KOMPOZ_PREVIEW='*' (or a name it doesn't know)
shows them all, one under another, with their names and sizes. .darkTheme = true sets isSystemInDarkTheme()
inside it, .background a background; the text scale is the window's (KOMPOZ_FONT_SCALE). As a picture:
KOMPOZ_HEADLESS=1 KOMPOZ_PREVIEW=CounterCardPreview KOMPOZ_SCRIPT="wait 10; shot preview.png; quit" ./build/showcase.
kompoz::testing::KompozTestRule composes, lays out and draws without a window, with a virtual clock and a text
measurer of fixed sizes; find nodes by text, tag or semantics, click, type, swipe, and check bounds, semantics and
what was drawn. ./build/kompoz_tests runs them all (./build/kompoz_tests lazy only those whose name contains
"lazy"). Every incremental layout is checked against a full one while the tests run.
scripts/package.shPackages the showcase for every platform it builds on, into dist/: Linux (.tar.gz, Release; it uses the
system's FreeType, HarfBuzz...), Windows (.zip, built statically with MinGW; Windows 10+), Android (.apk,
signed with the debug key, installable as it is) and the web (.zip; serve the folder over HTTP:
python3 -m http.server 8765). Only some of them: scripts/package.sh linux web. A platform whose toolchain is
missing is skipped, with a note.
On a version's tagged commit, after scripts/package.sh, scripts/release.sh publishes the four packages on GitHub
Releases under the v<version> tag, with the version's changelog entry as the notes (--dry-run shows what it would
do). It needs the GitHub CLI (gh), signed in.
Requirements: the Android SDK, NDK 29.0.14206865, SDK CMake 4.1.2 (from Android Studio's SDK Manager or
sdkmanager "ndk;29.0.14206865" "cmake;4.1.2"), JDK 17+ (Android Studio's jbr will do).
scripts/fetch-deps.shcd examples/showcase/android && ./gradlew installDebugscripts/fetch-deps.sh downloads FreeType, HarfBuzz, zlib and libpng into third_party/ at pinned versions,
checking their sha256. Gradle builds the root CMakeLists.txt with the NDK into the
app's libmain.so (Kompoz and the showcase's sources). The Java side is Kompoz's own
(platform/android/java): KompozActivity, which the example's MainActivity
extends, and its KompozView, whose frames come from the display's Choreographer (the vsync's time as the frame's, as
Compose takes it) and whose touches, keys and lifecycle go to the app on the UI thread, where it runs; the example's
android/ folder only has the manifest, a theme and that MainActivity. The fonts come into the APK from assets/,
the example's images from examples/showcase/assets/.
Deep links (navDeepLink): when the app is opened with a link, the NavHost goes to that screen, with the start
screen and the screens of the graphs in between under it, so back goes up. On the desktop it is the command line
(./build/showcase kompoz://item/42), on the web the address's # part (showcase.html#item/42; the screen shown
is written there too, so reloading opens the same one), on Android a VIEW intent: an intent-filter for its
scheme in the manifest (KompozActivity passes on the links that come while the app is open too). rememberSaveable
values come back after Android ended the process (KompozActivity keeps them in its saved instance state); on the
desktop KOMPOZ_STATE_FILE=state.bin writes the state at exit and reads it at start (to try the process being
ended). To try a deep link:
adb shell am start -a android.intent.action.VIEW -d "kompoz://item/42"The keyboard goes through an InputConnection of Kompoz's own
(KompozInput.java). One that keeps a separate copy of the text and
sends only differences (as SDL's did) lets suggestions and autocorrect garble the text. Ours has a single Editable;
every change sends the whole text, the selection and the word being composed to the native side. When the app changes
the text itself (clearing it after "Ekle", say), restartInput brings the keyboard back in step. Compose's
TextInputServiceAndroid works the same way.
What works on Android: touch, scrolling and flings, the keyboard with suggestions and autocorrect, the keyboard's
type and action key from KeyboardOptions (email, number, password; Next/Done/Search...), back (popup → focus →
BackHandler → leaving the app) and the predictive back gesture, density (dp) and the system's font size (sp, also
when it changes while running), edge to edge (status bar, navigation bar, cutout, keyboard and gesture edges as
typed WindowInsets; the system bars' icons for the light or dark theme), the system's dark mode
(isSystemInDarkTheme()), dynamic color on Android 12+ (the wallpaper's palette, refreshed when the app comes to the
front), TalkBack, Serif/Monospace, symbols, CJK and color emoji from the system's fonts (Android's COLRv1 emoji font
is drawn with the platform's text stack), the glyph atlas rebuilt when the GPU context is lost.
Requirements: Emscripten (Arch: pacman -S emscripten; its commands are under /usr/lib/emscripten) and
scripts/fetch-deps.sh. FreeType, HarfBuzz, zlib and libpng are built from source; the fonts and images are
packed into showcase.data; the page (platform/web/shell.html) is just a canvas and the
app fills it.
emcmake cmake -S . -B build-web -DCMAKE_BUILD_TYPE=Release -DKOMPOZ_BUILD_TESTS=OFFcmake --build build-web -jpython3 -m http.server 8765 --directory build-webThen open http://localhost:8765/showcase.html. There are no threads on the web (std::mutex does nothing; the
example's "download" runs in a coroutine). No font files either: characters the bundled font lacks (emoji, CJK,
Arabic, Indic scripts, symbols) are drawn by the browser with its own fonts on a 2D canvas, runs of them together
(Arabic words come out joined); FontFamily::Serif/Monospace fall back to the bundled sans-serif on the web.
Requirements: MinGW-w64 (Arch: pacman -S mingw-w64-gcc) and scripts/fetch-deps.sh. The dependencies are built
from source, statically; showcase.exe depends only on Windows' own DLLs and goes along with the fonts/,
licenses/ and images/ folders next to it.
cmake -S . -B build-windows -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake -DCMAKE_BUILD_TYPE=Releasecmake --build build-windows -jThe tests also run under Wine: WINEPATH=/usr/x86_64-w64-mingw32/bin wine build-windows/kompoz_tests.exe.
KOMPOZ_SCRIPT drives the app without a user (coordinates in dp; while the script runs, time is virtual and
animations are deterministic; when it ends, real time takes over and you can go on using the app from where the
script left it):
KOMPOZ_HEADLESS=1 KOMPOZ_SCRIPT="click 160 263; wait 30; shot out.png; quit" ./build/showcaseWith KOMPOZ_HEADLESS=1 it runs on Kompoz's headless platform, without a window: the GPU renderer draws only into its
kept frame, from which the screenshot is taken.
Commands: click x y, rclick x y, move x y, down x y / up x y (holding down), drag x1 y1 x2 y2,
mdrag x1 y1 x2 y2 (with the mouse), resize w h, swipe x1 y1 x2 y2 frames (a move per frame, so it flings),
fdown|fmove|fup id x y (multi-touch), wheel x y dy, zoom x y factor (Ctrl+wheel / touchpad pinch),
type text, key enter|backspace|escape|tab|shift+tab|space|up|down|left|right, wait frames, sleep ms (real
time, for work on other threads), shot file.png, quit. Environment variables: KOMPOZ_TRACE=1 logs how many scopes ran or were skipped in each recomposition;
KOMPOZ_FONT_SCALE=1.3 stands in for the system's font scale on the desktop, KOMPOZ_SCALE=2.625 for a phone's
density; KOMPOZ_NO_DESKTOP_ACCENT=1 and KOMPOZ_NO_WALLPAPER_COLORS=1 leave out the desktop's accent color and the
wallpaper's colors (screenshots the same on every machine); KOMPOZ_LANGUAGE=tr|en stands in for the system language of the library's own texts;
KOMPOZ_STATE_FILE=file keeps the saved state across runs; KOMPOZ_PREVIEW=name shows a preview.
KOMPOZ_PROFILE=1 logs frame times: for each slow frame the input, compose + layout, drawing and present times, the
offscreen layers and the glyphs rasterized, and a summary every second. Android has no environment variables, so it
looks for a file in the app's own storage (the output is in adb logcat -s kompoz):
adb shell run-as dev.kompoz.showcase touch files/kompoz-profileFrames are recorded first (DisplayList); compared with the frame before, only what changed is drawn again over
the kept pixels. A frame in which nothing on the screen changes (an indicator spinning out of view, a recomposition
that looks the same) is neither drawn nor shown. KOMPOZ_DAMAGE_SHOW=1 outlines what was drawn again,
KOMPOZ_DAMAGE_CHECK=1 also draws each frame from scratch and compares the pixels (logging any difference), and
KOMPOZ_FULL_REDRAW=1 draws every frame from scratch.
| Platform | Status |
|---|---|
| Linux | Built and tested (GCC 16 with ASan/UBSan, Clang 22). Dark mode and the accent color (Material You) come from the XDG desktop portal as they change (libdbus, loaded at run time); Kompoz's own Wayland and X11 platforms |
| Android | Built; run on a phone (Xiaomi 24030PN60G, Android 16, arm64) and in the emulator (API 36, x86_64, GLES). The APK has arm64-v8a and x86_64 |
| Windows | Cross-built from Linux with MinGW-w64 (GCC 16); the tests pass under Wine and the showcase draws there |
| Web | Built with Emscripten (WebGPU, WebGL 2; Kompoz's own web platform) and run in browsers (clicks, typing, the clipboard, scrolling) |
| macOS | Built and tested by CI with Homebrew's FreeType, HarfBuzz and libpng; an app runs in a real window (Kompoz's own AppKit platform, Metal); not yet used by hand |
| iOS | The vendored dependencies are ready (KOMPOZ_VENDORED_DEPS); it needs an Xcode project (on macOS) |
- macOS is built, tested and run by CI but hasn't been used by hand on a Mac, and isn't connected to NSAccessibility (TalkBack on Android, AT-SPI on Linux and UI Automation on Windows work); iOS hasn't been built.
- Overscroll is Android 12's stretch done as a scale, not a true warp. Content scaled up with
graphicsLayeris scaled from a 1× layer texture (Compose draws it again at the scale), so it softens when zoomed far in. In nested scrolling,onPreFling/onPostFlingdon't suspend as in Compose (a connection that animates starts its own coroutine). - Text: right-to-left text is ordered by a simplified Unicode bidi algorithm (isolates and bracket pairs, but no explicit embeddings or overrides). Missing glyphs come from the system's fallback fonts.
- The recompose highlighter's counts don't reset after 3 seconds as the snippets' do; they add up.
- Lazy lists don't reuse items' compositions by
contentType(Compose's SubcomposeLayout pool); measured, it wasn't needed yet. - Kotlin's
bydelegation has no counterpart: state is read and written with.get()and.set(). - The library's own texts (
LocalStrings) come in English and Turkish only; an app in another language provides its ownStrings.
Bug reports, fixes and ports are welcome; see CONTRIBUTING.md. If you have a Mac, the macOS and iOS port is where help is needed most.
Kompoz is licensed under the Apache License, Version 2.0; see NOTICE. It follows the behavior and API of Jetpack Compose and Material Color Utilities (both Apache 2.0) without including their sources.
Apps built with Kompoz ship the bundled Noto Sans fonts (SIL Open Font License 1.1) and, in the vendored builds (Android, iOS, Windows, the web), FreeType (FreeType License), HarfBuzz (MIT), libpng (PNG Reference Library License version 2) and zlib (zlib License). Their license texts are in assets/licenses, which is copied next to every app with its other assets; keep it when you ship one.





