Search Docs
Loading...
Skip to content

Canvas Menu

The canvas menu provides immediate access to frequently-used actions when you select a design element, offering operations like duplicate, delete, layer reordering, and grouping controls directly on the canvas. This guide shows you how to customize these contextual actions to streamline your users’ editing workflow and match your app’s feature priorities.

Explore a complete code sample on GitHub.

Canvas Menu Architecture#

Canvas Menu

The canvas menu displays horizontally when a design element is selected, offering quick access to editing actions relevant to that element type.

Key Components:

  • CanvasMenu.Button - Pre-built button implementation with icon and text
  • CanvasMenu.Divider - Visual separator between menu groups
  • CanvasMenu.Scope - Provides access to the engine, event handler, and selected element

Configuration#

Canvas menu customization is part of the EditorConfiguration, therefore, in order to configure the canvas menu we need to configure the EditorConfiguration. Below you can find the list of available configurations of the canvas menu. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise.

Available configuration properties:

  • scope - Scope of this component. Every new value will trigger recomposition of all ScopedPropertys such as visible, enterTransition, exitTransition etc. Consider using Compose androidx.compose.runtime.State objects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the Engine. By default, the scope is updated when the parent scope is updated, when selection is changed to a different design block (or unselected), and when the parent of the currently selected design block is changed to a different design block.

  • visible - Control canvas menu visibility based on selection state and editor conditions. By default the value is true when a design block is selected, touch interaction is inactive, no sheet is displayed, the selected design block is neither DesignBlockType.Audio nor DesignBlockType.Page, the editor is not in text edit mode, the scene is paused, and the selected design block is visible at the current playback time.

  • modifier - Jetpack Compose modifier of this component. By default empty modifier is applied.

  • enterTransition - Animation for when the canvas menu appears. Default value is always no enter transition.

  • exitTransition - Animation for when the canvas menu disappears. Default value is always no exit transition.

  • decoration - Apply custom styling like background, shadows, and positioning relative to the selected element. The default decoration adds background color, applies rounded corners and positions the list of items next to the selected design block.

  • listBuilder - Define the complete list of canvas menu items and their order. Items are only displayed when visible returns true.

  • itemDecoration - decoration of the items in the canvas menu. Useful when you want to add custom background, foreground, shadow, paddings etc to the items. Prefer using this decoration when you want to apply the same decoration to all the items, otherwise set decoration to individual items. Default value is always no decoration.

The CanvasMenu.Scope (this instance in all lambdas) provides access to the engine, event handler, and editor context. Use this for advanced customization logic and to maintain consistency with the current editor state.

EditorConfiguration.remember {
canvasMenu = {
CanvasMenu.remember {
// Implementation is too large, check the implementation of CanvasMenu.rememberDefaultScope
scope = {
CanvasMenu.rememberDefaultScope(parentScope = this)
}
modifier = { Modifier }
visible = {
val editorState by editorContext.state.collectAsState()
remember(this, editorState) {
editorState.isTouchActive.not() &&
editorState.activeSheet == null &&
editorContext.safeSelection != null &&
editorContext.selection.type != DesignBlockType.Page &&
editorContext.selection.type != DesignBlockType.Audio &&
editorContext.engine.editor.getEditMode() != "Text" &&
editorContext.isScenePlaying.not() &&
editorContext.selection.isVisibleAtCurrentPlaybackTime
}
}
enterTransition = { EnterTransition.None }
exitTransition = { ExitTransition.None }
// Implementation is too large, check the implementation of CanvasMenu.DefaultDecoration
decoration = { CanvasMenu.DefaultDecoration(scope = this) { it() } }
listBuilder = { CanvasMenu.ListBuilder.remember { /* Add items */ } }
// Default value is { it() }
itemDecoration = {
Box(modifier = Modifier.padding(2.dp)) {
it()
}
}
}
}
}

ListBuilder Configuration#

You can configure the items of the canvas menu using two main approaches:

New List Builder#

The simplest approach is to create a new list builder from scratch. In this example, we add both already declared and custom buttons:

CanvasMenu.ListBuilder.remember {
add {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.custom") }
onClick = {}
vectorIcon = null
textString = { "Custom Button" }
}
}
add { CanvasMenu.Button.rememberSelectGroup() }
if (editorContext.isSelectionInGroup) {
add {
CanvasMenu.Divider.remember()
}
}
add { CanvasMenu.Button.rememberSendBackward() }
add { CanvasMenu.Button.rememberBringForward() }
if (editorContext.canSelectionMove) {
add {
CanvasMenu.Divider.remember()
}
}
add { CanvasMenu.Button.rememberDuplicate() }
add { CanvasMenu.Button.rememberDelete() }
}

All available predefined buttons are listed below.

Modify Existing List Builder#

In case you already have an existing list builder and want to make simple modifications on it (prepend, append, replace, insert or remove an item) without touching the order invoke modify on the ListBuilder.

// Makes sense to use only with builders that are already available and cannot be modified by you directly.
val existingListBuilder = CanvasMenu.ListBuilder.remember {
add { CanvasMenu.Button.rememberBringForward() }
add { CanvasMenu.Button.rememberSendBackward() }
add { CanvasMenu.Button.rememberDuplicate() }
add { CanvasMenu.Button.rememberDelete() }
}
existingListBuilder.modify {
addFirst {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.first") }
vectorIcon = null
textString = { "First Button" }
onClick = {}
}
}
addLast {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.last") }
vectorIcon = null
textString = { "Last Button" }
onClick = {}
}
}
addAfter(id = CanvasMenu.Button.Id.bringForward) {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.afterBringForward") }
vectorIcon = null
textString = { "After Bring Forward" }
onClick = {}
}
}
addBefore(id = CanvasMenu.Button.Id.sendBackward) {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.beforeSendBackward") }
vectorIcon = null
textString = { "Before Send Backward" }
onClick = {}
}
}
replace(id = CanvasMenu.Button.Id.duplicate) {
// Note that it can be replaced with a component that has a different id.
CanvasMenu.Button.rememberDuplicate {
vectorIcon = { IconPack.Music }
}
}
remove(id = CanvasMenu.Button.Id.delete)
}

Available modification operations:

  • addFirst - prepends a new item at the beginning:
addFirst {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.first") }
vectorIcon = null
textString = { "First Button" }
onClick = {}
}
}
  • addLast - appends a new item at the end:
addLast {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.last") }
vectorIcon = null
textString = { "Last Button" }
onClick = {}
}
}
  • addAfter - adds a new item right after a specific item:
addAfter(id = CanvasMenu.Button.Id.bringForward) {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.afterBringForward") }
vectorIcon = null
textString = { "After Bring Forward" }
onClick = {}
}
}
  • addBefore - adds a new item right before a specific item:
addBefore(id = CanvasMenu.Button.Id.sendBackward) {
CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.beforeSendBackward") }
vectorIcon = null
textString = { "Before Send Backward" }
onClick = {}
}
}
  • replace - replaces an existing item with a new item:
replace(id = CanvasMenu.Button.Id.duplicate) {
// Note that it can be replaced with a component that has a different id.
CanvasMenu.Button.rememberDuplicate {
vectorIcon = { IconPack.Music }
}
}
  • remove - removes an existing item:
remove(id = CanvasMenu.Button.Id.delete)

CanvasMenu.Item Configuration#

Each CanvasMenu.Item is an EditorComponent. Its id must be unique which is a requirement for proper component management. You have multiple options for creating canvas menu items, from simple predefined buttons to fully custom implementations.

Use Predefined Buttons#

Start with predefined buttons which are provided as composable functions. All available predefined buttons are listed below.

Create New Buttons#

If our predefined buttons don’t fit your needs you can create your own. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise:

@Composable
fun rememberCanvasMenuButton() = CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.newButton") }
scope = {
val parentScope = this as Scope
rememberLastValue(parentScope) {
if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope)
}
}
modifier = { Modifier }
visible = { true }
enterTransition = { EnterTransition.None }
exitTransition = { ExitTransition.None }
// Default value is { it() }
decoration = {
Surface(color = MaterialTheme.colorScheme.background) {
it()
}
}
onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.Volume())) }
// Default value is null
icon = {
Icon(
imageVector = IconPack.Music,
contentDescription = null,
)
}
// Default value is null
text = {
Text(
text = "Hello World",
)
}
enabled = { true }
}

Required and optional properties:

  • id - the id of the button. Note that it is highly recommended that every unique EditorComponent has a unique id. By default property contains a random value.

  • scope - scope of this component. Every new value will trigger recomposition of all ScopedPropertys such as visible, enterTransition, exitTransition etc. Consider using Compose androidx.compose.runtime.State objects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the Engine. By default the scope is updated only when the parent component scope is updated.

  • visible - whether the button should be visible. Default value is always true.

  • modifier - Jetpack Compose modifier of this component. By default empty modifier is applied.

  • enterTransition - transition of the button when it enters the parent composable. Default value is always no enter transition.

  • exitTransition - transition of the button when it exits the parent composable. Default value is always no exit transition.

  • decoration - decoration of the button. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration.

  • onClick - the callback that is invoked when the button is clicked. By default it is a no-op.

  • icon - the icon content of the button. If null, it will not be rendered. Default value is null.

  • text - the text content of the button. If null, it will not be rendered. Default value is null.

  • tint - the tint color of the content. By default it is MaterialTheme.colorScheme.onSurfaceVariant.

  • enabled - whether the button is enabled. Default value is always true.

This gives full control over the content of the button. However, there are simpler configuration options if you do not want to fully customize text and icon composables. Let’s have a look at this example:

@Composable
fun rememberCanvasMenuButtonSimple() = CanvasMenu.Button.remember {
id = { EditorComponentId("my.package.canvasMenu.button.newButton") }
scope = {
val parentScope = this as Scope
rememberLastValue(parentScope) {
if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope)
}
}
modifier = { Modifier }
visible = { true }
enterTransition = { EnterTransition.None }
exitTransition = { ExitTransition.None }
// Default value is it
decoration = {
Surface(color = MaterialTheme.colorScheme.background) {
it()
}
}
onClick = { editorContext.eventHandler.send(ShowLoading) }
// Default value is null
vectorIcon = { IconPack.Music }
// Default value is null
text = { "Hello World" }
tint = { MaterialTheme.colorScheme.onSurfaceVariant }
enabled = { true }
contentDescription = null
}

It has three differences:

  1. icon is replaced with vectorIcon lambda, that returns ImageVector instead of drawing the icon content.
  2. text is replaced with textString lambda, that returns String instead of drawing the text content.
  3. contentDescription property is added that is used by accessibility services to describe what the button does. Provide it whenever the button does not contain visible text explaining its action.

Create Dividers#

Use dividers to visually separate groups of related actions:

@Composable
fun rememberCanvasMenuDivider() = CanvasMenu.Divider.remember {
scope = {
val parentScope = this as Scope
rememberLastValue(parentScope) {
if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope)
}
}
modifier = { Modifier }
visible = { true }
enterTransition = { EnterTransition.None }
exitTransition = { ExitTransition.None }
decoration = { it() }
modifier = {
remember(this) {
Modifier
.padding(horizontal = 8.dp)
.size(width = 1.dp, height = 24.dp)
}
}
}

Required and optional properties:

  • scope - scope of this component. Every new value will trigger recomposition of all ScopedPropertys such as visible, enterTransition, exitTransition etc. Consider using Compose androidx.compose.runtime.State objects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from the Engine. By default the scope is updated only when the parent component scope is updated.

  • visible - whether the divider should be visible. Default value is always true.

  • modifier - Jetpack Compose modifier of this component. By default padding and size modifiers are applied to the divider.

  • enterTransition - transition of the divider when it enters the parent composable. Default value is always no enter transition.

  • exitTransition - transition of the divider when it exits the parent composable. Default value is always no exit transition.

  • decoration - decoration of the divider. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration.

Create Custom Items#

For completely custom implementations, use EditorComponent.remember and render your custom UI inside decoration. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise:

@Composable
fun rememberCanvasMenuCustomItem() = EditorComponent.remember {
id = { EditorComponentId("my.package.canvasMenu.newCustomItem") }
scope = {
val parentScope = this as Scope
rememberLastValue(parentScope) {
if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope)
}
}
modifier = { Modifier }
visible = { true }
enterTransition = { EnterTransition.None }
exitTransition = { ExitTransition.None }
decoration = {
Box(
modifier = Modifier
.fillMaxHeight()
.clickable {
Toast
.makeText(editorContext.activity, "Hello World Clicked!", Toast.LENGTH_SHORT)
.show()
},
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Hello World",
)
}
}
}

Required and optional properties:

  • id - the unique id of the custom item. Note that it is highly recommended that every unique EditorComponent has a unique id. By default it contains a random value.

  • scope - the scope of this component. Every new value will trigger recomposition of all ScopedPropertys such as visible, enterTransition, and exitTransition. Consider using Compose State objects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component CanvasMenu - CanvasMenu.Scope) is updated and when you want to observe changes from the Engine. By default it is derived from the parent component scope.

  • visible - whether the custom item should be visible. Default value is always true.

  • enterTransition - transition of the custom item when it enters the parent composable. Default value is always no enter transition.

  • exitTransition - transition of the custom item when it exits the parent composable. Default value is always no exit transition.

  • decoration - render your custom item here. You are responsible for drawing the UI, handling clicks, and applying any custom styling. Default value is always no decoration.

List of Available CanvasMenu.Buttons#

All predefined buttons are available as composable functions in the CanvasMenu.Button namespace. Each function returns a CanvasMenu.Button with default properties that you can customize as shown in the Create New Buttons section.

ButtonIDDescription
CanvasMenu.Button.rememberBringForwardCanvasMenu.Button.Id.bringForwardBrings forward currently selected design block via EditorEvent.Selection.BringForward.
CanvasMenu.Button.rememberSendBackwardCanvasMenu.Button.Id.sendBackwardSends backward currently selected design block via EditorEvent.Selection.SendBackward.
CanvasMenu.Button.rememberDuplicateCanvasMenu.Button.Id.duplicateDuplicates currently selected design block via EditorEvent.Selection.Duplicate.
CanvasMenu.Button.rememberDeleteCanvasMenu.Button.Id.deleteDeletes currently selected design block via EditorEvent.Selection.Delete.
CanvasMenu.Button.rememberSelectGroupCanvasMenu.Button.Id.selectGroupSelects the group design block that contains the currently selected design block via EditorEvent.Selection.SelectGroup.