Customizable Appearance of Canvas Controls - Google Summer of Code 2023
2023年9月7日12:00
Project Report by Sanidhya Singh (sanidhyas3s)
- Project Proposal and Blogs,
sanidhyas3s/gsoc-inkscape-proposal
- Working Branch of the Project,
sanidhyas3s/inkscape/customize-handles
- Merge Request,
!5624
The project and its motivation
Interacting with path nodes is a vital part of Inkscape's user experience. Unfortunately, node handles or canvas controls were not so perfect, not providing any direct customization with most of their styling hard-coded. As a result, their design hadn't evolved over time, leading to extensive UX discussions calling for changes to their default style, see UX#115
. However, with no unanimous agreement on a single style definition among designers, the ability to easily change the style seemed required. This project aims to address these challenges by providing users, including our designers, with a unified file for customizing on-canvas controls, ensuring consistency yet allowing specific modifications.
With this project, customizing handle styles is now possible through share/ui/node-handles.css
or the corresponding user config file (e.g., .config/inkscape/ui/node-handles.css
for Ubuntu). This simplifies the implementation of new defaults, such as UX#181
, without requiring designers to navigate through the code to locate various handles. Recognizing that our user base mainly comprises designers with distinct preferences and needs, providing them with the ability to modify canvas elements aligns with their expectations. Users can now modify the shapes of various handles, adjust stroke-width, fill and stroke colors, and tweak their opacities using this CSS. A detailed guide for the stylesheet and a sample in the form of the proposed defaults can be found at share/ui/node-handles.css
.
In addition to these functional enhancements, this project introduces two key features:
-
Outlines, handles can now include an outline surrounding their shapes, enhancing visibility, and addressing requests from
UX#181
andUX#115
. This outline is also customizable with its colors, thickness and opacity. -
Normal rendering mode, the XOR rendering mode, which was unanimously disliked by everyone and caused various issues (e.g.,
#929
), has been replaced. Handles are now rendered simply on top of one another based on their opacity, which hopefully solves most of these issues.
In addition to the core functional tasks, I also undertook an effort to improve the overall code quality. This included enhancing code readability, eliminating unused portions such as bitmap handles, and conducting a thorough cleanup of the _render
function. The result was a substantial reduction in dead or unnecessary code, evident in the fact that the count of removed lines nearly matched the count of added lines in the project's Merge Request. Minor performance improvements were achieved by eliminating the redundant construction of the same handle multiple times, by implementing a two-level caching system between the handles (discussed under Rendering and caching in the next section).
Code structure and implementation details
The majority of the code I authored resides in canvas-item-ctrl.h
and canvas-item-ctrl.cpp
, containing most of the functions discussed below. The entirety of the changes are spread across a total of 40 different files.
Classes and structs
The primary class I worked with is CanvasItemCtrl
, complemented by three new structs created to support this project. The first of these is a template struct called Property
, designed to store and set properties. The second, HandleStyle
, brings these properties together and contains functions for extracting RGBA color values from RGB colors and their overlapping opacity for fill, stroke, and outlines. The third structure, Handle
, is intended to be linked with each instance of CanvasItemCtrl
through a member named _handle
. This struct combines handle type and state (selected, hover, click, and their combinations). The state and type settings of the _handle
member within the controls replace the earlier hard-coded style settings, as the _handle
now completely defines the control's appearance.
Parsing the stylesheet
The parsing of CSS is primarily done through Libcroco, with specific requirements addressed through custom code layered on top of Libcroco functionality. CSS files are parsed using a series of functions: set_selectors()
, set_properties()
, and clear_selectors()
. set_selectors()
assigns the selector using a helper function called configure_selector()
, based on the selector's type and pseudo classes. These selectors, represented as Handle
instances, are then checked against all handles, and those matching the selector's definition are added to a vector of selected handles. This vector is then iterated over during set_properties()
, and the selector's specificity is used to set the property. Selectors with higher specificity override those with lesser specificity. clear_selectors()
then simply clears this vector. This parsing occurs when _render
is first called during an Inkscape launch, triggered by the initial drawing of a handle on the canvas.
Rendering and caching
After parsing, styles are defined for every possible type and state configuration. When a handle needs to be drawn, it utilizes these style descriptions from the corresponding HandleStyle
instance to construct the handle using draw_shape()
. The pointer to the rendered shape is then saved in a common cache shared among all handles (handle_cache
), preventing redundant rendering of the same shape, and effectively creating a two-level cache. Whenever a handle needs to be rendered and its cache (_cache
) requires reconstruction, it first checks whether the handle corresponding to _handle
exists in the shared cache. If not found, the system proceeds to render the shape using draw_shape()
and subsequently stores it in the shared cache.
The draw_shape()
function is mostly borrowed from the earlier build_cache()
function, with the addition of variable stroke width and the introduction of outlines for all shapes. Some shapes are rendered pixel by pixel onto a flattened array, while others are initially drawn using Cairo and then copied to the array. Both approaches required different implementations for outlines. The former involved adding conditions according to the shape for outlines in addition to stroke and fill, while for the latter I used Cairo to mask the outside of handles and draw the outline with an adjusted width. Notably, all shapes are drawn entirely within their width, with the outline extending beyond the normal shape (but still within the width), and the stroke overlaying the fill within the normal shape. The ability to set and render styles in a hard-coded manner still exists and is employed by the guideline handles, which differ substantially from traditional handles and are already customizable through the UI.
The _render
function simply positions the drawn handle on the canvas based on opacity, operating in the normal mode, CANVAS_ITEM_CTRL_MODE_NORMAL
. Although older XOR modes remain in the code, they are no longer utilized.
Integrating different tools
The final step involved replacing the existing method of handling handle styling with the new approach of state and type-setting, leaving all styling to the CSS. This included modifying dozens of files wherever instances of CanvasItemCtrl
was utilized. This was one of the most time-consuming aspects of the project, as discussed in the following section. However, the majority of the changes involved replacing occurrences of set_shape()
, set_fill()
, set_stroke()
with set_selected()
, set_hover()
, set_type()
, and so forth. This covered all the different tools like gradient, mesh (which required a new type to be declared for it), pages tool, LPEs adjusting in accordance to the specific needs for each.
Challenges faced during the project
What I really appreciated about this project was the variety of hurdles it threw my way. Ranging from spending weeks trying to figure out how to use Libcroco properly, to plotting 20-30 lines on a calculator to figure out the equations needed for drawing a perfect '×' onto the canvas, it had a bit of everything. I actually enjoyed facing these challenges, as they provided me the opportunity to put my skills and knowledge to the test.
The most problematic thing in the earlier half of the project was working with Libcroco. Being written in C and not maintained for a long time now, I had only about a couple of code examples to decipher how to fit it for my project's unique requirements, to parse a CSS file (with custom pseudo selectors and custom classes) and set the properties for the handles according to a precedence which was again mostly designed specifically for this project. Often, I found myself digging through the library's source code, which was mostly uncommented and undocumented, searching for anything that could help me. Eventually, it worked out, and once it did, it didn't give me many issues afterward; it operated smoothly as expected for the most part.
As the project neared its end, my biggest task was handling the parts of the code that my project affected. Understanding what certain functions and handles did and integrating the new methods to deal with them was the main challenge. Sometimes, I had to create entirely new approaches to make certain handles work correctly. The uncertainty of whether my changes were the right ones and how they'd affect other parts of the project slowed me down. I often reached out for help in our chat to make sure I was on the right track and just waited for an assurance. It was a bit tedious, but the community's input was invaluable in getting things done correctly and efficiently.
There were some smaller challenges along the way, like ensuring synchronization to access shared variables between threads running simultaneously, handling memory efficiently, and other tasks like figuring out how color transparency overlap works and drawing shapes with their outlines using Cairo or manually pixel by pixel based on their styling description. These challenges, while sometimes frustrating, allowed me to put what I'd learned into practice, and I'm glad I was able to overcome them. Of course, there were numerous other challenges I faced throughout this journey, beyond what I can recall at the moment.
Future plans and possibilities
Though I find the project completed from my end, there are a few remaining tasks to address. This includes resolving any conflicts that might arise with the master
branch and addressing anything that could potentially block the merging process. I'm also committed to fixing any bugs that may emerge during testing or making adjustments if there are specific expectations that differ from the current implementation. It's crucial that everyone is on the same page when it comes to something as directly visible to the user as this. We can progressively introduce additional shapes to offer users a broader range of options, addressing specific needs as they arise. Also, would like to take this opportunity to invite feedback and reviews of the features and their proper functioning. So if you find any issues or anything that should've been done differently, please reach out through Inkscape Chat or elsewhere.
Looking beyond this project, since much of the framework is already in place, we can extend the same system of setting UI element styles through CSS to other elements on the canvas if it proves relevant. This could include elements like guides and page borders but again most of it is subject to actually being needed. Also, this can be extended to implement theming capabilities including canvas controls.
In terms of my personal plans, I'm dedicated to maintaining the code I've written and keeping it up-to-date to meet any changing requirements. Moreover, having explored a substantial portion of the codebase, I'd look out to address any issues that pertain to the specific parts of the codebase that I've worked on throughout this project. I'm also open to contributing to other repositories where I can apply the knowledge and skills I've gained. In short, I remain committed to actively contributing to Inkscape through code or otherwise.
A special thanks to my mentor, Marc and all the community members (Rafał, Mike, Tav, PBS, Adam, Daniel, Jabier and literally everyone else) who helped me throughout the project, eventually making it successful.
Thanks for reading!