Skip to content

InkCanvas

Header: winui/ui/media/InkCanvas.hpp. Spec: winui/ui/media/InkCanvas.hpp.

A surface the user draws ink on with the pointer.

In Inking mode a press starts a stroke, every move adds a point and the release commits it to strokes(), raising on_strokes_collected(). A press without a move leaves a dot. The stroke is drawn as it grows, each point at the pen's pressure. A mouse always reports 0.5. In Erasing mode the drag removes each stroke it passes over and raises on_strokes_erased(). A pen's eraser end or eraser button does the same in Inking mode, unless set_eraser_input_enabled(false) turns it off. With fit_to_curve, strokes are smoothed into Bézier curves. Highlighter strokes are drawn first and translucent, the rest on top, all clipped to the canvas. The canvas has no background of its own, so put it in a ui::BorderControl for paper.

Only the devices in input_device_types() draw. By default these are the pen and the mouse, which leaves touch to whatever scrolls the canvas. A right-button drag, including a pen with its barrel button held, draws nothing and goes to on_unprocessed_input() (see set_right_drag_action()). So does every press, move and release in None mode. Use that input to build a lasso or another tool of the app's own. A ruler and a protractor, shown with set_ruler() and set_protractor(), lie over the ink to draw straight lines and arcs along. With set_erase_mode(InkCanvasEraseMode::Point) the eraser removes only the ink under it, eraser_size() wide, and leaves the rest of a stroke it crosses as new strokes. strokes() is the data to save, in whatever format the app likes. ui::InkCanvas is the declarative form, and ui::InkToolbarControl is a ready-made set of tools for it.

Example:

auto  owned  = std::make_unique<ui::InkCanvasControl>();
auto* canvas = owned.get();
canvas->set_drawing_attributes({.color = ui::Color::from_rgba(230, 27, 27), .size = {4.0f, 4.0f}});
canvas->on_strokes_collected([](std::span<const ui::InkStroke> added)
                             { std::println("{} new stroke(s)", added.size()); });
window.set_content(std::move(owned));

canvas->set_input_mode(ui::InkCanvasInputMode::Erasing); // the next drag erases
canvas->strokes().clear();                                // or wipe it all

ui::InkCanvas

A surface the user draws ink on. Builds a ui::InkCanvasControl, whose description says what the pointer does in each mode.

strokes is the drawing it starts with. Read and change the strokes later through the control's InkCanvasControl::strokes(). The canvas has no background of its own, so it usually sits in a ui::Border.

Example:

ui::Control<ui::InkCanvas> canvas;
ui::Property<std::size_t>  count{0};

window.set_content(ui::VerticalStack{.spacing = 8, .content = {
    ui::Text{.text = ui::bind(count, [](std::size_t n) { return std::format("{} stroke(s)", n); })},
    ui::Border{
        .background       = ui::colors::White,
        .border_brush     = ui::Color::from_rgba(0, 0, 0, 40),
        .border_thickness = ui::Thickness{1.0f},
        .height           = 280.0f,
        .content          = ui::InkCanvas{
            .drawing_attributes = ui::InkDrawingAttributes{.color = ui::Color::from_rgba(0, 77, 230),
                                                           .size  = {4.0f, 4.0f}},
            .accessibility      = {.name = "Drawing canvas"},
            .on_strokes_changed = [&] { count = canvas->strokes().size(); },
            .control            = canvas,
        },
    },
    ui::Button{.text = "Clear", .on_click = [&] { canvas->strokes().clear(); }},
}});

Every field, in the order a designated initializer names them:

ui::InkCanvas{
    .drawing_attributes, .input_mode, .input_device_types, .eraser_input_enabled, .erase_mode,
    .eraser_size, .right_drag_action, .ruler, .protractor, .strokes, .style, .margin, .width,
    .height, .min_width, .min_height, .max_width, .max_height, .horizontal_alignment,
    .vertical_alignment, .visible, .tool_tip, .enabled, .accessibility, .on_strokes_collected,
    .on_strokes_erased, .on_strokes_changed, .on_unprocessed_input, .on_pointer_entered,
    .on_pointer_exited, .on_got_focus, .on_lost_focus, .on_key_down, .on_wheel, .control
}

Fields, in declaration order (designators must follow it):

Field Type Default
drawing_attributes ui::Binding<ui::InkDrawingAttributes> The attributes the next stroke is drawn with.
input_mode ui::Binding<ui::InkCanvasInputMode> {ui::InkCanvasInputMode::Inking} What a left-button drag does: draw (the default), erase whole strokes, or nothing.
input_device_types ui::Binding<ui::PointerDeviceTypes> {ui::PointerDeviceTypes::Pen \| ui::PointerDeviceTypes::Mouse} The devices whose presses draw or erase. The pen and the mouse by default.
eraser_input_enabled ui::Binding<bool> {true} Whether a pen's eraser end erases in Inking mode.
erase_mode ui::Binding<ui::InkCanvasEraseMode> {ui::InkCanvasEraseMode::Stroke} What the eraser removes: whole strokes, the default, or only the ink under it.
eraser_size ui::Binding<float> {8.0f} The point eraser's width, DIPs.
right_drag_action ui::Binding<ui::InkCanvasRightDragAction> {ui::InkCanvasRightDragAction::LeaveUnprocessed} Whether a right-button drag inks or goes to on_unprocessed_input, the default.
ruler ui::Binding<ui::InkRuler> The ruler. Hidden by default.
protractor ui::Binding<ui::InkProtractor> The protractor. Hidden by default.
strokes std::vector<ui::InkStroke> The strokes the canvas starts with. Each gets a fresh id.
on_strokes_collected ui::Callback<void(std::span<const ui::InkStroke>)> The stroke the user drew, when it is committed.
on_strokes_erased ui::Callback<void(std::span<const ui::InkStroke>)> The strokes the eraser removed.
on_strokes_changed ui::Callback<void()> Any change to the strokes: drawn, erased, or changed through the container.
on_unprocessed_input ui::Callback<void(const ui::PointerEvent&)> The pointer input the canvas doesn't turn into ink, in canvas DIPs.
on_pointer_entered ui::Action The pointer moved onto the control.
on_pointer_exited ui::Action The pointer left the control.
on_got_focus ui::Action Keyboard focus arrived on the control, or on anything inside it, from outside it. A control with nothing focusable in it never gets focus, and never raises this.
on_lost_focus ui::Action Keyboard focus left the control and everything inside it.
on_key_down ui::Callback<ui::Handling(const ui::KeyEvent&)> A key pressed while focus is on the control or inside it, before the control's own handling; ui::Handled stops it there (Element::on_key_down()).
on_wheel ui::Callback<ui::Handling(const ui::PointerEvent&)> A wheel turn over the control, before the control scrolls; ui::Handled stops it there (Element::on_wheel()).
control ui::ControlOut<ui::InkCanvas> Out: the handle that receives the built control.

Plus the common fields style, margin, width, height, min_width, min_height, max_width, max_height, horizontal_alignment, vertical_alignment, visible, tool_tip, enabled, accessibility every spec carries (conventions).

build

std::unique_ptr<Element> build()&&

Consumes the spec and returns the built ui::InkCanvasControl; the caller owns it.

ui::InkCanvasControl

Derives from ui::Element.

A surface the user draws ink on with the pointer.

In Inking mode a press starts a stroke, every move adds a point and the release commits it to strokes(), raising on_strokes_collected(). A press without a move leaves a dot. The stroke is drawn as it grows, each point at the pen's pressure. A mouse always reports 0.5. In Erasing mode the drag removes each stroke it passes over and raises on_strokes_erased(). A pen's eraser end or eraser button does the same in Inking mode, unless set_eraser_input_enabled(false) turns it off. With fit_to_curve, strokes are smoothed into Bézier curves. Highlighter strokes are drawn first and translucent, the rest on top, all clipped to the canvas. The canvas has no background of its own, so put it in a ui::BorderControl for paper.

Only the devices in input_device_types() draw. By default these are the pen and the mouse, which leaves touch to whatever scrolls the canvas. A right-button drag, including a pen with its barrel button held, draws nothing and goes to on_unprocessed_input() (see set_right_drag_action()). So does every press, move and release in None mode. Use that input to build a lasso or another tool of the app's own. A ruler and a protractor, shown with set_ruler() and set_protractor(), lie over the ink to draw straight lines and arcs along. With set_erase_mode(InkCanvasEraseMode::Point) the eraser removes only the ink under it, eraser_size() wide, and leaves the rest of a stroke it crosses as new strokes. strokes() is the data to save, in whatever format the app likes. ui::InkCanvas is the declarative form, and ui::InkToolbarControl is a ready-made set of tools for it.

Example:

auto  owned  = std::make_unique<ui::InkCanvasControl>();
auto* canvas = owned.get();
canvas->set_drawing_attributes({.color = ui::Color::from_rgba(230, 27, 27), .size = {4.0f, 4.0f}});
canvas->on_strokes_collected([](std::span<const ui::InkStroke> added)
                             { std::println("{} new stroke(s)", added.size()); });
window.set_content(std::move(owned));

canvas->set_input_mode(ui::InkCanvasInputMode::Erasing); // the next drag erases
canvas->strokes().clear();                                // or wipe it all

InkCanvasControl

InkCanvasControl()

An empty canvas in Inking mode with the default drawing attributes.

~InkCanvasControl

~InkCanvasControl() override

strokes

InkStrokeContainer& strokes()

The committed strokes.

strokes

const InkStrokeContainer& strokes() const

The committed strokes, read-only.

wet_stroke

const InkStroke* wet_stroke() const

The stroke being drawn right now, or nullptr between strokes. Not yet in strokes().

drawing_attributes

const InkDrawingAttributes& drawing_attributes() const

What the next stroke is drawn with.

set_drawing_attributes

void set_drawing_attributes(const InkDrawingAttributes& attributes)

Sets what the next stroke is drawn with. Strokes already drawn keep their own attributes.

input_mode

InkCanvasInputMode input_mode() const

What pointer input does. Inking by default.

set_input_mode

void set_input_mode(InkCanvasInputMode mode)

Switches what the next press does. A stroke in progress is committed first.

input_device_types

PointerDeviceTypes input_device_types() const

The devices whose presses draw or erase. Pen | Mouse by default.

set_input_device_types

void set_input_device_types(PointerDeviceTypes devices)

Sets the devices that draw or erase. Takes effect from the next press. A stroke in progress is finished with the device that started it.

eraser_input_enabled

bool eraser_input_enabled() const

Whether a pen's eraser erases in Inking mode. True by default. When off, the eraser end draws nothing.

set_eraser_input_enabled

void set_eraser_input_enabled(bool enabled)

Turns the pen's eraser on or off. Takes effect from the next press.

erase_mode

InkCanvasEraseMode erase_mode() const

What the eraser removes: whole strokes, the default, or only the ink under it.

set_erase_mode

void set_erase_mode(InkCanvasEraseMode mode)

Sets what the eraser removes. Takes effect from the next eraser move.

eraser_size

float eraser_size() const

The point eraser's width in DIPs, 8 by default.

set_eraser_size

void set_eraser_size(float size)

Sets the point eraser's width, at least 1 DIP.

right_drag_action

InkCanvasRightDragAction right_drag_action() const

Whether a right-button drag inks or goes to on_unprocessed_input(). LeaveUnprocessed by default.

set_right_drag_action

void set_right_drag_action(InkCanvasRightDragAction action)

Sets what a right-button drag does. Takes effect from the next press.

ruler

const InkRuler& ruler() const

The ruler. Hidden by default.

set_ruler

void set_ruler(const InkRuler& ruler)

Shows, hides, moves or turns the ruler. Redraws.

protractor

const InkProtractor& protractor() const

The protractor. Hidden by default.

set_protractor

void set_protractor(const InkProtractor& protractor)

Shows, hides, moves or turns the protractor. Redraws.

on_stencils_changed

void on_stencils_changed(Callback<void()> handler)

Raised when the user drags or turns the ruler or the protractor.

lasso_outline

std::span<const Point> lasso_outline() const

The outline set_lasso_outline() draws. Empty by default.

set_lasso_outline

void set_lasso_outline(std::vector<Point> outline)

Draws outline, in canvas DIPs, as a dashed line over the ink and the stencils. Use it to show the trail of a drag that selects strokes with InkStrokeContainer::select_with_polyline(). An empty outline hides it. Redraws.

on_strokes_collected

void on_strokes_collected(Callback<void(std::span<const InkStroke>)> handler)

Raised with the stroke the user drew, when it is committed to strokes().

on_strokes_erased

void on_strokes_erased(Callback<void(std::span<const InkStroke>)> handler)

Raised with the strokes the eraser removed, after they are gone. The point eraser reports the strokes it cut, and what is left of them stays as new strokes.

on_strokes_changed

void on_strokes_changed(Callback<void()> handler)

Raised after any change to strokes(): drawn, erased or changed through the container.

on_unprocessed_input

void on_unprocessed_input(Callback<void(const PointerEvent&)> handler)

Raised for pointer input the canvas doesn't turn into ink, with the position in canvas DIPs. In None mode that is every press, move and release over the canvas, plus the moves of a drag that left it. In the other modes it is a right-button drag, from press to release, when the right-drag action is InkCanvasRightDragAction::LeaveUnprocessed. A Leave event ends a drag the window lost.

automation_role

AutomationRole automation_role() const override

Exposed to UI Automation as a pane.

measure_override

Size measure_override(Size available, const MeasureContext& context) const override

Returns no size of its own. A canvas is as large as its slot, or its width and height, make it.

render_override

void render_override(Renderer& renderer, const RenderContext& context) const override

Draws the highlighter strokes, the other strokes, the wet stroke, the stencils and the lasso outline, clipped to the bounds.

wheel_override

bool wheel_override(const PointerEvent& e) override

Turns the ruler or the protractor under the pointer. Anywhere else the wheel isn't consumed.

pointer_override

bool pointer_override(const PointerEvent& e) override

Left-button drags (pen tip or finger) from input_device_types() draw or erase, as input_mode() and the pen's eraser say. Input that isn't inked goes to on_unprocessed_input().

enabled_changed

void enabled_changed() override

is_enabled() flipped, through set_enabled() here or on an ancestor, or a move to another parent. Drop any press or hover state. The element is redrawn either way. No-op by default.

class InkStrokeContainer

The strokes on a ui::InkCanvasControl. Read them, add, remove, clear or replace them all, and select, move and delete some of them.

Every stroke that goes in gets a fresh id in place of the one it came with, so a set saved with strokes() and handed back to replace() comes back renumbered. Every change, including a change of selection, redraws the canvas and raises InkCanvasControl::on_strokes_changed(). Each canvas owns one container, reached through InkCanvasControl::strokes().

Selection is the basis for a lasso. The canvas reports the pointer input it doesn't turn into ink through InkCanvasControl::on_unprocessed_input(). The app collects the outline, passes it to select_with_polyline(), then moves or deletes the selected strokes.

Example:

// Keep a copy, clear the canvas, and put the drawing back later.
auto canvas = std::make_unique<ui::InkCanvasControl>();
std::vector<ui::InkStroke> saved{canvas->strokes().strokes().begin(), canvas->strokes().strokes().end()};
canvas->strokes().clear();
canvas->strokes().replace(std::move(saved));

InkStrokeContainer

InkStrokeContainer() = default

InkStrokeContainer

InkStrokeContainer(const InkStrokeContainer&) = delete

operator=

InkStrokeContainer& operator=(const InkStrokeContainer&) = delete

strokes

std::span<const InkStroke> strokes() const

Every stroke, oldest first.

size

std::size_t size() const

The number of strokes.

empty

bool empty() const

True when there are no strokes.

stroke_by_id

const InkStroke* stroke_by_id(std::uint32_t id) const

The stroke with id, or nullptr. The pointer lasts until the next change.

add_stroke

std::uint32_t add_stroke(InkStroke stroke)

Appends stroke on top of the others and returns the id it was given. A stroke with no points is dropped and 0 is returned.

add_strokes

void add_strokes(std::vector<InkStroke> strokes)

Appends each stroke in order, as one change.

remove_stroke

bool remove_stroke(std::uint32_t id)

Removes the stroke with id. Returns false when there is none.

clear

void clear()

Removes every stroke. Does nothing, and raises no change, when there are none.

replace

void replace(std::vector<InkStroke> strokes)

Removes every stroke and adds strokes in their place, as one change. Use it to load a saved drawing.

bounding_rect

Rect bounding_rect() const

The union of every stroke's bounding_rect(). Empty when there are none.

select_with_polyline

Rect select_with_polyline(std::span<const Point> points)

Selects the strokes lying wholly inside the outline points and deselects the rest. The outline is closed back to its first point. Returns the selection's bounding rect, empty when nothing is inside. Pass it the outline of a lasso drawn over the canvas.

select_with_line

Rect select_with_line(Point from, Point to)

Selects the strokes the line from from to to crosses and deselects the rest. Returns the selection's bounding rect, empty when it crosses none.

set_selected

bool set_selected(std::uint32_t id, bool selected)

Selects or deselects the stroke with id. Returns false when there is none.

clear_selection

void clear_selection()

Deselects every stroke.

has_selection

bool has_selection() const

Whether any stroke is selected.

selection_bounding_rect

Rect selection_bounding_rect() const

The union of the selected strokes' bounding_rect(). Empty when none is selected.

move_selected

Rect move_selected(Point translation)

Moves every selected stroke by translation DIPs. Returns the selection's new bounding rect.

delete_selected

Rect delete_selected()

Removes every selected stroke. Returns the rect they covered, empty when none was selected.

struct InkStroke

One stroke of ink: its points, in drawing order, and how it looks.

A stroke is a plain value. A ui::InkStrokeContainer gives it an id when it is added.

Example:

auto          canvas = std::make_unique<ui::InkCanvasControl>();
ui::InkStroke tick{.points = {{.position = {20, 50}}, {.position = {40, 70}}, {.position = {80, 20}}}};
tick.attributes.color = ui::Color::from_rgba(0, 128, 85);
canvas->strokes().add_stroke(std::move(tick));

points

std::vector<InkPoint> points {}

The sampled points, in canvas DIPs.

attributes

InkDrawingAttributes attributes {}

Color, size, nib and the rest.

id

std::uint32_t id = 0

Unique within its container. 0 for a stroke in no container.

selected

bool selected = false

Whether the stroke is selected. A selected stroke is drawn hollow in its own color, and the container's move_selected() and delete_selected() act on it.

bounding_rect

Rect bounding_rect() const

The area the stroke paints: its points' bounds grown by half the nib at its widest. An empty rect at the origin for a stroke with no points.

struct InkPoint

One sampled point of an ink stroke: where it is, in the canvas's own DIPs (0,0 is the canvas's top-left corner), and how hard the pen pressed there.

Example:

ui::InkStroke line;
line.points = {{.position = {10, 10}}, {.position = {90, 40}, .pressure = 0.9f}};

Field Type Default
position Point Canvas DIPs.
pressure float 0.5f 0 to 1, the pen's PointerEvent::pressure. A mouse always reports 0.5.

struct InkDrawingAttributes

How a stroke looks. The defaults are a black, round, 2 DIP pen.

size is the nib's width and height. A pressure-sensitive stroke scales it at each point: a quarter of the size at no pressure, the full size at 0.5 and 1.75 times the size at full pressure. ignore_pressure keeps every point at the full size. A highlighter is drawn at half opacity and under the ordinary ink, so writing stays legible over it.

Example:

auto canvas = std::make_unique<ui::InkCanvasControl>();
ui::InkDrawingAttributes marker{
    .color               = ui::Color::from_rgba(255, 230, 0),
    .size                = {6.0f, 18.0f},
    .pen_tip             = ui::InkPenTip::Rectangle,
    .draw_as_highlighter = true,
};
canvas->set_drawing_attributes(marker);

color

Color color = colors::Black

The ink's color.

size

Size size = {2.0f, 2.0f}

The nib's width and height, DIPs.

pen_tip

InkPenTip pen_tip = InkPenTip::Circle

The nib's shape.

draw_as_highlighter

bool draw_as_highlighter = false

Translucent, drawn under other ink.

ignore_pressure

bool ignore_pressure = false

Every point at the full size.

fit_to_curve

bool fit_to_curve = true

Smooths the points into Bézier curves.

kind

InkDrawingAttributesKind kind = InkDrawingAttributesKind::Default

Pen or pencil ink.

pencil_opacity

float pencil_opacity = 1.0f

Opacity from 0 to 1, for pencil ink only.

for_pencil

static InkDrawingAttributes for_pencil()

The defaults with kind set to Pencil.

struct InkRuler

The ruler on a ui::InkCanvasControl, a straight edge to draw along.

While it shows, a press on its body drags it and the wheel over it turns it. A stroke started on or close outside one of its long edges is drawn straight along that edge, outside it, wherever the pointer goes. Its scale is in centimeters, at 96 DIPs to the inch, and it shows its angle.

Example:

auto canvas = std::make_unique<ui::InkCanvasControl>();
canvas->set_ruler({.visible = true, .angle = 30.0f}); // centered on the canvas, turned 30 degrees

Field Type Default
visible bool false Shown on the canvas.
center std::optional<Point> Its middle, canvas DIPs. nullopt is the canvas's center.
angle float 0.0f Degrees clockwise from horizontal.
length float 600.0f Its length, DIPs.
width float 80.0f Its width across, DIPs.
tick_marks bool true The centimeter scale.

struct InkProtractor

The protractor on a ui::InkCanvasControl, a circle to draw arcs along.

While it shows, a press on its body drags it and the wheel over it turns its scale. A stroke started on or close outside its edge is drawn around the circle, outside it.

Example:

auto canvas = std::make_unique<ui::InkCanvasControl>();
canvas->set_protractor({.visible = true, .center = ui::Point{200, 150}, .radius = 120.0f});

Field Type Default
visible bool false Shown on the canvas.
center std::optional<Point> Its center, canvas DIPs. nullopt is the canvas's center.
angle float 0.0f How far the scale is turned clockwise, degrees.
radius float 160.0f Its radius, DIPs.
tick_marks bool true A degree scale around the edge.
center_marker bool true A cross at the center.
angle_readout bool true A readout of its angle under the center.

Enumerations

InkPenTip

enum class InkPenTip

The shape of the nib a stroke is drawn with.

Value
Circle A round nib, elliptical when its width and height differ. Draws smooth, even lines.
Rectangle A flat, axis-aligned nib, like a highlighter's chisel tip.

InkDrawingAttributesKind

enum class InkDrawingAttributesKind

Which family of ink a stroke's attributes describe.

Value
Default Pen and highlighter ink.
Pencil Pencil ink: lighter, with a grain of specks, at pencil_opacity.

InkCanvasInputMode

enum class InkCanvasInputMode

What pointer input on a ui::InkCanvasControl does.

Value
None Draws and erases nothing. Pointer input goes to on_unprocessed_input().
Inking The default. A drag draws a stroke with drawing_attributes(), and a pen's eraser erases.
Erasing A left-button drag removes every whole stroke it touches (the stroke eraser).

InkCanvasRightDragAction

enum class InkCanvasRightDragAction

What a right-button drag on a ui::InkCanvasControl does. A pen pressed with its barrel button held counts as the right button.

Value
LeaveUnprocessed The default. The drag isn't inked and goes to on_unprocessed_input().
AllowProcessing Inks or erases as a left-button drag does.

InkCanvasEraseMode

enum class InkCanvasEraseMode

What the eraser removes.

Value
Stroke The default. Every whole stroke the eraser touches.
Point Only the ink under the eraser. A stroke it crosses splits in two.

Tests

  • tests/test_ink_canvas.cpp