Chapter 39. Custom x::w::canvas

Index

Horizontal and vertical metrics
Mixin templates
Drawing custom widgets
Scratch buffers
Overriding process_button_event()
Custom canvas widget

customcanvas.C shows an example of creating a custom subclass of the x::w::canvas widget that draws a circle. This example demonstrates:

Horizontal and vertical metrics

Canvas's implementation object class's, x::w::canvasObj::implObj's constructor takes a parameter that sets its minimum, preferred, and maximum size (as well as the optional background color). This is the only widget in its window, so the window becomes resizable within the specified constraints.

Mixin templates

LibCXXW's internal header files define several mixin templates. Each such template creates a class that inherits from its template parameter. This class is usually a subclass of x::w::child_elementObj. The template either directly overrides any methods itself, or also multiply inherits from a mixin class; then overrides or defines methods inherited from its x::w::child_elementObj superclass using the mixin-provided functionality.

This example uses two mixin templates that implement a simplified interface for drawing the contents of the widget, and specifying a theme-based color for drawing this circle.

Drawing custom widgets

LibCXXW implements several classes that provide access to the graphic context, pixmap, and picture resources of the underlying X protocol and its RENDER extension. customcanvas.C uses them to draw a circle in the custom widget.

/*
** Copyright 2018-2021 Double Precision, Inc.
** See COPYING for distribution information.
*/

#include "config.h"
#include <x/mpobj.H>
#include <x/exception.H>
#include <x/destroy_callback.H>
#include <x/ref.H>
#include <x/obj.H>
#include <x/appid.H>

#include <x/w/main_window.H>
#include <x/w/gridlayoutmanager.H>
#include <x/w/gridfactory.H>
#include <x/w/canvas.H>
#include <x/w/button_event.H>

#include <x/w/impl/canvas.H>
#include <x/w/impl/background_color_element.H>
#include <x/w/impl/scratch_and_mask_buffer_draw.H>
#include <x/w/impl/container.H>

#include <string>
#include <iostream>

#include "close_flag.H"

std::string x::appid() noexcept
{
	return "customcanvas.examples.w.libcxx.com";
}

// Implementation object derives from the canvas element implementation object.
//
// Mixin templates:
//
// background_color_elementObj: attaches a background_color to the
// implementation object, the color to draw the circle with.
//
// scratch_and_mask_buffer_draw: simplified element drawing. Draw the entire
// element, and provide an additional masking pixmap.

struct fore_color_tag; // Tag for the background_color_element

class my_canvas_implObj : public x::w::scratch_and_mask_buffer_draw<
	x::w::background_color_elementObj<x::w::canvasObj::implObj,
					  fore_color_tag>> {

	// Alias for the superclass.

	typedef x::w::scratch_and_mask_buffer_draw<
		x::w::background_color_elementObj<x::w::canvasObj::implObj,
						  fore_color_tag>
		> superclass_t;


	static constexpr double default_minimum=20,
		default_preferred=50,
		default_maximum=100;


	double minimum=default_minimum;
	double preferred=default_preferred;
	double maximum=default_maximum;

public:

	using x::w::canvasObj::implObj::update;

	// Construction needs to take care of several details.

	// 1) Prepare a canvas_init_params for canvasObj::implObj's constructor,
	// that specifies the size of the display element, in millimeters
	//
	// 2) It would be nice if we could construct the masking scratch buffer
	// specifying the dimensions we expect to have. The preferred
	// element size is specified as 50x50 millimeters, and the scratch
	// buffer wants the size specified in pixels.
	//
	// Well, the canvas constructor computes the initial metrics for the
	// display element in pixels, so we can use this to initialize the
	// scratch buffer mixin.
	//
	// Start the ball rolling by constructing the canvas_init_params

	my_canvas_implObj(const x::w::container_impl &parent_container)
		: my_canvas_implObj
		{
			parent_container,
			x::w::canvas_init_params{
				// Horizontal metrics:
				{default_minimum,
				 default_preferred,
				 default_maximum},

				// Vertical metrics:
				{default_minimum,
				 default_preferred,
				 default_maximum},

				// Label ID for the custom
				// element's scratch buffer.

				"my_canvas@customcanvas.examples.w.libcxx.com"
			}
		}
	{
	}

	// Now, we use x::w::canvasObj::implObj::create_child_element_params
	// to compute the metrics of the display elements in pixels.

	my_canvas_implObj(const x::w::container_impl &parent_container,
			  const x::w::canvas_init_params &init_params)
		: my_canvas_implObj{parent_container,
			init_params,
			create_child_element_params(parent_container,
						    init_params)}
	{
	}

	// Now we have all the info we need.
	my_canvas_implObj(const x::w::container_impl &parent_container,
			  const x::w::canvas_init_params &init_params,
			  const x::w::child_element_init_params
			  &child_init_params)
		: superclass_t{

		// scratch_and_mask_buffer_draw mixin constructor params:
		//
		// Label ID for the scratch mask.
		        "my_canvas_mask@customcanvas.examples.w.libcxx.com",


		// The background_color_element mixin constructor param:

		// The circle's color. Take color "0%" from the theme.

		        "0%",

		// Finally, the parameters to canvasObj::implObj's constructor:
			parent_container, init_params, child_init_params}
	{
	}

	~my_canvas_implObj()=default;

	// Implement do_draw().

	void do_draw(ONLY IN_THREAD,
		     const x::w::draw_info &di,
		     const x::w::picture &area_picture,
		     const x::w::pixmap &area_pixmap,
		     const x::w::gc &area_gc,
		     const x::w::picture &mask_picture,
		     const x::w::pixmap &mask_pixmap,
		     const x::w::gc &mask_gc,
		     const x::w::clip_region_set &clipped,
		     const x::w::rectangle &area_entire_rect) override;

	// Override process_button_event().

	bool process_button_event(ONLY IN_THREAD,
				  const x::w::button_event &be,
				  xcb_timestamp_t timestamp) override;
};

typedef x::ref<my_canvas_implObj> my_canvas_impl;

// Draw our custom canvas display element.

void my_canvas_implObj::do_draw(ONLY IN_THREAD,
				const x::w::draw_info &di,

				// The picture, pixmap, and the graphic context
				// for the drawing area.

				const x::w::picture &area_picture,
				const x::w::pixmap &area_pixmap,
				const x::w::gc &area_gc,

				// The picture, pixmap, and the graphic context
				// for the 1-bit masking buffer.
				const x::w::picture &mask_picture,
				const x::w::pixmap &mask_pixmap,
				const x::w::gc &mask_gc,

				// A handle indicating that the drawing area
				// is ready, and clipped.
				const x::w::clip_region_set &clipped,

				// Give the current size of the display element.
				//
				// x & y are always 0, and width+height gives
				// the element's current size, in pixels.
				const x::w::rectangle &area_entire_rect)
{
	// We are responsible for clearing the masking buffer.

	x::w::gc::base::properties props;

	props.function(x::w::gc::base::function::CLEAR);
	mask_gc->fill_rectangle(area_entire_rect, props);

	// The circle's size is 1/10th the size of the display element.

	x::w::dim_t circle_width=area_entire_rect.width/10;
	x::w::dim_t circle_height=area_entire_rect.height/10;

	// Draw a filled in circle, then clear our the center of the circle.
	// We end up with the mask to draw the border with.
	//
	// Then we'll reduce the radius by twice the circle_width/height, and
	// clear the inner part of the circle.

	props.function(x::w::gc::base::function::SET);

	// Fill in a circle in the rectangle that, basically, encompasses our
	// entire drawing area.
	mask_gc->fill_arc(0, 0, area_entire_rect.width, area_entire_rect.height,
			  0, 360*64, props);

	if (circle_width > 0 && circle_height > 0)
	{
		// Subtract circle_width/height*2 from the element's size.
		//
		// This gives the width/height of the inner size of the circle.

		x::w::dim_t inner_width=area_entire_rect.width
			-circle_width-circle_width;

		x::w::dim_t inner_height=area_entire_rect.height
			-circle_height-circle_height;

		props.function(x::w::gc::base::function::CLEAR);

		// So we now clear a circle in a rectangle whose top-left
		// corner is (circle_width, circle_height), and whose size is
		// inner_width x inner_height.
		//
		// The library uses type-safe integer types like dim_t and
		// coord_t that are not only type-safe, but also guard against
		// overflows. Which is the reason why, above, the width/height
		// get subtracted twice, instead of subtracting width/height*2
		// (the multiplication results in a larger integer type.
		//
		// The first two parameters to fill_arc are coord_t-s, so
		// need to use truncate() in order to pass the dim_t-s.

		mask_gc->fill_arc(x::w::coord_t::truncate(circle_width),
				  x::w::coord_t::truncate(circle_height),
				  inner_width,
				  inner_height,
				  0, 360*64, props);
	}

	// Now retrieve the circle_color

	x::w::background_color current_color=
		x::w::background_color_element<fore_color_tag>::get(IN_THREAD);

	x::w::const_picture current_color_picture=
		current_color->get_current_color(IN_THREAD);

	// And use the mask to composite the circle into the scratch buffer,
	// which is already filled in with the default background color.

	area_picture->composite(current_color_picture,
				mask_picture,
				0, 0, // src_x, src_y
				0, 0, // mask_x, mask_y
				0, 0, // dst_x, dst_y
				area_entire_rect.width,
				area_entire_rect.height,
				x::w::render_pict_op::op_over);
}

/////////////////////////////////////////////////////////////////////
//
// An implementation object overrides process_button_event() in order to
// to do something with button clicks.

bool my_canvas_implObj::process_button_event(ONLY IN_THREAD,
					     const x::w::button_event &be,
					     xcb_timestamp_t timestamp)
{
	// If the overriden process_button_event() does not wish to process
	// this button event: it should invoke the overriden method.

	// Take action on pointer button #1 clicks. activate_for() is just
	// a fancy check for a button press, as opposed to a button release,
	// event.

	if (!activate_for(be) || be.button != 1)
		return superclass_t::process_button_event(IN_THREAD, be,
							  timestamp);

	// In response to button #1 clicks we toggle the element metrics
	// between minimum/preferred/maximum of 20/50/100 and 110/140/150.
	//
	// Our container will oblige and resize us accordingly. Note that
	// after the first resize-ment, subsequent ones result in very little
	// adjustment. This is because the largest metric's minimum size
	// is 100. When we flip back to 20/50/100, since the maximum is 100
	// our size only gets reduced slightly to 100, from 110.

	minimum=130-minimum;
	preferred=190-preferred;
	maximum=250-maximum;

	update(IN_THREAD,
	       {minimum, preferred, maximum},
	       {minimum, preferred, maximum});

	return true;
}

// "Public" object subclasses the public canvas object. Not really needed
// here, just for completeness sake.

class my_canvasObj : public x::w::canvasObj {

public:

	const my_canvas_impl impl; // My implementation object.

	// Constructor

	my_canvasObj(const my_canvas_impl &impl)
		: x::w::canvasObj{impl}, impl{impl}
	{
	}

	~my_canvasObj()=default;
};

typedef x::ref<my_canvasObj> my_canvas;

void testcustomcanvas()
{
	x::destroy_callback::base::guard guard;

	auto close_flag=close_flag_ref::create();

	auto main_window=x::w::main_window::create
		([&]
		 (const auto &main_window)
		 {
			 auto layout=main_window->gridlayout();

			 auto factory=layout->append_row();
			 factory->padding(0);

			 // Obtain the parent container from the factory.

			 x::w::container_impl parent_container=
				 factory->get_container_impl();

			 // Create the "implementation" object for the custom
			 // display element.

			 auto impl=my_canvas_impl::create(parent_container);

			 // Create the "public" object for the custom display
			 // element.
			 auto c=my_canvas::create(impl);

			 // Notify the factory that a new display element
			 // has been created, and it goes into its parent
			 // container.

			 factory->created_internally(c);

		 },
		 x::w::new_gridlayoutmanager{});

	main_window->set_window_title("Custom canvas");

	guard(main_window->connection_mcguffin());

	main_window->on_disconnect([]
				   {
					   _exit(1);
				   });

	main_window->on_delete
		([close_flag]
		 (THREAD_CALLBACK,
		  const auto &ignore)
		 {
			 close_flag->close();
		 });

	main_window->show_all();

	close_flag->wait();
}

int main(int argc, char **argv)
{
	try {
		testcustomcanvas();
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}

Horizontal and vertical metrics

x::w::canvasObj::implObj's constructor first parameter is a reference to the new widget's parent container. Its second parameter is an x::w::canvas_init_params object. The object's first two members specify the new widget's metrics: the horizontal metric and the vertical metric. They are specified as a x::w::dim_axis_arg values.

A horizontal or a vertical metric consists of three discrete values: minimum, preferred, and maximum; and the metric specifies that the widget's width or height should have this minimum, preferred, or maximum size. The widget's container should size the widget to at least its minimum width, or height; but no larger than the maximum value. The preferred metric requests the widget's initial width and height; however in a container with multiple widgets it's unlikely that the container will make everyone happy.

Widgets do not get explicitly resized, directly. This is done indirectly by updating their metrics. Their container and its layout manager always have the final word on each widget's actual size.

Note

Its possible that a container resizes a widget to be larger than its maximum requested metric. For example, the grid layout manager can be instructed to use x::w::halign::fill or x::w::valign::fill for an widget's alignment, which may exceed the widget's maximum metric.

It's unlikely that a container will size an widget to be smaller than its minimum metrics. However, a new widget's width or height is usually 0, until it gets formally sized by its container. So it's possible that a widget's state indicates that its smaller than its minimum metric; this can also happen in some edge cases when the parent container has no other options.

In conclusion: custom widgets must be prepared to draw themselves when their size is outside of their declared metrics. Widgets should still reasonably draw their contents if their size exceeds their maximum metrics. It is acceptable to simply draw an empty widget if their size is smaller than their minimum metrics.

x::w::dim_arg specifies the value of a minimum, preferred, or maximum metric. The value may be:

  • A text string. This references a width or height defined by the current display theme, by name. An exception gets thrown if the theme does not define the width/height with this label.

  • A double value specifies the width or height in millimeters. The actual pixel size gets computed based on the display screen's resolution, and scaled by the current theme scaling factor.

  • Additionally, a NAN value for a maximum metric, and only the maximum metric, sets an unlimited maximum metric. The widget has no defined maximum width or height.

	  /* ... */

x::w::canvas_init_params{
   {20.0, 50.0, 100.0},
   {20.0, 50.0, 100.0},
   "my_canvas@examples.w.libcxx.com"}
	

customcanvas.C creates horizontal and vertical metrics that set the custom canvas's minimum width and height to 20 millimeters, the preferred width and height to 50 millimeters, and 100 millimeters as the maximum metric. This is the only widget in the window, so the resulting window is resizable within these bounds. Replacing the 100 millimeter value with NAN results in a window that has no upper maximum width or height.

x::w::canvasObj::implObj major selling point, as a candidate for a custom subclass, is that its size gets specified in millimeters and does not depend on the display screen's resolution. Furthermore, it automatically updates its metrics, in realtime, if the current display theme scaling factor gets adjusted by cxxwtheme. If the window's size ends up being outside the minimum or maximum metrics because of the new theme scaling factor, the window gets resized accordingly.