Customize the canvas menu through complete replacement for strict control or modification for flexible extension.

Overview#
The canvas menu is the floating toolbar that appears when a design block is selected on the canvas. CE.SDK iOS provides two approaches for customizing it:
| Approach | Modifier | Best For |
|---|---|---|
| Replacement | .imgly.canvasMenuItems | Exact control over items and order, version-safe |
| Modification | .imgly.modifyCanvasMenuItems | Extending or adjusting the default configuration |
The context.selection property provides cached selection info (block, type, fillType, kind) optimized for the canvas menu’s presentation lifecycle.
Complete Replacement#
Use .imgly.canvasMenuItems to define the exact set of items from scratch. This approach is version-safe since you control everything explicitly.
.imgly.canvasMenuItems { context in CanvasMenu.Buttons.selectGroup() CanvasMenu.Divider() CanvasMenu.Buttons.bringForward() CanvasMenu.Buttons.sendBackward() CanvasMenu.Divider() CanvasMenu.Buttons.duplicate() CanvasMenu.Buttons.delete()}- The
@CanvasMenu.Builderclosure receives aCanvasMenu.Contextwith the current selection - No default items are included — only what you specify appears
- Item order is guaranteed across editor versions
Modifying Default Items#
Use .imgly.modifyCanvasMenuItems to extend or adjust the default configuration without replacing it entirely.
.imgly.modifyCanvasMenuItems { context, items inThe modifier provides a CanvasMenu.Modifier with these operations:
| Operation | Purpose |
|---|---|
items.addFirst(_:) | Prepend at beginning |
items.addLast(_:) | Append at end |
items.addBefore(id:_:) | Insert before specific item |
items.addAfter(id:_:) | Insert after specific item |
items.replace(id:_:) | Replace existing item |
items.remove(id:) | Remove item by ID |
Add items at the start or end:
items.addFirst { CanvasMenu.Button(id: "my.package.canvasMenu.button.first") { context in print("First Button action") } label: { context in Label("First Button", systemImage: "arrow.backward.circle") }}items.addLast { CanvasMenu.Button(id: "my.package.canvasMenu.button.last") { context in print("Last Button action") } label: { context in Label("Last Button", systemImage: "arrow.forward.circle") }}Position relative to existing items:
items.addBefore(id: CanvasMenu.Buttons.ID.sendBackward) { CanvasMenu.Button(id: "my.package.canvasMenu.button.beforeSendBackward") { context in print("Before Send Backward action") } label: { context in Label("Before Send Backward", systemImage: "arrow.backward.square") }}items.addAfter(id: CanvasMenu.Buttons.ID.bringForward) { CanvasMenu.Button(id: "my.package.canvasMenu.button.afterBringForward") { context in print("After Bring Forward action") } label: { context in Label("After Bring Forward", systemImage: "arrow.forward.square") }}Replace or remove items:
items.replace(id: CanvasMenu.Buttons.ID.duplicate) { CanvasMenu.Button(id: "my.package.canvasMenu.button.replacedDuplicate") { context in print("Replaced Duplicate action") } label: { context in Label("Replaced Duplicate", systemImage: "arrow.uturn.down.square") }}items.remove(id: CanvasMenu.Buttons.ID.delete)Item Types#
Predefined Buttons#
CE.SDK provides predefined buttons for common canvas actions:
CanvasMenu.Buttons.duplicate()| Button | ID |
|---|---|
CanvasMenu.Buttons.bringForward | .bringForward |
CanvasMenu.Buttons.sendBackward | .sendBackward |
CanvasMenu.Buttons.duplicate | .duplicate |
CanvasMenu.Buttons.delete | .delete |
CanvasMenu.Buttons.selectGroup | .selectGroup |
Each predefined button accepts optional action, label, isEnabled, and isVisible parameters to customize its behavior:
CanvasMenu.Buttons.delete( action: { context in context.eventHandler.send(.deleteSelection) }, label: { context in Label { Text("Delete") } icon: { Image.imgly.delete } }, isEnabled: { context in true }, isVisible: { context in try context.engine.block.isAllowedByScope(context.selection.block, key: "lifecycle/destroy") },)Custom Buttons#
Create custom buttons with unique IDs using reverse domain notation:
CanvasMenu.Button( id: "my.package.canvasMenu.button.newButton",) { context in print("New Button action")} label: { context in Label("New Button", systemImage: "star.circle")} isEnabled: { context in true} isVisible: { context in true}Dividers#
Use CanvasMenu.Divider() to visually separate groups of items. Adjacent dividers collapse automatically, and dangling leading or trailing dividers are removed. See the complete replacement example above for usage.
Custom Items#
For full control over rendering, conform to the CanvasMenu.Item protocol:
private struct CustomCanvasMenuItem: CanvasMenu.Item { var id: EditorComponentID { "my.package.canvasMenu.newCustomItem" }
func body(_ context: CanvasMenu.Context) throws -> some View { ZStack { RoundedRectangle(cornerRadius: 10) .fill(.conicGradient(colors: [.red, .yellow, .green, .cyan, .blue, .purple, .red], center: .center)) Text("New Custom Item") .padding(4) } .onTapGesture { print("New Custom Item action") } }
func isVisible(_ context: CanvasMenu.Context) throws -> Bool { true }}Use the custom item in your menu configuration:
CustomCanvasMenuItem()Next Steps#
- Add a New Button - Add custom buttons to editor components
- Rearrange Buttons - Customize button order in UI components
- Inspector Bar - Configure the property inspector sidebar
- Dock - Customize the bottom toolbar