This is a line

    Developing a Beautiful and Performant Block Editor in Qt C++ and QML

    How I developed Daino Notes block editor from scratch - a cross-platform and native-like application

    Computers are fast, but modern software - especially web apps, are so bloated that they hold their full potential back. This is why I've decided to build my own block editor from scratch using Qt C++ and QML.



    Native/Non-Native

    When people refer to native apps, they typically mean applications developed using the GUI frameworks provided by the operating system. However, using native frameworks isn't always the best choice. For instance, Apple's SwiftUI is reportedly slow[1][2][3][4], and Microsoft tends to abandon each new UI framework every five years[5][6].

    So, it's more useful to ask: what do we expect from good native apps? We expect them to:

    1. Look visually appealing and consistent with the rest of the OS.
    2. Behave consistently.
    3. Perform well.

    I will attempt to convince you that it's possible to achieve all three of these qualities using Qt, at least to some extent. In the Qt community, we refer to our applications as 'native-like' because they can perform and look like native apps, even though they aren't built with native frameworks.

    *While Qt apps typically don't look or behave exactly like native apps, I'm going to argue that they can.



    Block Editor

    The previous version of Daino Notes[7] was a simple note-taking app built using Qt Widgets. It featured a familiar three-pane design. The text editor was a basic plain text editor[8] with Markdown syntax highlighting[9]. It was simple and worked fairly well.

    This is a line Old

    But for a long time, I wanted something more. Daino Notes has been featured as one of the top results on Google searches for the keyword 'notes,' so many of its users are not technical. They aren't concerned with Markdown; they simply want standard formatting options and an app that just works. They want a WYSIWYG (What You See Is What You Get) experience. I wanted that, too.

    At the same time, I've grown fond of Markdown. The idea that all my notes are formatted in a syntax that will essentially last as long as computers exist—plain text—is very reassuring. I don't want to change that, so Markdown is here to stay.

    Around that time, I became interested in new types of editors that were gaining popularity, particularly those popularized by Notion. Notion's block editor is a brilliant concept—each piece of content, whether it's text, an image, a to-do list, or a complex view like Kanban, is treated as an individual block. This allows for great flexibility in organizing and manipulating content, such as dragging and dropping different types of blocks. Most importantly, it enables the integration of complex block types like Kanban and Timeline views within the same document. For instance, the Daino Notes Block Editor is so versatile that a block in the middle of a document could easily be a video game, if I so desired.

    The main problems with Notion are that:

    1. It's a resource hog, with high CPU and RAM utilization, leading to battery drain and inefficiency.
    2. It's too complex.

    Notion is a resource hog because it's built on multiple layers of abstraction, like many other web apps, preventing optimal utilization of computer resources. Although there are faster web-based alternatives, even the fastest one (MarkText) is 60 times slower than Daino Notes and uses six times more RAM (when it doesn't hang). I'll explore performance comparisons with other apps later.

    The complexity of Notion might be both its strength and its weakness. In Notion, you can create databases that hold data in a table-like structure, allowing you to organize a list of tasks for your project under different categories. You can then use the same database across different pages in your workspace to visualize the data in various ways. This way, you can transform a table-like dataset into a Kanban view, Timeline view, Calendar, and more.

    This is a line

    I don't believe the complexity lies inherently in the use of databases but rather in the overwhelming number of options and variations available to users. Too many options can lead to stress, and stress can cause procrastination. I want to liberate users from the stress of excessive choices. Daino Notes' solution is to eliminate the need for users to think about databases altogether. Want to create a Task Board (i.e., Kanban)? Just insert a Task Board. There's no need to worry about linking 'data sources' from different databases. It's an opinionated design, but the core goal of Daino Notes is to let users focus on their content[10].

    So there I was, aiming to create a note-taking app where:

    1.The underlying data is simple plain text (with Markdown syntax)
    2.That editor is a flexible block editor.
    3.The app is simple and intuitive, even for the most non-technical users.

    In the next sections, I'll show you how I built it.



    Architecture

    I briefly mentioned Qt Widgets, which represent the classical approach to creating GUI apps imperatively using Qt in C++. While powerful, Qt Widgets lack some essential modern features, in my opinion, such as declarative UI, bindings, behaviors, anchors, and more. These features enable the creation of beautiful, animated UIs simply and quickly, as seen in QML.


    Note: I won't elaborate on how Qt/C++/QML works but will provide an overview of the implementations.


    I've been experimenting for a while with ways to incorporate active widgets—ranging from a simple checkbox for a to-do list item to an image or a Kanban board—inside QTextEdit, which is part of Qt Widgets. However, everything I tried proved cumbersome or too complex for my skill set. Enter QML. Although relatively new compared to Qt Widgets, QML is starting to mature. It's the new approach to creating modern UI interfaces in Qt. It's possible (and recommended) to write the logic in a compiled language like C++, which is officially supported. There are also numerous bindings to other languages, such as Rust, for this purpose. The view/UI is then written in QML, and that was my approach.

    After experimenting and studying QML, I realized that my block editor could be a simple ListView where each delegate (the type of item that makes up a ListView) is a Block type. I could then load different components based on the current block type (e.g., Regular Text, Todo item, Kanban, Image, etc.). While this approach is feasible with Qt Widgets, I find it much simpler and better to use QML due to the modern features I described earlier.

    Qt follows the MVC (Model-View-Controller) architecture. While it might seem complicated at first, it's actually simple, straightforward, and logical. Let's take my block editor as an example. Here's a quick overview of the Daino Notes architecture:

    Data → This is the content you want to display. In the case of Daino Notes, it's the content of a note stored as a plain text string in a local SQLite database.
    Model (QAbstractListModel) → This is the code that manages the underlying structure of the document. It takes the data and organizes it in a way that the view can understand. I named mine BlockModel. It is written in Qt C++ and communicates with the view, which is written in QML.
    View (QML ListView) → This is where the code for the rendered components resides—the elements the user actually sees, such as checkboxes, text, images, etc. The view receives its structured data from the model. It is written in QML and uses Qt Quick, a library of components provided by Qt and accessible via QML.

    Within the BlockModel each item is a QObject of the Block class type. A Block can hold simple information such as blockType (e.g. Todo, Heading, Kanban, Image, etc., using C++ enums), indentationLevel, text, and more. It also contains pointers to other objects that represent more complex blocks, such as Kanban* and Image*. These objects are initially NULL and are only created if the block is of a type that requires them (e.g., Kanban or Image).

    The fact that each item in the ListView is the same Block component (which loads different types of components), gives me the flexibility to put very complex and different components within the same document, to drag and drop between them, convert one type into another, etc.

    The first thing I did after coming up with this idea was to test whether I could implement text selection across different blocks. I knew that successfully achieving this would enable me to build the entire editor from scratch. That was my proof-of-concept. After trying various approaches, I found an excellent blog post[11] by Shantanu Tushar along with his source code[12] which laid the groundwork for my proof of concept.

    The main idea behind Shantanu Tushar's code for text selection between different blocks (or discrete delegates in a ListView) is to:

    1. Instruct all the currently visible delegates to check if they should select their text by verifying whether the delegate's index falls within selectionStartIndex and selectionEndIndex,
    2. when a signal selectionChanged() is called,
    3. each time the cursor position changes during a mouse press-and-move event.

    It worked very well. I sent a Pull Request[13] with some improvements including: backward selection, word/line selection, smooth accelerated scrolling, and more, and then continued the development.



    Basics of A Text Editor

    There are many interactions I took for granted when using a text editor. For example, consider the simple action of moving the cursor up or down. In most text editors, the x position of the cursor at the start of the operation is saved, and the editor tries to maintain that x position as you navigate.



    I discovered that I needed to implement many operations I had taken for granted, such as cursor movements (up/down, left/right), copy and paste, undo and redo, and one of the most challenging tasks—displaying the raw Markdown when the cursor is inside a Markdown-formatted text—from scratch.



    Undo & Redo

    When implementing the undo/redo stack, I opted for the simplest solution: using simple structs to hold the information for each operation. For each operation (a singleAction struct), I save both the old underlying plain text and the new one. This approach might seem redundant, as a diff algorithm could be more efficient, but I chose to stick with simplicity.




    Another surprising aspect I hadn't considered before is the merging of single operations (or SingleActions). For example, when you type some characters in a text editor and then press 'undo,' you expect all the characters you just inserted to be removed at once. What happens in the background is that each OneCharOperation of a SingleAction (such as inserting or removing a character) is merged into a single CompoundAction. This way, if a user types a long text and then presses undo/redo, the user doesn't have to wait for each individual character operation to undo/redo.

    There are many other examples of merging single operations in Daino Notes. For instance, when you select multiple blocks and indent them, each block is indented separately, but all indent operations are merged into one CompoundAction. This means that when the user presses undo, all these blocks will unindent together. Another example is moving a task from one place in the Task Board (or Kanban) to another by dragging and dropping. In the background, this involves two different single operations merged into one: first, removing the current task, and second, inserting it in the desired location.

    When dealing with advanced blocks like Kanban boards and images, I initially implemented their own undo/redo stacks as private members within their respective classes. However, I quickly realized that once these objects were deleted, their undo/redo stacks were deleted along with them. The solution was to store all advanced blocks' undo/redo stacks within the BlockModel. Here's how it looks inside the class:




    Each advanced block object class defines its own SingleAction and all the other data necessary for undoing/redoing its operations. Below are the undo/redo structs for the Kanban and Image:






    Markdown Under Cursor

    One of my primary goals was to make Daino Notes accessible to even the most non-technical users by creating a WYSIWYG (What You See Is What You Get) editor—similar to how a Word document displays content as it will appear when printed. If a user bolds a word in Daino Notes, the word is displayed in bold.

    In Markdown format (**like this**), the choice of how to display or render the text is left to the editor. Some editors do nothing, some render the word in bold but keep the asterisks, and others, like Daino Notes, hide the asterisks and display the word in bold. However, whenever the cursor is inside the formatted text, the Markdown syntax becomes visible. This approach bridges the gap between WYSIWYG (what the user sees) and showing the underlying Markdown-formatted text.

    Getting this right—rendering the underlying Markdown when the cursor is inside a Markdown-formatted text—was quite challenging. For example, if the cursor is inside this bold and italicized text it will show as ***bold and italicized text***. Once the cursor moves outside of it, the text will render normally.



    The main problem was determining the underlying Markdown from the cursor's position in the rendered text. I scoured the web for implementation ideas, but found nothing useful for Qt and QML, so I devised my own.

    Obtaining the underlying plain text from the rendered text at the cursor position is crucial for almost any interaction in the editor. Consider this example: you have the text "The quick brown fox jumps over the lazy dog" and you want to select and copy exactly "ick brown fox jump" from the text, which corresponds to the underlying Markdown text: ick brown_** fox ***jump. How do we achieve that? Let's begin with what we know

    1. We know that the entire underlying Markdown text is: The **_quick brown_** fox ***jumps*** over the lazy dog
    2. We know that the selection starts at position 6 and ends at position 23, in the rendered text.



    This is a lineThis is a line

    String Manipulator: A small program I developed to visualize strings

    However, since each piece of text in the editor is a QML TextArea with the property textFormat: TextArea.RichText, which is represented in HTML, it's not straightforward to determine the cursor or selection positions in the underlying Markdown. This is because we're dealing with rendered text. After much experimentation, the most obvious solution emerged:

    1. Obtain the HTML of the entire TextArea and convert it to Markdown.
    2. Extract the HTML from position 0 to the desired position (start/end selection) using text.getFormattedText(0, selectionStartPos) and convert it to Markdown.
    3. Identify the longest common prefix between the two Markdown strings.
    4. This process gives us the underlying Markdown from position 0 to selectionStartPos. With this, we can determine the starting position of the selection in the rendered text. We can then repeat the process for selectionEndPos if we are selecting text.

    However, there were some issues. Using QTextDocument to convert the HTML of the TextArea didn't work well due to a bug in its .toMarkdown() function, which doesn't correctly close Markdown characters. So, could we simply use a different library to convert HTML to Markdown? Not quite. It turns out that Qt uses a very unusual inline syntax for its HTML which standard HTML-to-Markdown parsers don't understand. Fortunately, the person who reported the issue also provided a solution by creating QBasicHtmlExporter , a library that converts QTextDocument's HTML into a standard HTML syntax.

    With that in place, all that was left was to use an HTML-to-Markdown parser (another great open-source tool!), and voilà, we could extract the underlying Markdown text from the rendered text positions.

    Well, there are still some more things to do. Now that we know the positions of the underlying Markdown from the rendered text we need to check if the cursor is within a Markdown syntax. If it is, we send the TextArea HTML with the part where the cursor is in plain text to display the Markdown, while keeping the rest rendered. If the cursor isn't within Markdown syntax, we simply show the rendered text. This is accomplished using some regular expressions (QRegularExpression) and some Voodoo spell.

    I try to avoid using regular expressions as much as possible, as they are often slow and inefficient (I'm not using any in any other part of the editor's code). I usually try to break down what I need, and write a custom algorithm that is much faster. However, since cursor position changes are not CPU-intensive operations, it is safe to use regular expressions here.



    Advanced Blocks

    Every block must be saved in plain text, including 'advanced blocks' like Kanban boards and images. I aimed to make these blocks as useful in plain text as they are when rendered.



    Syntax

    The syntax for an advanced block is as follows:

    {{blockType "parmater1":value,"paramater2":value}}
    {{/blockType}}

    1. It starts with an opening tag of two curly braces, followed by
    2. The block type
    3. The metadata in JSON format, followed by a closing curly brace
    4. A closing tag of the same block type

    That's it.

    Let's take the syntax of a Kanban board as an example. In plain text, it appears as normal headers and a list of to-dos, encapsulated by the minimal syntax described earlier:



    If you paste this exact plain text into Daino Notes, you'll see it rendered as a Task Board:


    Task Board

    Even in plain text, it is simple enough to read and closely resembles a regular Markdown-formatted list of to-dos.



    Drag & Drop

    One of the neatest features in Daino Notes is to be able to drag and drop any block inside the editor just like drag and drop should always been - the dragged object simply "pops" from its place and the surrounding blocks make room for it, as one would intuitively expect.





    If you remember, all blocks are contained within a ListView—a type of virtualized list that renders only what the user needs to see, optimizing memory usage. Can you guess the issue this might cause for dragging items? It means that if we attempt to drag a delegate while scrolling, the delegate (and thus the dragged item) could be destroyed because the ListView no longer needs to display it.

    One potential solution is to increase the cacheSize of the ListView while dragging an item, which instructs Qt to allocate more memory for items not currently visible. However, this approach can be memory-intensive, slow, and inefficient.

    The solution I came up with is create a copy of the block being dragged at its exact location when the user initiates the drag action. Then, display the replica, hide the original block, and drag the replica. This way, the replica can be moved anywhere without being destroyed by the ListView. No one notices it's a replica because a) it happens so quickly, and b) it's an exact replica at the exact location of the original block. As they say, magic is skillful deception—and sometimes programming is much the same.



    External Drag & Drop


    Daino Notes supports the external drag and drop of images from local files or other applications, such as browsers. Achieving this functionality the way I envisioned was not trivial. Initially, I wanted the dragged image from an external source to quickly hide the image provided by the OS and display a replica at the cursor's location, allowing control over size and effects. However, it seemed nearly impossible to remove or hide the OS-provided image using Qt in a cross-platform manner, so I opted for a different approach.

    When an image is dragged from an external source into Daino Notes, the first step—after detecting and creating a copy of the image (more on that shortly)—is to create an invisible replica block of the image. This block acts as an object that other blocks must accommodate during the drag, giving the user the impression that the OS-provided image is interacting with the application.

    In order to detect an external drag I had to jump through some hoops. Unfortunately, the QML DragEvent doesn't support retrieving image data and unlike QDropEvent, there's no direct access to QMimeData from QML DragEvent. That's annoying. Fortunately, KDE wrote their own declarative drag and drop functionality by subclassing QDropEvent and exposing it via QML and open sourced it. I had to make some changes to it - for some reason QMimeData::setData didn't set the imageData or didn't do it correctly, and I added a Q_PROPERTY image (a variable that can be exposed to QML) that was lacking. I'll publish these changes in the Daino Notes public repository where I also share common files with my previous open-source note-taking app, Notes (FOSS), to comply with its MPL license.


    NOTE: Currently, Daino Notes doesn't support 'natural' drag and drop, like that provided by react-beautiful-dnd. Implementing this requires two hotspots for collision detection, which is not straightforward in QML. However, it's definitely on my to-do list.



    Performance


    War and Peace Workout

    To test the performance of Daino Notes as a text editor, I adapted the Moby Dick Workout, a test invented by Jesse Grosjean, with slight modifications.

    The test is straightforward: a text editor should load the text of Moby Dick quickly, remain fully responsive when resizing the window (testing its word-wrap performance), use a low amount of RAM, scroll quickly to a distant point in the text, perform multiple operations (like editing in the middle of the text, undo/redo, etc.) efficiently, and maintain a small binary size (footprint).

    Moby Dick contains 208,000 words. To raise the bar, I used the text of Leo Tolstoy's War and Peace, which contains 561,693 words—2.7 times more.

    The following are the results comparing multiple block editors (custom editors that can hold arbitrary objects as blocks, support drag and drop between them, and convert them to different types).

    I believe that any good software should be able run fast even on old hardware. So I tested all apps both on 2017 MacBook Air equipped with a 1.8 GHz Dual-Core Intel Core i5 processor with 8GB RAM and a much more powerful Macbook Air M1 2020 with 8GB RAM.

    Results for 2017 MacBook Air with a 1.8 GHz Dual-Core Intel Core i5 loading the text of War and Peace:



    AppVersionLoading Time (s)Memory use after loadScroll jumpResizeMulti OperationsEditingMemory use second timeBinary sizeCross-platform
    Daino Notes iconDaino Notes (Qt)3.1.10.33183.1 MBFastFastFastFast229.2 mb89.55 mbYes
    Bike's iconBike (Native)1.18.50.8589.2FastFastFastFast118.5 mb30.4 mbNo
    AppFlowy's iconAppFlowy (Flutter)0.7.12.20*346 MBHangsHangsHangsHangsOut Of Memory179.5 mbYes
    Apple Notes' iconApple Notes (Native)4.93.7195.8 MBFastSlowFastFast124 mb37.7 mbNo
    MarkText's iconMarkText (Electron)0.17.119.90588.4 MBAcceptableSlowHangsToo slownot completed284.1 MBYes
    Anytype's iconAnytype (Electron)0.42.828.81.2 GBGoodToo slowToo slowSlownot completed472.8 MBYes
    Bear's iconBear (Native)2.3.374111.3 MBFastHangsHangsAcceptablenot completed110.6 MBNo
    Craft's iconCraft (Native)2.8.7Limited252 MBNo
    Notion's iconNotion (Electron)3.16.0Too slow256.7 MBYes

    *AppFlowy wasn't usuable after loading the text

    The results speak for themselves—Daino Notes is by far the fastest block editor at loading large texts like War and Peace. It is also the fastest (alongside Bike) for resizing, scrolling, and editing on an "old" 2017 MacBook Air, all while being cross-platform.

    Methodology:

    1. Loading time: Fully loads the entire text and ready to scroll.
    2. Memory use after load: Memory used by the app after loading the text.
    3. Scroll jump: How quickly the app scrolls to a distant point in the text.
    4. Resize: How fast the app resizes after scrolling to the middle of the text.
    5. Multi Operations: How fast the app is at multiple operations: Select all text, cut, paste, undo, redo.
    6. Editing: How fast the app is at typing at the middle of the text.
    7. Memory use second time: Memory usage after doing all the above operations multiple times.
    8. Binary size: Binary size of the app.
    9. Cross-platform: Can the app run on Windows, Linux and macOS?

    Since the MacBook Air M1 2020 is much more powerful, I decided to test the apps on the text of War and Peace multuplied by 20. Only Daino Notes and Bike were able to load the text and remain responsive. The other apps either crashed, were too slow to be usable, or limited the amount of text that could be loaded.

    Here are the results for War and Peace—X20 on the MacBook Air M1:



    AppVersionLoading Time (s)Memory use after loadScroll jumpResizeMulti OperationsEditingMemory use second timeBinary sizeCross-platform
    Daino Notes iconDaino Notes (Qt)3.1.12.661.06 GBFastFastSlowSlow1.52 GB89.55 mbYes
    Bike's iconBike (Native)1.18.52.63437.8 MBFastFastFastFast1.13 GB27.5 mbNo

    Results for War and Peace—X4 on the MacBook Air M1:



    AppVersionLoading Time (s)Memory use after loadScroll jumpResizeMulti OperationsEditingMemory use second timeBinary sizeCross-platform
    Apple Notes' iconApple Notes (Native)4.93.7195.8 MBFastSlowFastFast124 mb37.7 mbNo
    MarkText's iconMarkText (Electron)0.17.119.90588.4 MBAcceptableSlowHangsToo slownot completed284.1 MBYes
    Anytype's iconAnytype (Electron)0.42.828.81.2 GBGoodToo slowToo slowSlownot completed472.8 MBYes

    Results for War and Peace—X1 on the MacBook Air M1:



    AppVersionLoading Time (s)Memory use after loadScroll jumpResizeMulti OperationsEditingMemory use second timeBinary sizeCross-platform
    Bear's iconBear (Native)2.3.374111.3 MBFastHangsHangsAcceptablenot completed110.6 MBNo
    AppFlowy's iconAppFlowy (Flutter)0.7.12.20*346 MBHangsHangsHangsHangsOut Of Memory179.5 mbYes
    Craft's iconCraft (Native)2.8.7Limited252 MBNo
    Notion's iconNotion (Electron)3.16.0Too slow256.7 MBYes

    As you can see, Both Daino Notes and Bike are extremely preformant at loading large text on the M1, while other apps struggled to load a fifth or even one twentieth of the same text.

    Daino Notes seems to struggle with multiple operations on War and Peace X20-specifically deletion of the large text (part of the multi operations test), and editing in the middle of the text.

    Deletion (freeing memory) of many blocks is slow, since it happens on the main thread and blocks the UI. Maybe a solution is to delete blocks on a separate thread, but initial tests showed that it's not trivial to do so.

    Editing in the middle of the text is slow because currently on each save operation the entire text is saved on disk. This is inefficient, a better aproach would be to save only the blocks that have changed.

    Memory utilization could also be improved much further. I plan to explore these optimizations in the future. Stay tuned for an updated chart.



    reuseitems

    One reason scrolling in Daino Notes remains fast, even with very large texts, is due to the use of the reuseitems property of QML ListView. The Qt documentation explains its function well:


    When an item is flicked out [moves outside the visible area of the ListView], it moves to the reuse pool, which is an internal cache of unused items. When this happens, the TableView::pooled signal is emitted to inform the item about it. Likewise, when the item is moved back from the pool [into the visible area of the ListView], the TableView::reused signal is emitted.

    This approach saves a lot of time that would otherwise be spent on deallocating and deleting objects, which can be very time-consuming for complex delegates.

    It requires some adjustments when delegates have numerous bindings, signals, or animations. However, the solution is relatively straightforward: as the documentation suggests, creating a simple property bool isPooled in the delegate to disable bindings, signals, and animations when a delegate is pooled effectively addresses the issue.

    There's much more that can be done to enhance performance, but I'll save these optimizations for another time.


    Binary Size

    The binary size (or 'footprint') of Daino Notes varies across different operating systems. On Linux (.AppImage), it's 62 MB; on Windows, it's 134 MB; and on macOS, it's 89.55 MB (with the universal binary supporting both ARM and x86, resulting in a 179.1 MB size).

    I haven't yet focused on reducing the app's binary size, as my primary emphasis has been on improving user experience, speed, and memory usage (which can also be further optimized). Some potential ways to reduce the bundle size include:

    1. Compiling Qt from source with only the specific modules needed and using static linking.
    2. Use the -optimize-size flag and Link Time Optamization flag -ltcg.
    3. Running strip on the resulting executable to remove unused symbols.
    4. Use UPX to compress the executable.

    I plan to explore these options in the future.



    Aesthetics

    Frank Sinatra famously said, 'If you can make it there [in New York], you can make it anywhere.' The equivalent for GUI apps is macOS—if you can succeed there, you can succeed anywhere. Native macOS apps set the standard for geat GUI apps - in terms of design and user experience.

    Therefore, when designing and implementing Daino Notes, my benchmark has always been the great native macOS note-taking apps and text editors, such as Apple Notes, Bike, Bear, Ulysses, and others.

    Selecting multiple notes looks and behaves similarly to the way it does in Apple's macOS Mail app



    Lowest common denominator

    It's true that when using cross-platform frameworks, you often end up with the lowest common denominator of features. However, Qt's thriving open-source ecosystem allows for workarounds to some extent. For example, qwindowkit enables beautiful, frameless windows across macOS, Windows, and Linux (I'm planning on integrating it into Daino Notes for Windows and Linux soon), while QSimpleUpdater is excellent for implementing cross-platform automatic updates. These are just some of the tools I've used in my projects, and there are many more great open-source libraries that help bridge the gap between OS-specific behaviors and Qt.

    One obvious area where Daino Notes' non-nativeity is noticeable is the scrolling behavior of ListViews throughout the app. They don't quite match the smoothness of native macOS apps. I've experimented with the environment variable QT_QUICK_FLICKABLE_WHEEL_DECELERATION but it doesn't seem to have much impact. I hope to see improvements from Qt in this area.

    If any Qt developers are reading this, I would love the ability to create a QML Pane with a blurred, semi-transparent background, as seen in many native macOS apps. It's probably possible to create a new native frameless window but it's a bit overkill for a popup.

    Another request is to support borders for span elements in Rich Text, with border radius support, please.

    Indeed, if you're looking for the latest special APIs, you might need to write some platform-specific code. This is almost inevitable with any serious cross-platform project, but the open-source community significantly reduces the effort and is rapidly improving.



    Qt license


    Let's get the legal disclaimer out of the way: I am not a lawyer, and this should not be considered legal advice.

    There's a misconception that you can't statically link your app when using the open-source LGPL version of Qt. From my reading of the LGPL license this doesn't appear to be the case. The LGPL allows you to statically link your app as long as you provide the object files and allow users to relink your app with a different version of Qt.

    I've observed many people spreading this misinformation about only being able to dynamically link with the LGPL version of Qt.



    Bugs, Bugs Everywhere

    One of the most frustrating aspects of developing a Qt application is the slew of Qt bugs you encounter along the way. During ten months of development, I reported seven bugs, three of which were assigned 'critical' priority—two of which resulted in crashes. I also came across many bugs already reported by others that remain unfixed.

    I do want to credit the Qt developers—they have been very attentive to all my bug reports and have already fixed two of the critical issues. However, I wish they would focus more on fixing bugs rather than implementing new features, a sentiment I share with other developers as well[14].

    Here's the list of bug reports I filed:

    QTBUG-122658
    QTBUG-123133
    QTBUG-124572
    QTBUG-124814
    QTBUG-126565
    QTBUG-126730
    QTBUG-127134

    The issue with these bugs, and the reason The Qt Company should prioritize fixing them, is that it can take considerable time to find (often hacky) solutions to work around these bugs and to reproduce and report them adequately. I hope to see this getting better in the future.



    Build Process

    Deploying a cross-platform app is difficult—not only must it behave well on different operating systems, but it also needs to run on all of them. This involves creating a package or binary that each operating system can execute, which can be a very labor-intensive process if not properly automated.

    Fortunately, some amazing contributors to my open-source note-taking app developed a robust build and deployment system for all major operating systems. Now, when we push a new release tag to GitHub, the build process automatically starts, and by the end, we have packages ready for macOS, Windows, and various flavors of Linux. Before this system, I had to compile and package each new release separately on each operating system.

    Our workflow is completely open source, so if you're developing a Qt app, you can copy the GitHub Actions configuration files and deploy your Qt app on all major operating systems with a single click.

    Here are some thoughts from @guihkx, who worked on it almost exclusively:

    Prior to working on Notes, I had never worked with building Qt apps, so learning while doing it was quite fun!

    I began working on the Linux part of the CI/CD first, which is the OS I'm most comfortable with. Linux has a lot of packaging options, but to not overcomplicate things, at first I decided to just use AppImage, which is a format that's expected to "just work" on almost any Linux distribution out there.

    Unfortunately, though, there's no official Qt tooling to create AppImages, but no worries, the open source community has our back: By combining linuxdeploy with its Qt plugin, I was able to create an AppImage package with very little effort.

    I also had to decide how I would set up Qt on Linux, in order to build Notes and also include it in the AppImage. I could technically just install Qt from Ubuntu's official repository and use that, but I wanted to have complete control over the Qt version we were going to ship with Notes. Ideally, I wanted to use the same Qt version across all operating systems, so another approach had to be used...

    And that's when I came across the amazing world of extensions for GitHub Actions, which we can use to simplify our CI/CD workflow. On the marketplace, I found this amazing extension called "Install Qt" extension, which was perfect for our use-case: Easy to use, highly-configurable, and cross-platform!

    After getting the Linux part done, I began working on the macOS and Windows builds. I was not familiar at all with building and packaging on these two OSes, but the official tooling provided by Qt made things so much easier: On Windows, I used windeployqt, and on macOS, I used macdeployqt.

    Overall, I'd say the tools we have to build Qt apps with GitHub Actions are quite nice and easy to use.


    What's Next

    The next major milestone for Daino Notes is to release a mobile version of the app with built-in sync. I'm eager to explore the development experience using Qt for mobile. One aspect I'm particularly interested in is creating an 'adaptive' UI, allowing a single code base to switch smoothly and seamlessly between desktop and mobile layouts.

    At Daino, our mission is to help people actualize their potential. We create tools for the mind, allowing people to transform their potential into wonderful creations. These tools should stay out of your way so you can achieve your goals. That's why we believe technology should be so intuitive that it becomes invisible—therefore, software should be simple, fast, and responsive.

    We aim to build software that squizzes every little bit of the hardware it runs on. Software that doesn't lock users in by proprietary data formats, and software that is simple and delightful to use, even for the most non-tenchical users.

    Daino Notes is just the beginning of this journey. There's much more to come.



    Licensing the Block Editor Code

    Since the release of Daino Notes, we've received numerous requests from developers and companies interested in our block editor technology. In response to this demand, we're excited to announce that we're now offering the block editor code for licensing.

    This decision allows other developers to leverage the speed, efficiency, and flexibility of our block editor in their own applications. Whether you're building a document editor, a chat app, or any other software that could benefit from a powerful block-based interface, our technology might just be what you need.

    For inquiries about licensing the block editor code, please contact us.







    Footnotes

    1. https://notes.alinpanaitiu.com/SwiftUI%20is%20convenient,%20but%20slow

    2. https://x.com/daniel_nguyenx/status/1734495508746702936

    3. https://wadetregaskis.com/swifts-native-clocks-are-very-inefficient/

    4. https://danielchasehooper.com/posts/why-swift-is-slow/

    5. https://irrlicht3d.org/index.php?t=1626

    6. https://www.reillywood.com/blog/windows-ui-frameworks/

    7. The previous version of Daino Notes, called Notes is FOSS (free and open-source software) available at https://www.notes-foss.com/ and the source code is available at https://github.com/nuttyartist/notes. I decided to make Daino Notes closed source due to difficulties in monetizing FOSS. In order to comply with Notes' MPL license, all common files between Notes and Daino Notes are published in https://github.com/nuttyartist/daino-notes-public

    8. https://doc.qt.io/qt-6/qtextedit.html

    9. https://github.com/pbek/qmarkdowntextedit

    10. In the future, I can see how I'm easily able to turn the underlying plaintext of complex blocks (such as Kanban) to other type of complex blocks. For example, if the start and due dates are added to the Kanban syntax like so:



    I could easily allow users to switch between a Kanban view and a Timeline view. This is still much simpler than Notion as the user doesn't need to deal with connecting external data sources. But that's still due to an opionanted design choice, not due to a techincal one.

    11. https://www.kdab.com/handling-a-lot-of-text-in-qml/

    12. https://github.com/shaan7/qtdevcon2022-textarea

    13. https://github.com/shaan7/qtdevcon2022-textarea/pull/1

    14. https://camg.me/qt-mobile-2023/#keyboard-issues