├── .github └── workflows │ └── c-cpp.yml ├── COPYING ├── COPYING.LGPL-2.1 ├── COPYING.MPL-2.0 ├── README.md ├── TODO.txt ├── com.mattjakeman.TextEngine.Demo.json ├── demo ├── demo.c ├── demo.html ├── meson.build ├── resources.gresource.xml ├── screenshot.png └── style.css ├── meson.build ├── src ├── editor │ ├── editor.c │ ├── editor.h │ └── meson.build ├── format │ ├── import-html.c │ ├── import.h │ └── meson.build ├── layout │ ├── README.md │ ├── layout.c │ ├── layout.h │ ├── layoutblock.c │ ├── layoutblock.h │ ├── layoutbox-impl.h │ ├── layoutbox.c │ ├── layoutbox.h │ ├── layoutinline.c │ ├── layoutinline.h │ ├── meson.build │ └── types.h ├── meson.build ├── model │ ├── README.md │ ├── block.c │ ├── block.h │ ├── document.c │ ├── document.h │ ├── fragment.c │ ├── fragment.h │ ├── frame.c │ ├── frame.h │ ├── image.c │ ├── image.h │ ├── item.c │ ├── item.h │ ├── mark.c │ ├── mark.h │ ├── meson.build │ ├── opaque.c │ ├── opaque.h │ ├── paragraph.c │ ├── paragraph.h │ ├── run.c │ └── run.h ├── text-engine-version.h.in ├── text-engine.c ├── text-engine.h ├── tree │ ├── README.md │ ├── meson.build │ ├── node.c │ └── node.h └── ui │ ├── display.c │ ├── display.h │ ├── inspector.c │ ├── inspector.h │ ├── meson.build │ ├── resources.gresource.xml │ └── style.css └── test ├── delete.c ├── insert.c ├── mark.c ├── meson.build ├── move.c ├── replace.c └── split.c /.github/workflows/c-cpp.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | pull_request: 5 | name: CI 6 | jobs: 7 | flatpak: 8 | name: "Flatpak" 9 | runs-on: ubuntu-latest 10 | container: 11 | image: bilelmoussaoui/flatpak-github-actions:gnome-nightly 12 | options: --privileged 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4 16 | with: 17 | bundle: bluetype.flatpak 18 | manifest-path: com.mattjakeman.TextEngine.Demo.json 19 | cache-key: flatpak-builder-${{ github.sha }} 20 | run-tests: true 21 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Text Engine 2 | 3 | Copyright 2022 Matthew Jakeman 4 | 5 | This library and all accompanying materials are made available under the 6 | terms of the Mozilla Public License 2.0, or the Lesser General Public 7 | License 2.1 (or any later version), at your option. 8 | 9 | You should have received a copy of the GNU Lesser General Public 10 | License along with this program. If not, see . 11 | 12 | If a copy of the MPL was not distributed with this file, You can obtain 13 | one at http://mozilla.org/MPL/2.0/. 14 | 15 | SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 16 | -------------------------------------------------------------------------------- /COPYING.MPL-2.0: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text Engine 2 | Text Engine is a rich-text editing framework for GTK 4. The primary user 3 | of this library is [bluetype](https://github.com/mjakeman/bluetype) but it 4 | can be used wherever rich text display and editing is needed. 5 | 6 | ![The text engine demo displaying sample rich text](demo/screenshot.png) 7 | 8 | ## Status 9 | The library is under heavy development and generally not suitable for 10 | use in applications. For packagers, note that Extension Manager builds 11 | against version 0.1 of this library. 12 | 13 | ## Matrix 14 | Development of text-engine and bluetype takes place on matrix. 15 | 16 | Join [#bluetype](https://matrix.to/#/#bluetype:matrix.org) to chat 17 | about the project or if you would like to get involved. Come say hello! 18 | 19 | ## Licence 20 | Text Engine is dual-licensed under the Mozilla Public License 2.0 and 21 | the GNU Lesser General Public License 2.1 (or any later version), at your 22 | option. You may choose to use the library under either of the aforementioned 23 | licenses, or both. See `COPYING` for more information. 24 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Immediate TODO list for text-engine: 2 | - Add basic input/editing capabilities 3 | - Keyboard shortcuts (Ctrl) 4 | - Unicode 5 | - Add style module 6 | - Line spacing 7 | - Bold, italic, underline 8 | - Colours 9 | - Cascades 10 | - Styles 11 | - CSS Importer? 12 | - Rich text content 13 | - Simple equations 14 | - Tables 15 | - Images 16 | - Rendering 17 | - Invalidation 18 | - Partial updates 19 | - Caching 20 | - Undo/redo system 21 | - Piece table data structure? 22 | - Operational transform? 23 | -------------------------------------------------------------------------------- /com.mattjakeman.TextEngine.Demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id" : "com.mattjakeman.TextEngine.Demo", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "46", 5 | "sdk" : "org.gnome.Sdk", 6 | "command" : "text-engine-demo", 7 | "finish-args" : [ 8 | "--share=network", 9 | "--share=ipc", 10 | "--socket=fallback-x11", 11 | "--device=dri", 12 | "--socket=wayland" 13 | ], 14 | "cleanup" : [ 15 | "/include", 16 | "/lib/pkgconfig", 17 | "/man", 18 | "/share/doc", 19 | "/share/gtk-doc", 20 | "/share/man", 21 | "/share/pkgconfig", 22 | "*.la", 23 | "*.a" 24 | ], 25 | "modules" : [ 26 | { 27 | "name" : "text-engine", 28 | "builddir" : true, 29 | "buildsystem" : "meson", 30 | "sources" : [ 31 | { 32 | "type" : "dir", 33 | "path" : "./" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /demo/demo.c: -------------------------------------------------------------------------------- 1 | /* demo.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to 7 | * deal in the Software without restriction, including without limitation the 8 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | * sell copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | * 23 | * SPDX-License-Identifier: MIT 24 | */ 25 | 26 | #include 27 | 28 | #include 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | #define DEMO_TYPE_WINDOW demo_window_get_type () 35 | G_DECLARE_FINAL_TYPE (DemoWindow, demo_window, DEMO, WINDOW, AdwApplicationWindow) 36 | 37 | struct _DemoWindow 38 | { 39 | AdwApplicationWindow parent_instance; 40 | }; 41 | 42 | G_DEFINE_FINAL_TYPE (DemoWindow, demo_window, ADW_TYPE_APPLICATION_WINDOW) 43 | 44 | enum { 45 | PROP_0, 46 | N_PROPS 47 | }; 48 | 49 | static GParamSpec *properties [N_PROPS]; 50 | 51 | static void 52 | demo_window_finalize (GObject *object) 53 | { 54 | DemoWindow *self = (DemoWindow *)object; 55 | 56 | G_OBJECT_CLASS (demo_window_parent_class)->finalize (object); 57 | } 58 | 59 | static void 60 | demo_window_get_property (GObject *object, 61 | guint prop_id, 62 | GValue *value, 63 | GParamSpec *pspec) 64 | { 65 | DemoWindow *self = DEMO_WINDOW (object); 66 | 67 | switch (prop_id) 68 | { 69 | default: 70 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 71 | } 72 | } 73 | 74 | static void 75 | demo_window_set_property (GObject *object, 76 | guint prop_id, 77 | const GValue *value, 78 | GParamSpec *pspec) 79 | { 80 | DemoWindow *self = DEMO_WINDOW (object); 81 | 82 | switch (prop_id) 83 | { 84 | default: 85 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 86 | } 87 | } 88 | 89 | static void 90 | demo_window_class_init (DemoWindowClass *klass) 91 | { 92 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 93 | 94 | object_class->finalize = demo_window_finalize; 95 | object_class->get_property = demo_window_get_property; 96 | object_class->set_property = demo_window_set_property; 97 | } 98 | 99 | static void 100 | demo_window_init (DemoWindow *self) 101 | { 102 | TextFrame *frame; 103 | TextDisplay *display; 104 | TextDocument *document; 105 | gchar *contents; 106 | gsize contents_length; 107 | 108 | GtkWidget *header_bar; 109 | GtkWidget *toolbar_view; 110 | GtkWidget *inspector_btn; 111 | GtkWidget *scroll_area; 112 | 113 | GFile *file; 114 | GError *error; 115 | 116 | error = NULL; 117 | 118 | toolbar_view = adw_toolbar_view_new (); 119 | adw_application_window_set_content (ADW_APPLICATION_WINDOW (self), toolbar_view); 120 | 121 | // Example rich text document (uses html subset) 122 | file = g_file_new_for_uri ("resource:///com/mattjakeman/TextEngine/Demo/demo.html"); 123 | 124 | if (g_file_load_contents (file, NULL, &contents, &contents_length, NULL, &error)) 125 | { 126 | GString *string; 127 | 128 | string = g_string_new_len (contents, contents_length); 129 | contents = g_string_free (string, FALSE); 130 | } 131 | else if (error) 132 | { 133 | contents = g_strdup_printf ("Unable to load demo.html content: %s\n", error->message); 134 | g_clear_pointer (&error, g_error_free); 135 | } 136 | else 137 | { 138 | contents = g_strdup ("Unable to load demo.html content."); 139 | } 140 | 141 | // test = "

There was an Old Man with a beard

Who said, "It is just as I feared!

> Two Owls and a Hen,
> Four Larks and a Wren,

Have all built their nests in my beard!"

"; 142 | // frame = format_parse_html (contents); 143 | frame = text_frame_new (); 144 | 145 | TextParagraph *paragraph = text_paragraph_new (); 146 | text_paragraph_append_fragment(paragraph, TEXT_FRAGMENT(text_run_new("Hello World. "))); 147 | text_paragraph_append_fragment(paragraph, TEXT_FRAGMENT(text_run_new("This is some text"))); 148 | text_frame_append_block (frame, TEXT_BLOCK (paragraph)); 149 | 150 | paragraph = text_paragraph_new (); 151 | // text_paragraph_append_fragment (paragraph, TEXT_FRAGMENT (text_run_new (""))); 152 | text_paragraph_append_fragment(paragraph, TEXT_FRAGMENT(text_image_new("screenshot.png"))); 153 | // text_paragraph_append_fragment (paragraph, TEXT_FRAGMENT (text_run_new (""))); 154 | text_frame_append_block (frame, TEXT_BLOCK (paragraph)); 155 | 156 | paragraph = text_paragraph_new (); 157 | text_paragraph_append_fragment(paragraph, TEXT_FRAGMENT(text_run_new("And some more text... "))); 158 | text_paragraph_append_fragment(paragraph, TEXT_FRAGMENT(text_run_new(":)"))); 159 | text_frame_append_block (frame, TEXT_BLOCK (paragraph)); 160 | 161 | document = text_document_new (); 162 | document->frame = frame; 163 | 164 | header_bar = adw_header_bar_new (); 165 | scroll_area = gtk_scrolled_window_new(); 166 | display = text_display_new (document); 167 | 168 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scroll_area), GTK_WIDGET (display)); 169 | gtk_widget_set_vexpand (scroll_area, TRUE); 170 | 171 | adw_toolbar_view_add_top_bar (ADW_TOOLBAR_VIEW (toolbar_view), header_bar); 172 | adw_toolbar_view_set_content (ADW_TOOLBAR_VIEW (toolbar_view), GTK_WIDGET (scroll_area)); 173 | 174 | inspector_btn = gtk_button_new_with_label ("Inspector"); 175 | g_signal_connect_swapped (inspector_btn, 176 | "clicked", 177 | G_CALLBACK (gtk_window_set_interactive_debugging), 178 | (gpointer) TRUE); 179 | 180 | adw_header_bar_pack_start (ADW_HEADER_BAR (header_bar), inspector_btn); 181 | } 182 | 183 | static void 184 | demo_activate (GApplication *app) 185 | { 186 | GtkWindow *window; 187 | 188 | g_assert (G_IS_APPLICATION (app)); 189 | 190 | // Initialise text-engine for inspector page 191 | text_engine_init (); 192 | 193 | // Get the current window or create one if necessary. 194 | window = gtk_application_get_active_window (GTK_APPLICATION (app)); 195 | 196 | if (window == NULL) 197 | window = g_object_new (DEMO_TYPE_WINDOW, 198 | "application", app, 199 | "default-width", 500, 200 | "default-height", 500, 201 | NULL); 202 | 203 | // Ask the window manager/compositor to present the window. 204 | gtk_window_present (window); 205 | } 206 | 207 | int 208 | main (int argc, char **argv) 209 | { 210 | AdwApplication *app; 211 | int ret; 212 | 213 | app = adw_application_new ("com.mattjakeman.TextEngine.Demo", G_APPLICATION_DEFAULT_FLAGS); 214 | 215 | g_signal_connect (app, "activate", G_CALLBACK (demo_activate), NULL); 216 | 217 | ret = g_application_run (G_APPLICATION (app), argc, argv); 218 | 219 | g_clear_object (&app); 220 | 221 | return ret; 222 | } 223 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 |

GNOME (/ɡəˈnoʊm, ˈnoʊm/), originally an acronym for GNU Network Object Model Environment, is a free and open-source desktop environment for Linux and other Unix-like operating systems. GNOME is developed by the GNOME Project, which is composed of both volunteers and paid contributors, the largest corporate contributor being Red Hat.

3 |

It is an international project that aims to develop frameworks for software development, to program end-user applications based on these frameworks, and to coordinate efforts for internationalization and localization and accessibility of that software.

4 |

athis is some alt textb

5 |

GNOME is the default desktop environment of many major Linux distributions, including Debian, Endless OS, Fedora Linux, Red Hat Enterprise Linux, SUSE Linux Enterprise, Ubuntu, and Tails; it is also the default of Solaris, a Unix operating system.

6 |

This text is sourced from Wikipedia under the CC BY-SA 3.0 license. The original document can be found here: https://en.wikipedia.org/wiki/GNOME.

7 | -------------------------------------------------------------------------------- /demo/meson.build: -------------------------------------------------------------------------------- 1 | demo_sources = [ 2 | 'demo.c' 3 | ] 4 | 5 | demo_deps = [ 6 | text_engine_dep, 7 | dependency('libadwaita-1') 8 | ] 9 | 10 | gnome = import('gnome') 11 | demo_sources += gnome.compile_resources('resources', 12 | 'resources.gresource.xml', 13 | source_dir: '.', 14 | c_name: 'resources' 15 | ) 16 | 17 | executable('text-engine-demo', demo_sources, 18 | dependencies: demo_deps, 19 | install: true, 20 | ) 21 | -------------------------------------------------------------------------------- /demo/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style.css 5 | demo.html 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjakeman/text-engine/4c26887556fd8e28211324c4058d49508eb5f557/demo/screenshot.png -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | textdisplay { 2 | padding: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('text-engine', 'c', 2 | version: '0.1.0', 3 | meson_version: '>= 0.59.0', 4 | default_options: [ 'warning_level=2', 5 | 'c_std=gnu11', 6 | ], 7 | ) 8 | 9 | 10 | config_h = configuration_data() 11 | config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) 12 | configure_file( 13 | output: 'text-engine-config.h', 14 | configuration: config_h, 15 | ) 16 | add_project_arguments([ 17 | '-I' + meson.project_build_root(), 18 | ], language: 'c') 19 | 20 | 21 | subdir('src') 22 | subdir('demo') 23 | subdir('test') 24 | -------------------------------------------------------------------------------- /src/editor/editor.h: -------------------------------------------------------------------------------- 1 | /* editor.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "../model/document.h" 17 | 18 | G_BEGIN_DECLS 19 | 20 | #define TEXT_TYPE_EDITOR (text_editor_get_type()) 21 | 22 | G_DECLARE_FINAL_TYPE (TextEditor, text_editor, TEXT, EDITOR, GObject) 23 | 24 | typedef enum 25 | { 26 | TEXT_EDITOR_CURSOR, 27 | TEXT_EDITOR_SELECTION 28 | } TextEditorMarkType; 29 | 30 | TextEditor *text_editor_new (TextDocument *document); 31 | 32 | // TODO: Refactor into TextMark 33 | void text_editor_move_mark_first (TextMark *mark); 34 | void text_editor_move_mark_last (TextMark *mark); 35 | void text_editor_move_mark_right (TextMark *mark, int amount); 36 | void text_editor_move_mark_left (TextMark *mark, int amount); 37 | 38 | void text_editor_insert_text_at_mark (TextEditor *self, TextMark *start, gchar *str); 39 | void text_editor_insert_fragment_at_mark (TextEditor *self, TextMark *start, TextFragment *fragment); 40 | void text_editor_delete_at_mark (TextEditor *self, TextMark *start, int length); 41 | void text_editor_replace_at_mark (TextEditor *self, TextMark *start, TextMark *end, gchar *text); 42 | void text_editor_split_at_mark (TextEditor *self, TextMark *mark); 43 | 44 | void text_editor_move_first (TextEditor *self, TextEditorMarkType type); 45 | void text_editor_move_last (TextEditor *self, TextEditorMarkType type); 46 | void text_editor_move_right (TextEditor *self, TextEditorMarkType type, int amount); 47 | void text_editor_move_left (TextEditor *self, TextEditorMarkType type, int amount); 48 | void text_editor_insert_text (TextEditor *self, TextEditorMarkType type, gchar *str); 49 | void text_editor_insert_fragment (TextEditor *self, TextEditorMarkType type, TextFragment *fragment); 50 | void text_editor_delete (TextEditor *self, TextEditorMarkType type, int length); 51 | void text_editor_replace (TextEditor *self, TextEditorMarkType start_type, TextEditorMarkType end_type, gchar *text); 52 | void text_editor_split (TextEditor *self, TextEditorMarkType type); 53 | 54 | TextFragment *text_editor_get_item (TextEditor *self, TextEditorMarkType type); 55 | TextFragment *text_editor_get_item_at_mark (TextEditor *self, TextMark *mark); 56 | 57 | gchar *text_editor_dump_plain_text (TextEditor *self); 58 | 59 | // Traversal Helpers 60 | // TODO: These shouldn't really be part of the editor 61 | void text_editor_sort_marks (TextMark *mark1, TextMark *mark2, TextMark **first, TextMark **last); 62 | TextParagraph *text_editor_next_paragraph (TextParagraph *paragraph); 63 | TextParagraph *text_editor_previous_paragraph (TextParagraph *paragraph); 64 | 65 | // Format Helpers 66 | // TODO: Make this more abstract 67 | void text_editor_apply_format_bold (TextEditor *self, TextMark *start, TextMark *end, gboolean is_bold); 68 | gboolean text_editor_get_format_bold_at_mark (TextEditor *self, TextMark *mark); 69 | void text_editor_apply_format_italic (TextEditor *self, TextMark *start, TextMark *end, gboolean is_italic); 70 | gboolean text_editor_get_format_italic_at_mark (TextEditor *self, TextMark *mark); 71 | void text_editor_apply_format_underline (TextEditor *self, TextMark *start, TextMark *end, gboolean is_underline); 72 | gboolean text_editor_get_format_underline_at_mark (TextEditor *self, TextMark *mark); 73 | 74 | G_END_DECLS 75 | -------------------------------------------------------------------------------- /src/editor/meson.build: -------------------------------------------------------------------------------- 1 | text_engine_sources += files([ 2 | 'editor.c' 3 | ]) 4 | 5 | editor_headers = [ 6 | 'editor.h' 7 | ] 8 | 9 | install_headers(editor_headers, subdir : header_dir / 'editor') 10 | -------------------------------------------------------------------------------- /src/format/import-html.c: -------------------------------------------------------------------------------- 1 | /* import-html.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "import.h" 13 | 14 | #include 15 | #include 16 | 17 | #include "../model/paragraph.h" 18 | #include "../model/block.h" 19 | #include "../model/run.h" 20 | #include "../model/image.h" 21 | 22 | // Style Info 23 | // TODO: Refactor this into a stylesheet module rather than setting it on runs directly 24 | static gboolean is_bold = FALSE; 25 | static gboolean is_underline = FALSE; 26 | static gboolean is_italic = FALSE; 27 | 28 | static void 29 | build_text_frame_recursive (xmlNode *nodes, 30 | TextFrame *frame, 31 | TextParagraph **current) 32 | { 33 | g_return_if_fail (TEXT_IS_FRAME (frame)); 34 | g_return_if_fail (current); 35 | 36 | xmlNode *cur_node = NULL; 37 | 38 | if (nodes == NULL) 39 | return; 40 | 41 | for (cur_node = nodes; cur_node != NULL; cur_node = cur_node->next) 42 | { 43 | // ENTER NODE 44 | if (cur_node->type == XML_ELEMENT_NODE) 45 | { 46 | if (g_str_equal (cur_node->name, "p") || 47 | g_str_equal (cur_node->name, "br")) 48 | { 49 | *current = text_paragraph_new (); 50 | text_frame_append_block (frame, TEXT_BLOCK (*current)); 51 | } 52 | if (g_str_equal (cur_node->name, "img")) 53 | { 54 | TextImage *image; 55 | xmlAttr *iter; 56 | char *img_src; 57 | 58 | *current = text_paragraph_new (); 59 | text_frame_append_block (frame, TEXT_BLOCK (*current)); 60 | 61 | img_src = NULL; 62 | 63 | for (iter = cur_node->properties; iter != NULL; iter = iter->next) 64 | { 65 | if (g_str_equal (iter->name, "src")) 66 | img_src = g_strdup (iter->name); 67 | } 68 | 69 | image = text_image_new (img_src); 70 | text_paragraph_append_fragment(*current, TEXT_FRAGMENT(image)); 71 | 72 | } 73 | else if (g_str_equal (cur_node->name, "b")) 74 | is_bold = TRUE; 75 | else if (g_str_equal (cur_node->name, "i")) 76 | is_italic = TRUE; 77 | else if (g_str_equal (cur_node->name, "u")) 78 | is_underline = TRUE; 79 | else 80 | { 81 | // Catch-all for not-yet implemented elements 82 | g_info ("Ignored element %s\n", cur_node->name); 83 | } 84 | } 85 | else if (cur_node->type == XML_TEXT_NODE) 86 | { 87 | // Append text as new run 88 | TextRun *new_run; 89 | 90 | const gchar *content = (gchar *)cur_node->content; 91 | new_run = text_run_new (content); 92 | text_run_set_style_bold (new_run, is_bold); 93 | text_run_set_style_italic (new_run, is_italic); 94 | text_run_set_style_underline (new_run, is_underline); 95 | text_paragraph_append_fragment(*current, TEXT_FRAGMENT (new_run)); 96 | } 97 | 98 | // PROCESS CHILDREN 99 | build_text_frame_recursive (cur_node->children, frame, current); 100 | 101 | // EXIT NODE 102 | if (cur_node->type == XML_ELEMENT_NODE) 103 | { 104 | if (g_str_equal (cur_node->name, "b")) 105 | is_bold = FALSE; 106 | else if (g_str_equal (cur_node->name, "i")) 107 | is_italic = FALSE; 108 | else if (g_str_equal (cur_node->name, "u")) 109 | is_underline = FALSE; 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * build_text_frame: 116 | * @nodes: Array of #xmlNode structures to parse 117 | * @frame: An initialised #TextFrame to populate with data 118 | * 119 | * Recursively builds a new #TextFrame and populates it with data 120 | * from the provided normalised HTML tree. 121 | */ 122 | static void 123 | build_text_frame (xmlNode *nodes, 124 | TextFrame *frame) 125 | { 126 | TextParagraph *current = NULL; 127 | build_text_frame_recursive (nodes, frame, ¤t); 128 | } 129 | 130 | TextFrame * 131 | format_parse_html (const gchar *html) 132 | { 133 | htmlDocPtr doc; 134 | xmlNode *root; 135 | TextFrame *frame; 136 | 137 | g_info ("%s\n", html); 138 | 139 | doc = htmlParseDoc ((const guchar *)html, "utf-8"); 140 | 141 | if (doc == NULL) 142 | { 143 | g_critical ("Could not parse HTML document."); 144 | return NULL; 145 | } 146 | 147 | root = xmlDocGetRootElement (doc); 148 | 149 | if (root == NULL) 150 | { 151 | g_warning ("Empty HTML document."); 152 | xmlFreeDoc (doc); 153 | xmlCleanupParser (); 154 | return NULL; 155 | } 156 | 157 | frame = text_frame_new (); 158 | 159 | g_info ("Root Node discovered: %s\n", root->name); 160 | 161 | build_text_frame (root, frame); 162 | 163 | xmlFreeDoc (doc); 164 | xmlCleanupParser (); 165 | 166 | return frame; 167 | } 168 | -------------------------------------------------------------------------------- /src/format/import.h: -------------------------------------------------------------------------------- 1 | /* import.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "../model/frame.h" 17 | 18 | G_BEGIN_DECLS 19 | 20 | TextFrame *format_parse_html (const gchar *html); 21 | 22 | G_END_DECLS 23 | -------------------------------------------------------------------------------- /src/format/meson.build: -------------------------------------------------------------------------------- 1 | text_engine_sources += files([ 2 | 'import-html.c', 3 | ]) 4 | 5 | format_headers = [ 6 | 'import.h', 7 | ] 8 | 9 | install_headers(format_headers, subdir : header_dir / 'format') 10 | -------------------------------------------------------------------------------- /src/layout/README.md: -------------------------------------------------------------------------------- 1 | # Layout 2 | This is the default layout engine. It is a layout policy used to transform 3 | the semantic text model into a format that can be displayed. Custom layouts 4 | can be implemented (not now, in the future) by subclassing `TextLayout` 5 | and overriding select virtual methods. 6 | 7 | The 'layout manager' takes in a 'semantic tree' and produces a 'layout tree', 8 | which is passed on the toolkit-specific drawing widget. It is responsible for 9 | caching the layout tree in between redraws and minimising the recomputation 10 | needed. 11 | 12 | The architecture of the layout tree is loosely inspired by Matt Brubeck's 13 | [excellent series](https://limpet.net/mbrubeck/2014/09/08/toy-layout-engine-5-boxes.html) 14 | on building a browser engine, itself a simplification of modern web engines. 15 | -------------------------------------------------------------------------------- /src/layout/layout.c: -------------------------------------------------------------------------------- 1 | /* layout.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "layout.h" 13 | 14 | typedef struct 15 | { 16 | int padding; 17 | } TextLayoutPrivate; 18 | 19 | G_DEFINE_TYPE_WITH_PRIVATE (TextLayout, text_layout, G_TYPE_OBJECT) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | TextLayout * 29 | text_layout_new (void) 30 | { 31 | return g_object_new (TEXT_TYPE_LAYOUT, NULL); 32 | } 33 | 34 | static void 35 | text_layout_finalize (GObject *object) 36 | { 37 | TextLayout *self = (TextLayout *)object; 38 | 39 | G_OBJECT_CLASS (text_layout_parent_class)->finalize (object); 40 | } 41 | 42 | static void 43 | text_layout_get_property (GObject *object, 44 | guint prop_id, 45 | GValue *value, 46 | GParamSpec *pspec) 47 | { 48 | TextLayout *self = TEXT_LAYOUT (object); 49 | 50 | switch (prop_id) 51 | { 52 | default: 53 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 54 | } 55 | } 56 | 57 | static void 58 | text_layout_set_property (GObject *object, 59 | guint prop_id, 60 | const GValue *value, 61 | GParamSpec *pspec) 62 | { 63 | TextLayout *self = TEXT_LAYOUT (object); 64 | 65 | switch (prop_id) 66 | { 67 | default: 68 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 69 | } 70 | } 71 | 72 | TextLayoutBox * 73 | build_layout_tree_recursive (TextLayout *self, 74 | PangoContext *context, 75 | TextItem *item) 76 | { 77 | TextLayoutBox *box; 78 | TextNode *iter; 79 | 80 | g_return_val_if_fail (TEXT_IS_LAYOUT (self), NULL); 81 | g_return_val_if_fail (PANGO_IS_CONTEXT (context), NULL); 82 | g_return_val_if_fail (TEXT_IS_ITEM (item), NULL); 83 | 84 | // Construct a layout item for this node using the item factory 85 | // Subclasses can override this to add and use their own items 86 | box = TEXT_LAYOUT_GET_CLASS (self)->item_factory (TEXT_ITEM (item)); 87 | 88 | // For now, if a node does not provide a LayoutBox then we assume 89 | // it and its children are invisible. Perhaps we want to introduce 90 | // some kind of LayoutAnonymousBox which is transparently skipped by 91 | // the layout engine. 92 | if (!TEXT_IS_LAYOUT_BOX (box)) 93 | return NULL; 94 | 95 | // Setup Box 96 | text_layout_box_set_item (box, item); 97 | text_item_detach (TEXT_ITEM (item)); // TODO: Move to a 'cleanup_tree' function? 98 | text_item_attach (TEXT_ITEM (item), TEXT_NODE (box)); 99 | 100 | // Append children 101 | for (iter = text_node_get_first_child (TEXT_NODE (item)); 102 | iter != NULL; 103 | iter = text_node_get_next (iter)) 104 | { 105 | TextLayoutBox *child_box; 106 | 107 | g_assert (TEXT_IS_ITEM(iter)); 108 | 109 | child_box = build_layout_tree_recursive (self, context, TEXT_ITEM (iter)); 110 | 111 | if (TEXT_IS_LAYOUT_BOX (child_box)) 112 | { 113 | text_node_append_child (TEXT_NODE (box), TEXT_NODE (child_box)); 114 | } 115 | } 116 | 117 | return box; 118 | } 119 | 120 | TextLayoutBox * 121 | text_layout_default_item_factory (TextItem *item) 122 | { 123 | GType type; 124 | type = G_TYPE_FROM_INSTANCE (item); 125 | 126 | // Go from most specific to least specific, otherwise 127 | // we could accidentally create the wrong layout box 128 | // by taking the base class instead of the subclass 129 | 130 | // Images 131 | if (type == TEXT_TYPE_IMAGE) 132 | return TEXT_LAYOUT_BOX (text_layout_inline_new ()); 133 | 134 | // Paragraphs 135 | if (type == TEXT_TYPE_PARAGRAPH) 136 | return TEXT_LAYOUT_BOX (text_layout_block_new ()); 137 | 138 | // Text Runs 139 | if (type == TEXT_TYPE_RUN) 140 | return NULL; 141 | 142 | // Frames 143 | if (type == TEXT_TYPE_FRAME) 144 | return TEXT_LAYOUT_BOX (text_layout_block_new ()); 145 | 146 | // It is an error to provide a type for which no layout 147 | // item exists - we cannot display it. 148 | g_critical ("Cannot create layout item for type '%s'.", 149 | g_type_name (type)); 150 | 151 | return NULL; 152 | } 153 | 154 | TextLayoutBox * 155 | text_layout_build_layout_tree (TextLayout *self, 156 | PangoContext *context, 157 | TextFrame *frame, 158 | int width) 159 | { 160 | TextLayoutBox *root; 161 | 162 | g_return_val_if_fail (TEXT_IS_LAYOUT (self), NULL); 163 | g_return_val_if_fail (TEXT_IS_FRAME (frame), NULL); 164 | 165 | root = build_layout_tree_recursive (self, context, TEXT_ITEM (frame)); 166 | text_layout_box_layout (root, context, width, 0, 0); 167 | return root; 168 | } 169 | 170 | TextLayoutBox * 171 | text_layout_find_above (TextLayoutBox *item) 172 | { 173 | g_return_val_if_fail (TEXT_IS_LAYOUT_BOX (item), NULL); 174 | return TEXT_LAYOUT_BOX (text_node_get_previous (TEXT_NODE (item))); 175 | } 176 | 177 | TextLayoutBox * 178 | text_layout_find_below (TextLayoutBox *item) 179 | { 180 | g_return_val_if_fail (TEXT_IS_LAYOUT_BOX (item), NULL); 181 | return TEXT_LAYOUT_BOX (text_node_get_next (TEXT_NODE (item))); 182 | } 183 | 184 | TextLayoutBox * 185 | text_layout_pick_internal (TextLayoutBox *root, 186 | double x, 187 | double y, 188 | double *min_y_distance, 189 | TextLayoutBox **min_y_layout) 190 | { 191 | // Note: 'x' and 'y' are relative to the document origin 192 | TextNode *child; 193 | TextNode *found; 194 | 195 | g_return_val_if_fail (TEXT_IS_LAYOUT_BOX (root), NULL); 196 | 197 | for (child = text_node_get_first_child (TEXT_NODE (root)); 198 | child != NULL; 199 | child = text_node_get_next (child)) 200 | { 201 | TextLayoutBox *layout_item; 202 | const TextDimensions *bbox; 203 | double dist_to_y; 204 | 205 | layout_item = TEXT_LAYOUT_BOX (child); 206 | bbox = text_layout_box_get_bbox (layout_item); 207 | 208 | // Recursively check child layouts first 209 | found = TEXT_NODE (text_layout_pick_internal (layout_item, x - bbox->x, y - bbox->y, min_y_distance, min_y_layout)); 210 | 211 | if (found) { 212 | return TEXT_LAYOUT_BOX (found); 213 | } 214 | 215 | // Check if the cursor is fully within the bounding box 216 | if (x >= bbox->x && 217 | y >= bbox->y && 218 | x <= bbox->x + bbox->width && 219 | y <= bbox->y + bbox->height) 220 | { 221 | return layout_item; 222 | } 223 | 224 | // Check if the cursor is vertically within the bounding box 225 | if (y >= bbox->y && 226 | y <= bbox->y + bbox->height) 227 | { 228 | *min_y_distance = 0; 229 | *min_y_layout = layout_item; 230 | continue; 231 | } 232 | 233 | // Finally check the vertical offset from the cursor to the 234 | // closest edge of the bounding box 235 | dist_to_y = (y < bbox->y) 236 | ? bbox->y - y 237 | : y - bbox->y; 238 | 239 | if (dist_to_y < *min_y_distance) { 240 | *min_y_distance = dist_to_y; 241 | *min_y_layout = layout_item; 242 | } 243 | } 244 | 245 | return NULL; 246 | } 247 | 248 | TextLayoutBox * 249 | text_layout_pick (TextLayoutBox *root, 250 | int x, 251 | int y) 252 | { 253 | double min_y_distance = G_MAXDOUBLE; 254 | TextLayoutBox *min_y_layout = NULL; 255 | TextLayoutBox *result; 256 | 257 | result = text_layout_pick_internal(root, x, y, &min_y_distance, &min_y_layout); 258 | 259 | if (result) { 260 | return result; 261 | } 262 | 263 | // Otherwise return nearest layout 264 | return min_y_layout; 265 | } 266 | 267 | static void 268 | text_layout_class_init (TextLayoutClass *klass) 269 | { 270 | klass->item_factory = text_layout_default_item_factory; 271 | 272 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 273 | 274 | object_class->finalize = text_layout_finalize; 275 | object_class->get_property = text_layout_get_property; 276 | object_class->set_property = text_layout_set_property; 277 | } 278 | 279 | static void 280 | text_layout_init (TextLayout *self) 281 | { 282 | } 283 | -------------------------------------------------------------------------------- /src/layout/layout.h: -------------------------------------------------------------------------------- 1 | /* layout.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include "layoutbox.h" 19 | #include "layoutblock.h" 20 | #include "layoutinline.h" 21 | 22 | #include "../model/frame.h" 23 | #include "../model/paragraph.h" 24 | #include "../model/mark.h" 25 | #include "../model/image.h" 26 | 27 | G_BEGIN_DECLS 28 | 29 | #define TEXT_TYPE_LAYOUT (text_layout_get_type()) 30 | 31 | G_DECLARE_DERIVABLE_TYPE (TextLayout, text_layout, TEXT, LAYOUT, GObject) 32 | 33 | struct _TextLayoutClass 34 | { 35 | GObjectClass parent_class; 36 | TextLayoutBox *(*item_factory)(TextItem *item); 37 | }; 38 | 39 | TextLayout *text_layout_new (void); 40 | 41 | TextLayoutBox * 42 | text_layout_build_layout_tree (TextLayout *self, 43 | PangoContext *context, 44 | TextFrame *frame, 45 | int width); 46 | 47 | TextLayoutBox * 48 | text_layout_pick (TextLayoutBox *root, 49 | int x, 50 | int y); 51 | 52 | TextLayoutBox * 53 | text_layout_find_above (TextLayoutBox *item); 54 | 55 | TextLayoutBox * 56 | text_layout_find_below (TextLayoutBox *item); 57 | 58 | G_END_DECLS 59 | -------------------------------------------------------------------------------- /src/layout/layoutblock.c: -------------------------------------------------------------------------------- 1 | /* layoutblock.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "layoutblock.h" 13 | 14 | #include "../model/paragraph.h" 15 | #include "../model/image.h" 16 | 17 | #include "layoutinline.h" 18 | 19 | typedef struct 20 | { 21 | PangoLayout *layout; 22 | } TextLayoutBlockPrivate; 23 | 24 | G_DEFINE_TYPE_WITH_PRIVATE (TextLayoutBlock, text_layout_block, TEXT_TYPE_LAYOUT_BOX) 25 | 26 | enum { 27 | PROP_0, 28 | N_PROPS 29 | }; 30 | 31 | static GParamSpec *properties [N_PROPS]; 32 | 33 | TextLayoutBlock * 34 | text_layout_block_new () 35 | { 36 | return TEXT_LAYOUT_BLOCK (g_object_new (TEXT_TYPE_LAYOUT_BLOCK, NULL)); 37 | } 38 | 39 | static void 40 | text_layout_block_finalize (GObject *object) 41 | { 42 | TextLayoutBlock *self = (TextLayoutBlock *)object; 43 | TextLayoutBlockPrivate *priv = text_layout_block_get_instance_private (self); 44 | 45 | G_OBJECT_CLASS (text_layout_block_parent_class)->finalize (object); 46 | } 47 | 48 | static void 49 | text_layout_block_get_property (GObject *object, 50 | guint prop_id, 51 | GValue *value, 52 | GParamSpec *pspec) 53 | { 54 | TextLayoutBlock *self = TEXT_LAYOUT_BLOCK (object); 55 | 56 | switch (prop_id) 57 | { 58 | default: 59 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 60 | } 61 | } 62 | 63 | static void 64 | text_layout_block_set_property (GObject *object, 65 | guint prop_id, 66 | const GValue *value, 67 | GParamSpec *pspec) 68 | { 69 | TextLayoutBlock *self = TEXT_LAYOUT_BLOCK (object); 70 | 71 | switch (prop_id) 72 | { 73 | default: 74 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 75 | } 76 | } 77 | 78 | 79 | static void 80 | _set_inline_attribute (TextOpaque *opaque, 81 | PangoAttrList *list, 82 | int start_index, 83 | int run_length) 84 | { 85 | PangoAttribute *attr; 86 | PangoRectangle rect; 87 | 88 | TextLayoutBox *inline_box; 89 | const TextDimensions *bbox; 90 | 91 | inline_box = TEXT_LAYOUT_BOX (text_item_get_attachment (TEXT_ITEM (opaque))); 92 | bbox = text_layout_box_get_bbox (inline_box); 93 | 94 | g_assert (TEXT_IS_LAYOUT_INLINE (inline_box)); 95 | 96 | // Get Style Properties 97 | rect.width = (int) bbox->width * PANGO_SCALE; 98 | rect.height = (int) bbox->height * PANGO_SCALE; 99 | 100 | // Shape Attribute 101 | attr = pango_attr_shape_new (&rect, &rect); 102 | attr->start_index = start_index; 103 | attr->end_index = start_index + run_length; 104 | pango_attr_list_insert (list, attr); 105 | } 106 | 107 | static void 108 | _set_run_attribute (TextRun *run, 109 | PangoAttrList *list, 110 | int start_index, 111 | int run_length) 112 | { 113 | gboolean is_bold, is_italic, is_underline; 114 | PangoAttribute *attr; 115 | 116 | // Get Style Properties 117 | is_bold = text_run_get_style_bold (TEXT_RUN (run)); 118 | is_italic = text_run_get_style_italic (TEXT_RUN (run)); 119 | is_underline = text_run_get_style_underline (TEXT_RUN (run)); 120 | 121 | // Attribute: Bold 122 | if (is_bold) 123 | { 124 | attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); 125 | attr->start_index = start_index; 126 | attr->end_index = start_index + run_length; 127 | pango_attr_list_insert (list, attr); 128 | } 129 | 130 | // Attribute: Italic 131 | if (is_italic) 132 | { 133 | attr = pango_attr_style_new (PANGO_STYLE_ITALIC); 134 | attr->start_index = start_index; 135 | attr->end_index = start_index + run_length; 136 | pango_attr_list_insert (list, attr); 137 | } 138 | 139 | // Attribute: Underline 140 | if (is_underline) 141 | { 142 | attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); 143 | attr->start_index = start_index; 144 | attr->end_index = start_index + run_length; 145 | pango_attr_list_insert (list, attr); 146 | } 147 | } 148 | 149 | void 150 | _set_attributes (TextParagraph *paragraph, 151 | PangoLayout *pango_layout) 152 | { 153 | TextNode *fragment; 154 | PangoAttrList *list; 155 | 156 | int start_index; 157 | 158 | list = pango_attr_list_new(); 159 | 160 | g_return_if_fail (TEXT_IS_PARAGRAPH (paragraph)); 161 | 162 | start_index = 0; 163 | 164 | for (fragment = text_node_get_first_child (TEXT_NODE (paragraph)); 165 | fragment != NULL; 166 | fragment = text_node_get_next (fragment)) 167 | { 168 | int run_length; 169 | 170 | run_length = text_fragment_get_size_bytes (TEXT_FRAGMENT (fragment)); 171 | 172 | if (TEXT_IS_RUN (fragment)) 173 | _set_run_attribute (TEXT_RUN (fragment), list, start_index, run_length); 174 | else if (TEXT_IS_OPAQUE (fragment)) 175 | _set_inline_attribute (TEXT_OPAQUE (fragment), list, start_index, run_length); 176 | 177 | start_index += run_length; 178 | } 179 | 180 | pango_layout_set_attributes (pango_layout, list); 181 | } 182 | 183 | static void 184 | do_block_layout (TextLayoutBox *self, 185 | PangoContext *context, 186 | int width, 187 | int offset_x, 188 | int offset_y) 189 | { 190 | TextNode *iter; 191 | TextDimensions *bbox; 192 | 193 | int child_offset_y; 194 | int height; 195 | 196 | height = 0; 197 | child_offset_y = 0; 198 | bbox = text_layout_box_get_mutable_bbox (self); 199 | 200 | // Recompute child element offset 201 | for (iter = text_node_get_first_child (TEXT_NODE (self)); 202 | iter != NULL; 203 | iter = text_node_get_next (TEXT_NODE (iter))) 204 | { 205 | const TextDimensions *child_bbox; 206 | TextLayoutBox *child_box = TEXT_LAYOUT_BOX (iter); 207 | 208 | g_assert (TEXT_IS_LAYOUT_BLOCK (iter)); 209 | 210 | text_layout_box_layout (child_box, context, width, offset_x, offset_y + child_offset_y); 211 | 212 | child_bbox = text_layout_box_get_bbox (child_box); 213 | child_offset_y += (int) child_bbox->height; 214 | } 215 | 216 | height += child_offset_y; 217 | 218 | bbox->x = offset_x; 219 | bbox->y = offset_y; 220 | bbox->width = width; 221 | bbox->height = height; 222 | } 223 | 224 | static void 225 | do_inline_layout (TextLayoutBox *self, 226 | PangoContext *context, 227 | int width, 228 | int offset_x, 229 | int offset_y) 230 | { 231 | TextNode *iter; 232 | TextItem *item; 233 | TextDimensions *bbox; 234 | TextLayoutBlockPrivate *priv; 235 | 236 | int height; 237 | int byte_offset; 238 | 239 | item = text_layout_box_get_item (self); 240 | priv = text_layout_block_get_instance_private (TEXT_LAYOUT_BLOCK (self)); 241 | bbox = text_layout_box_get_mutable_bbox (self); 242 | 243 | // Precompute inline children requested size 244 | for (iter = text_node_get_first_child (TEXT_NODE (self)); 245 | iter != NULL; 246 | iter = text_node_get_next (TEXT_NODE (iter))) 247 | { 248 | // Get fixed size so we can set pango attributes accordingly 249 | g_assert (TEXT_IS_LAYOUT_INLINE (iter)); 250 | text_layout_box_layout (TEXT_LAYOUT_BOX (iter), context, 0, 0, 0); 251 | } 252 | 253 | // Setup pango layout 254 | if (item && TEXT_IS_PARAGRAPH (item)) 255 | { 256 | gchar *text; 257 | text = text_paragraph_get_text (TEXT_PARAGRAPH (item)); 258 | 259 | if (!priv->layout) 260 | priv->layout = pango_layout_new (context); 261 | 262 | // Set style information 263 | // TODO: Matching from ruleset 264 | _set_attributes (TEXT_PARAGRAPH (item), priv->layout); 265 | 266 | // Set basic layout properties 267 | pango_layout_set_text (priv->layout, text, -1); 268 | pango_layout_set_wrap (priv->layout, PANGO_WRAP_WORD_CHAR); 269 | pango_layout_set_width (priv->layout, PANGO_SCALE * width); 270 | pango_layout_get_pixel_size (priv->layout, NULL, &height); 271 | g_free (text); 272 | } 273 | 274 | // Recompute x/y offsets of inline children 275 | 276 | byte_offset = 0; 277 | for (iter = text_node_get_first_child (TEXT_NODE (item)); 278 | iter != NULL; 279 | iter = text_node_get_next (TEXT_NODE (iter))) 280 | { 281 | TextNode *inline_box; 282 | PangoRectangle rect; 283 | 284 | // Try to get layout item attachment 285 | inline_box = text_item_get_attachment (TEXT_ITEM (iter)); 286 | 287 | if (TEXT_IS_LAYOUT_INLINE (inline_box)) 288 | { 289 | // Get starting x,y position of run at this index 290 | pango_layout_index_to_pos (priv->layout, byte_offset, &rect); 291 | 292 | // Re-layout child with new x/y offset 293 | text_layout_box_layout (TEXT_LAYOUT_BOX (inline_box), context, 0, 294 | rect.x / PANGO_SCALE, 295 | rect.y / PANGO_SCALE); 296 | } 297 | 298 | // Increase byte offset into the paragraph 299 | byte_offset += text_fragment_get_size_bytes (TEXT_FRAGMENT (iter)); 300 | } 301 | 302 | bbox->x = offset_x; 303 | bbox->y = offset_y; 304 | bbox->width = width; 305 | bbox->height = height; 306 | } 307 | 308 | static void 309 | text_layout_block_layout (TextLayoutBox *self, 310 | PangoContext *context, 311 | int width, 312 | int offset_x, 313 | int offset_y) 314 | { 315 | TextNode *first_child; 316 | 317 | g_return_if_fail (TEXT_IS_LAYOUT_BLOCK (self)); 318 | 319 | // Blocks can only contain all block children OR 320 | // all inline children, but not a mixture of each. 321 | first_child = text_node_get_first_child (TEXT_NODE (self)); 322 | 323 | if (TEXT_IS_LAYOUT_BLOCK (first_child)) 324 | do_block_layout (self, context, width, offset_x, offset_y); 325 | else 326 | do_inline_layout (self, context, width, offset_x, offset_y); 327 | } 328 | 329 | PangoLayout * 330 | text_layout_block_get_pango_layout (TextLayoutBlock *self) 331 | { 332 | TextLayoutBlockPrivate *priv = text_layout_block_get_instance_private (self); 333 | return priv->layout; 334 | } 335 | 336 | static void 337 | text_layout_block_class_init (TextLayoutBlockClass *klass) 338 | { 339 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 340 | 341 | object_class->finalize = text_layout_block_finalize; 342 | object_class->get_property = text_layout_block_get_property; 343 | object_class->set_property = text_layout_block_set_property; 344 | 345 | TextLayoutBoxClass *layout_box_class = TEXT_LAYOUT_BOX_CLASS (klass); 346 | 347 | layout_box_class->layout = text_layout_block_layout; 348 | } 349 | 350 | static void 351 | text_layout_block_init (TextLayoutBlock *self) 352 | { 353 | TextLayoutBlockPrivate *priv = text_layout_block_get_instance_private (self); 354 | } 355 | -------------------------------------------------------------------------------- /src/layout/layoutblock.h: -------------------------------------------------------------------------------- 1 | /* layoutblock.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | 17 | #include "layoutbox.h" 18 | #include "layoutbox-impl.h" 19 | 20 | #include "types.h" 21 | 22 | G_BEGIN_DECLS 23 | 24 | #define TEXT_TYPE_LAYOUT_BLOCK (text_layout_block_get_type()) 25 | 26 | G_DECLARE_DERIVABLE_TYPE (TextLayoutBlock, text_layout_block, TEXT, LAYOUT_BLOCK, TextLayoutBox) 27 | 28 | struct _TextLayoutBlockClass 29 | { 30 | TextLayoutBoxClass parent_class; 31 | }; 32 | 33 | TextLayoutBlock *text_layout_block_new (void); 34 | 35 | PangoLayout * 36 | text_layout_block_get_pango_layout (TextLayoutBlock *self); 37 | 38 | G_END_DECLS 39 | -------------------------------------------------------------------------------- /src/layout/layoutbox-impl.h: -------------------------------------------------------------------------------- 1 | /* layoutbox-impl.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include "layoutbox.h" 15 | 16 | G_BEGIN_DECLS 17 | 18 | TextDimensions * 19 | text_layout_box_get_mutable_bbox (TextLayoutBox *self); 20 | 21 | G_END_DECLS 22 | -------------------------------------------------------------------------------- /src/layout/layoutbox.c: -------------------------------------------------------------------------------- 1 | /* layoutbox.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "layoutbox.h" 13 | #include "layoutbox-impl.h" 14 | 15 | typedef struct 16 | { 17 | TextItem *item; 18 | TextDimensions bbox; 19 | } TextLayoutBoxPrivate; 20 | 21 | G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (TextLayoutBox, text_layout_box, TEXT_TYPE_NODE) 22 | 23 | enum { 24 | PROP_0, 25 | N_PROPS 26 | }; 27 | 28 | static GParamSpec *properties [N_PROPS]; 29 | 30 | /** 31 | * text_layout_box_new: 32 | * 33 | * Create a new #TextLayoutBox. 34 | * 35 | * Returns: (transfer full): a newly created #TextLayoutBox 36 | */ 37 | TextLayoutBox * 38 | text_layout_box_new (void) 39 | { 40 | return g_object_new (TEXT_TYPE_LAYOUT_BOX, NULL); 41 | } 42 | 43 | static void 44 | text_layout_box_finalize (GObject *object) 45 | { 46 | TextLayoutBox *self = (TextLayoutBox *)object; 47 | TextLayoutBoxPrivate *priv = text_layout_box_get_instance_private (self); 48 | 49 | // TODO: Dispose of children 50 | 51 | G_OBJECT_CLASS (text_layout_box_parent_class)->finalize (object); 52 | } 53 | 54 | static void 55 | text_layout_box_get_property (GObject *object, 56 | guint prop_id, 57 | GValue *value, 58 | GParamSpec *pspec) 59 | { 60 | TextLayoutBox *self = TEXT_LAYOUT_BOX (object); 61 | 62 | switch (prop_id) 63 | { 64 | default: 65 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 66 | } 67 | } 68 | 69 | static void 70 | text_layout_box_set_property (GObject *object, 71 | guint prop_id, 72 | const GValue *value, 73 | GParamSpec *pspec) 74 | { 75 | TextLayoutBox *self = TEXT_LAYOUT_BOX (object); 76 | 77 | switch (prop_id) 78 | { 79 | default: 80 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 81 | } 82 | } 83 | 84 | void 85 | text_layout_box_layout (TextLayoutBox *self, 86 | PangoContext *context, 87 | int width, 88 | int offset_x, 89 | int offset_y) 90 | { 91 | TEXT_LAYOUT_BOX_GET_CLASS (self)->layout (self, context, width, offset_x, offset_y); 92 | } 93 | 94 | void 95 | text_layout_box_real_layout (TextLayoutBox *self, 96 | PangoContext *context, 97 | int width, 98 | int offset_x, 99 | int offset_y) 100 | { 101 | g_return_if_fail (TEXT_IS_LAYOUT_BOX (self)); 102 | 103 | TextLayoutBoxPrivate *priv; 104 | 105 | priv = text_layout_box_get_instance_private (self); 106 | 107 | priv->bbox.x = offset_x; 108 | priv->bbox.y = offset_y; 109 | priv->bbox.width = width; 110 | priv->bbox.height = 0; 111 | 112 | g_warning ("%s does not override text_layout_box_layout(), no layout will occur.\n", 113 | g_type_name_from_instance ((GTypeInstance *) self)); 114 | } 115 | 116 | void 117 | text_layout_box_set_item (TextLayoutBox *self, 118 | TextItem *item) 119 | { 120 | TextLayoutBoxPrivate *priv = text_layout_box_get_instance_private (self); 121 | priv->item = item; 122 | } 123 | 124 | TextItem * 125 | text_layout_box_get_item (TextLayoutBox *self) 126 | { 127 | TextLayoutBoxPrivate *priv = text_layout_box_get_instance_private (self); 128 | return priv->item; 129 | } 130 | 131 | TextDimensions * 132 | text_layout_box_get_mutable_bbox (TextLayoutBox *self) 133 | { 134 | TextLayoutBoxPrivate *priv = text_layout_box_get_instance_private (self); 135 | return &priv->bbox; 136 | } 137 | 138 | const TextDimensions * 139 | text_layout_box_get_bbox (TextLayoutBox *self) 140 | { 141 | return text_layout_box_get_mutable_bbox (self); 142 | } 143 | 144 | static void 145 | text_layout_box_class_init (TextLayoutBoxClass *klass) 146 | { 147 | klass->layout = text_layout_box_real_layout; 148 | 149 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 150 | 151 | object_class->finalize = text_layout_box_finalize; 152 | object_class->get_property = text_layout_box_get_property; 153 | object_class->set_property = text_layout_box_set_property; 154 | } 155 | 156 | static void 157 | text_layout_box_init (TextLayoutBox *self) 158 | { 159 | TextLayoutBoxPrivate *priv = text_layout_box_get_instance_private (self); 160 | } 161 | -------------------------------------------------------------------------------- /src/layout/layoutbox.h: -------------------------------------------------------------------------------- 1 | /* layoutbox.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | 17 | #include "../tree/node.h" 18 | #include "../model/item.h" 19 | 20 | #include "types.h" 21 | 22 | G_BEGIN_DECLS 23 | 24 | #define TEXT_TYPE_LAYOUT_BOX (text_layout_box_get_type()) 25 | 26 | G_DECLARE_DERIVABLE_TYPE (TextLayoutBox, text_layout_box, TEXT, LAYOUT_BOX, TextNode) 27 | 28 | struct _TextLayoutBoxClass 29 | { 30 | TextNodeClass parent_class; 31 | void (*layout)(TextLayoutBox *self, PangoContext *context, int width, int offset_x, int offset_y); 32 | }; 33 | 34 | void 35 | text_layout_box_set_item (TextLayoutBox *self, 36 | TextItem *item); 37 | 38 | TextItem * 39 | text_layout_box_get_item (TextLayoutBox *self); 40 | 41 | void 42 | text_layout_box_layout (TextLayoutBox *self, 43 | PangoContext *context, 44 | int width, 45 | int offset_x, 46 | int offset_y); 47 | 48 | const TextDimensions * 49 | text_layout_box_get_bbox (TextLayoutBox *self); 50 | 51 | G_END_DECLS 52 | -------------------------------------------------------------------------------- /src/layout/layoutinline.c: -------------------------------------------------------------------------------- 1 | /* layoutinline.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "layoutinline.h" 13 | 14 | typedef struct 15 | { 16 | int padding; 17 | } TextLayoutInlinePrivate; 18 | 19 | G_DEFINE_TYPE_WITH_PRIVATE (TextLayoutInline, text_layout_inline, TEXT_TYPE_LAYOUT_BOX) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | /** 29 | * text_layout_inline_new: 30 | * 31 | * Create a new #TextLayoutInline. 32 | * 33 | * Returns: (transfer full): a newly created #TextLayoutInline 34 | */ 35 | TextLayoutInline * 36 | text_layout_inline_new (void) 37 | { 38 | return g_object_new (TEXT_TYPE_LAYOUT_INLINE, NULL); 39 | } 40 | 41 | static void 42 | text_layout_inline_finalize (GObject *object) 43 | { 44 | TextLayoutInline *self = (TextLayoutInline *)object; 45 | TextLayoutInlinePrivate *priv = text_layout_inline_get_instance_private (self); 46 | 47 | // TODO: Dispose of children 48 | 49 | G_OBJECT_CLASS (text_layout_inline_parent_class)->finalize (object); 50 | } 51 | 52 | static void 53 | text_layout_inline_get_property (GObject *object, 54 | guint prop_id, 55 | GValue *value, 56 | GParamSpec *pspec) 57 | { 58 | TextLayoutInline *self = TEXT_LAYOUT_INLINE (object); 59 | 60 | switch (prop_id) 61 | { 62 | default: 63 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 64 | } 65 | } 66 | 67 | static void 68 | text_layout_inline_set_property (GObject *object, 69 | guint prop_id, 70 | const GValue *value, 71 | GParamSpec *pspec) 72 | { 73 | TextLayoutInline *self = TEXT_LAYOUT_INLINE (object); 74 | 75 | switch (prop_id) 76 | { 77 | default: 78 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 79 | } 80 | } 81 | 82 | void 83 | text_layout_inline_layout (TextLayoutBox *self, 84 | PangoContext *context, 85 | int width, 86 | int offset_x, 87 | int offset_y) 88 | { 89 | TextLayoutInlinePrivate *priv; 90 | TextDimensions *bbox; 91 | 92 | g_return_if_fail (TEXT_IS_LAYOUT_INLINE (self)); 93 | 94 | priv = text_layout_inline_get_instance_private (TEXT_LAYOUT_INLINE (self)); 95 | bbox = text_layout_box_get_mutable_bbox (self); 96 | 97 | bbox->x = offset_x; 98 | bbox->y = offset_y; 99 | bbox->width = 200; 100 | bbox->height = 100; 101 | } 102 | 103 | static void 104 | text_layout_inline_class_init (TextLayoutInlineClass *klass) 105 | { 106 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 107 | 108 | object_class->finalize = text_layout_inline_finalize; 109 | object_class->get_property = text_layout_inline_get_property; 110 | object_class->set_property = text_layout_inline_set_property; 111 | 112 | TextLayoutBoxClass *layout_box_class = TEXT_LAYOUT_BOX_CLASS (klass); 113 | 114 | layout_box_class->layout = text_layout_inline_layout; 115 | } 116 | 117 | static void 118 | text_layout_inline_init (TextLayoutInline *self) 119 | { 120 | TextLayoutInlinePrivate *priv = text_layout_inline_get_instance_private (self); 121 | } 122 | -------------------------------------------------------------------------------- /src/layout/layoutinline.h: -------------------------------------------------------------------------------- 1 | /* layoutinline.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | 17 | #include "layoutbox.h" 18 | #include "layoutbox-impl.h" 19 | 20 | #include "types.h" 21 | 22 | G_BEGIN_DECLS 23 | 24 | #define TEXT_TYPE_LAYOUT_INLINE (text_layout_inline_get_type()) 25 | 26 | G_DECLARE_DERIVABLE_TYPE (TextLayoutInline, text_layout_inline, TEXT, LAYOUT_INLINE, TextLayoutBox) 27 | 28 | struct _TextLayoutInlineClass 29 | { 30 | TextLayoutBoxClass parent_class; 31 | }; 32 | 33 | TextLayoutInline *text_layout_inline_new (void); 34 | 35 | G_END_DECLS 36 | -------------------------------------------------------------------------------- /src/layout/meson.build: -------------------------------------------------------------------------------- 1 | text_engine_sources += files([ 2 | 'layout.c', 3 | 'layoutbox.c', 4 | 'layoutblock.c', 5 | 'layoutinline.c' 6 | ]) 7 | 8 | layout_headers = [ 9 | 'layout.h', 10 | 'layoutbox.h', 11 | 'layoutblock.h', 12 | 'layoutinline.h', 13 | 'types.h' 14 | ] 15 | 16 | install_headers(layout_headers, subdir : header_dir / 'layout') 17 | -------------------------------------------------------------------------------- /src/layout/types.h: -------------------------------------------------------------------------------- 1 | /* types.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | /* TODO: Make these GBoxed for introspection support! */ 13 | 14 | #pragma once 15 | 16 | #include 17 | 18 | typedef struct 19 | { 20 | gdouble x; 21 | gdouble y; 22 | gdouble width; 23 | gdouble height; 24 | 25 | // TODO: Also consider padding/margin/border? 26 | } TextDimensions; 27 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | api_version = '0.1' 2 | header_dir = 'text-engine' 3 | 4 | text_engine_sources = [ 5 | 'text-engine.c', 6 | ] 7 | 8 | text_engine_headers = [ 9 | 'text-engine.h', 10 | ] 11 | 12 | # Utility 13 | subdir('format') 14 | 15 | # Trees 16 | subdir('tree') 17 | subdir('model') 18 | subdir('layout') 19 | subdir('editor') 20 | subdir('ui') 21 | 22 | version_split = meson.project_version().split('.') 23 | MAJOR_VERSION = version_split[0] 24 | MINOR_VERSION = version_split[1] 25 | MICRO_VERSION = version_split[2] 26 | 27 | version_conf = configuration_data() 28 | version_conf.set('VERSION', meson.project_version()) 29 | version_conf.set('MAJOR_VERSION', MAJOR_VERSION) 30 | version_conf.set('MINOR_VERSION', MINOR_VERSION) 31 | version_conf.set('MICRO_VERSION', MICRO_VERSION) 32 | 33 | configure_file( 34 | input: 'text-engine-version.h.in', 35 | output: 'text-engine-version.h', 36 | configuration: version_conf, 37 | install: true, 38 | install_dir: join_paths(get_option('includedir'), header_dir) 39 | ) 40 | 41 | text_engine_deps = [ 42 | dependency('gio-2.0', version: '>= 2.50'), 43 | dependency('json-glib-1.0'), 44 | dependency('libxml-2.0'), 45 | dependency('gtk4') 46 | ] 47 | 48 | text_engine_lib = shared_library('text-engine-' + api_version, 49 | text_engine_sources, 50 | dependencies: text_engine_deps, 51 | soversion: 0, 52 | install: true, 53 | ) 54 | 55 | install_headers(text_engine_headers, subdir: header_dir) 56 | 57 | text_engine_dep = declare_dependency( 58 | link_with: text_engine_lib, 59 | include_directories: include_directories('.'), 60 | dependencies: text_engine_deps, 61 | # sources: resources 62 | ) 63 | 64 | pkg = import('pkgconfig') 65 | 66 | pkg.generate( 67 | description: 'A shared library for ...', 68 | libraries: text_engine_lib, 69 | name: 'text-engine', 70 | filebase: 'text-engine-' + api_version, 71 | version: meson.project_version(), 72 | subdirs: 'text-engine', 73 | requires: 'glib-2.0', 74 | install_dir: join_paths(get_option('libdir'), 'pkgconfig') 75 | ) 76 | -------------------------------------------------------------------------------- /src/model/README.md: -------------------------------------------------------------------------------- 1 | # Model 2 | This is the text engine document model. It is a platform agnostic representation 3 | of a rich text document using a tree-like structure. 4 | -------------------------------------------------------------------------------- /src/model/block.c: -------------------------------------------------------------------------------- 1 | /* block.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "block.h" 13 | 14 | typedef struct 15 | { 16 | int _padding; 17 | } TextBlockPrivate; 18 | 19 | G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (TextBlock, text_block, TEXT_TYPE_ITEM) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | static void 29 | text_block_finalize (GObject *object) 30 | { 31 | TextBlock *self = (TextBlock *)object; 32 | TextBlockPrivate *priv = text_block_get_instance_private (self); 33 | 34 | G_OBJECT_CLASS (text_block_parent_class)->finalize (object); 35 | } 36 | 37 | static void 38 | text_block_get_property (GObject *object, 39 | guint prop_id, 40 | GValue *value, 41 | GParamSpec *pspec) 42 | { 43 | TextBlock *self = TEXT_BLOCK (object); 44 | 45 | switch (prop_id) 46 | { 47 | default: 48 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 49 | } 50 | } 51 | 52 | static void 53 | text_block_set_property (GObject *object, 54 | guint prop_id, 55 | const GValue *value, 56 | GParamSpec *pspec) 57 | { 58 | TextBlock *self = TEXT_BLOCK (object); 59 | 60 | switch (prop_id) 61 | { 62 | default: 63 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 64 | } 65 | } 66 | 67 | static void 68 | text_block_class_init (TextBlockClass *klass) 69 | { 70 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 71 | 72 | object_class->finalize = text_block_finalize; 73 | object_class->get_property = text_block_get_property; 74 | object_class->set_property = text_block_set_property; 75 | } 76 | 77 | static void 78 | text_block_init (TextBlock *self) 79 | { 80 | } 81 | -------------------------------------------------------------------------------- /src/model/block.h: -------------------------------------------------------------------------------- 1 | /* block.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "item.h" 17 | 18 | G_BEGIN_DECLS 19 | 20 | #define TEXT_TYPE_BLOCK (text_block_get_type()) 21 | 22 | G_DECLARE_DERIVABLE_TYPE (TextBlock, text_block, TEXT, BLOCK, TextItem) 23 | 24 | struct _TextBlockClass 25 | { 26 | TextNodeClass parent_class; 27 | }; 28 | 29 | G_END_DECLS 30 | -------------------------------------------------------------------------------- /src/model/document.c: -------------------------------------------------------------------------------- 1 | /* document.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "document.h" 13 | 14 | G_DEFINE_FINAL_TYPE (TextDocument, text_document, G_TYPE_OBJECT) 15 | 16 | enum { 17 | PROP_0, 18 | N_PROPS 19 | }; 20 | 21 | static GParamSpec *properties [N_PROPS]; 22 | 23 | TextDocument * 24 | text_document_new (void) 25 | { 26 | return g_object_new (TEXT_TYPE_DOCUMENT, NULL); 27 | } 28 | 29 | static void 30 | text_document_finalize (GObject *object) 31 | { 32 | TextDocument *self = (TextDocument *)object; 33 | 34 | G_OBJECT_CLASS (text_document_parent_class)->finalize (object); 35 | } 36 | 37 | static void 38 | text_document_get_property (GObject *object, 39 | guint prop_id, 40 | GValue *value, 41 | GParamSpec *pspec) 42 | { 43 | TextDocument *self = TEXT_DOCUMENT (object); 44 | 45 | switch (prop_id) 46 | { 47 | default: 48 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 49 | } 50 | } 51 | 52 | static void 53 | text_document_set_property (GObject *object, 54 | guint prop_id, 55 | const GValue *value, 56 | GParamSpec *pspec) 57 | { 58 | TextDocument *self = TEXT_DOCUMENT (object); 59 | 60 | switch (prop_id) 61 | { 62 | default: 63 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 64 | } 65 | } 66 | 67 | static void 68 | text_document_class_init (TextDocumentClass *klass) 69 | { 70 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 71 | 72 | object_class->finalize = text_document_finalize; 73 | object_class->get_property = text_document_get_property; 74 | object_class->set_property = text_document_set_property; 75 | } 76 | 77 | GSList * 78 | text_document_get_all_marks (TextDocument *doc) 79 | { 80 | GSList *marks; 81 | 82 | marks = g_slist_copy (doc->marks); 83 | marks = g_slist_append (marks, doc->cursor); 84 | 85 | if (doc->selection) 86 | marks = g_slist_append (marks, doc->selection); 87 | 88 | return marks; 89 | } 90 | 91 | TextMark * 92 | text_document_create_mark (TextDocument *doc, 93 | TextParagraph *paragraph, 94 | int index, 95 | TextGravity gravity) 96 | { 97 | TextMark *new; 98 | 99 | g_return_val_if_fail (TEXT_IS_DOCUMENT (doc), NULL); 100 | g_return_val_if_fail (TEXT_IS_PARAGRAPH (paragraph), NULL); 101 | 102 | new = text_mark_new (doc, paragraph, index, gravity); 103 | doc->marks = g_slist_append (doc->marks, new); 104 | 105 | return new; 106 | } 107 | 108 | TextMark * 109 | text_document_copy_mark (TextDocument *doc, 110 | TextMark *mark) 111 | { 112 | TextMark *new; 113 | 114 | g_return_val_if_fail (TEXT_IS_DOCUMENT (doc), NULL); 115 | g_return_val_if_fail (mark != NULL, NULL); 116 | 117 | new = text_mark_copy (mark); 118 | doc->marks = g_slist_append (doc->marks, new); 119 | 120 | return new; 121 | } 122 | 123 | void 124 | text_document_delete_mark (TextDocument *doc, 125 | TextMark *mark) 126 | { 127 | g_return_if_fail (TEXT_IS_DOCUMENT (doc)); 128 | g_return_if_fail (mark != NULL); 129 | 130 | doc->marks = g_slist_remove (doc->marks, mark); 131 | } 132 | 133 | void 134 | text_document_clear_mark (TextDocument *doc, 135 | TextMark **mark) 136 | { 137 | g_return_if_fail (TEXT_IS_DOCUMENT (doc)); 138 | g_return_if_fail (mark != NULL); 139 | 140 | doc->marks = g_slist_remove (doc->marks, *mark); 141 | *mark = NULL; 142 | } 143 | 144 | static void 145 | text_document_init (TextDocument *self) 146 | { 147 | self->cursor = text_mark_new (self, NULL, 0, TEXT_GRAVITY_RIGHT); 148 | } 149 | -------------------------------------------------------------------------------- /src/model/document.h: -------------------------------------------------------------------------------- 1 | /* document.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "frame.h" 17 | #include "mark.h" 18 | 19 | struct _TextDocument 20 | { 21 | GObject parent_instance; 22 | 23 | TextFrame *frame; 24 | TextMark *cursor; 25 | TextMark *selection; 26 | GSList *marks; 27 | }; 28 | 29 | G_BEGIN_DECLS 30 | 31 | #define TEXT_TYPE_DOCUMENT (text_document_get_type()) 32 | 33 | G_DECLARE_FINAL_TYPE (TextDocument, text_document, TEXT, DOCUMENT, GObject) 34 | 35 | TextDocument *text_document_new (void); 36 | 37 | TextMark *text_document_create_mark (TextDocument *doc, TextParagraph *paragraph, int index, TextGravity gravity); 38 | TextMark *text_document_copy_mark (TextDocument *doc, TextMark *mark); 39 | void text_document_delete_mark (TextDocument *doc, TextMark *mark); 40 | void text_document_clear_mark (TextDocument *doc, TextMark **mark); 41 | 42 | // TODO: Make private 43 | GSList *text_document_get_all_marks (TextDocument *doc); 44 | 45 | G_END_DECLS 46 | -------------------------------------------------------------------------------- /src/model/fragment.c: -------------------------------------------------------------------------------- 1 | /* fragment.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "fragment.h" 13 | 14 | typedef struct 15 | { 16 | int _padding; 17 | } TextFragmentPrivate; 18 | 19 | G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (TextFragment, text_fragment, TEXT_TYPE_ITEM) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | static void 29 | text_fragment_finalize (GObject *object) 30 | { 31 | TextFragment *self = (TextFragment *)object; 32 | TextFragmentPrivate *priv = text_fragment_get_instance_private (self); 33 | 34 | G_OBJECT_CLASS (text_fragment_parent_class)->finalize (object); 35 | } 36 | 37 | static void 38 | text_fragment_get_property (GObject *object, 39 | guint prop_id, 40 | GValue *value, 41 | GParamSpec *pspec) 42 | { 43 | TextFragment *self = TEXT_FRAGMENT (object); 44 | TextFragmentPrivate *priv = text_fragment_get_instance_private (self); 45 | 46 | switch (prop_id) 47 | { 48 | default: 49 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 50 | } 51 | } 52 | 53 | static void 54 | text_fragment_set_property (GObject *object, 55 | guint prop_id, 56 | const GValue *value, 57 | GParamSpec *pspec) 58 | { 59 | TextFragment *self = TEXT_FRAGMENT (object); 60 | TextFragmentPrivate *priv = text_fragment_get_instance_private (self); 61 | 62 | switch (prop_id) 63 | { 64 | default: 65 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 66 | } 67 | } 68 | 69 | const char * 70 | text_fragment_real_get_text (TextFragment *self) 71 | { 72 | TextFragmentPrivate *priv = text_fragment_get_instance_private (self); 73 | return ""; 74 | } 75 | 76 | int 77 | text_fragment_get_length (TextFragment *self) 78 | { 79 | const char *text; 80 | 81 | g_return_val_if_fail (TEXT_IS_FRAGMENT (self), -1); 82 | 83 | text = text_fragment_get_text (self); 84 | // g_print ("Length of %s is %d\n", g_type_name_from_instance (self), (int) g_utf8_strlen (text, -1)); 85 | return (int) g_utf8_strlen (text, -1); 86 | } 87 | 88 | int 89 | text_fragment_get_size_bytes (TextFragment *self) 90 | { 91 | const char *text; 92 | 93 | g_return_val_if_fail (TEXT_IS_FRAGMENT (self), -1); 94 | 95 | text = text_fragment_get_text (self); 96 | // g_print ("Size of %s is %d\n", g_type_name_from_instance (self), (int) strlen (text)); 97 | return (int) strlen (text); 98 | } 99 | 100 | const char * 101 | text_fragment_get_text (TextFragment *self) 102 | { 103 | return TEXT_FRAGMENT_CLASS (G_OBJECT_GET_CLASS (self))->get_text (self); 104 | } 105 | 106 | static void 107 | text_fragment_class_init (TextFragmentClass *klass) 108 | { 109 | klass->get_text = text_fragment_real_get_text; 110 | 111 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 112 | 113 | object_class->finalize = text_fragment_finalize; 114 | object_class->get_property = text_fragment_get_property; 115 | object_class->set_property = text_fragment_set_property; 116 | } 117 | 118 | static void 119 | text_fragment_init (TextFragment *self) 120 | { 121 | } 122 | -------------------------------------------------------------------------------- /src/model/fragment.h: -------------------------------------------------------------------------------- 1 | /* fragment.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "item.h" 17 | 18 | G_BEGIN_DECLS 19 | 20 | #define TEXT_TYPE_FRAGMENT (text_fragment_get_type()) 21 | 22 | G_DECLARE_DERIVABLE_TYPE (TextFragment, text_fragment, TEXT, FRAGMENT, TextItem) 23 | 24 | struct _TextFragmentClass 25 | { 26 | TextNodeClass parent_class; 27 | const char *(*get_text)(TextFragment *self); 28 | }; 29 | 30 | int text_fragment_get_length (TextFragment *self); 31 | const char* text_fragment_get_text (TextFragment *self); 32 | int text_fragment_get_size_bytes (TextFragment *self); 33 | 34 | G_END_DECLS 35 | -------------------------------------------------------------------------------- /src/model/frame.c: -------------------------------------------------------------------------------- 1 | /* frame.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "frame.h" 13 | 14 | typedef struct 15 | { 16 | int _padding; 17 | } TextFramePrivate; 18 | 19 | G_DEFINE_FINAL_TYPE_WITH_PRIVATE (TextFrame, text_frame, TEXT_TYPE_BLOCK) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | /** 29 | * text_frame_new: 30 | * 31 | * Create a new #TextFrame. 32 | * 33 | * Returns: (transfer full): a newly created #TextFrame 34 | */ 35 | TextFrame * 36 | text_frame_new (void) 37 | { 38 | return g_object_new (TEXT_TYPE_FRAME, NULL); 39 | } 40 | 41 | static void 42 | text_frame_finalize (GObject *object) 43 | { 44 | TextFrame *self = (TextFrame *)object; 45 | TextFramePrivate *priv = text_frame_get_instance_private (self); 46 | 47 | G_OBJECT_CLASS (text_frame_parent_class)->finalize (object); 48 | } 49 | 50 | static void 51 | text_frame_get_property (GObject *object, 52 | guint prop_id, 53 | GValue *value, 54 | GParamSpec *pspec) 55 | { 56 | TextFrame *self = TEXT_FRAME (object); 57 | 58 | switch (prop_id) 59 | { 60 | default: 61 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 62 | } 63 | } 64 | 65 | static void 66 | text_frame_set_property (GObject *object, 67 | guint prop_id, 68 | const GValue *value, 69 | GParamSpec *pspec) 70 | { 71 | TextFrame *self = TEXT_FRAME (object); 72 | 73 | switch (prop_id) 74 | { 75 | default: 76 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 77 | } 78 | } 79 | 80 | void 81 | text_frame_append_block (TextFrame *self, 82 | TextBlock *block) 83 | { 84 | g_return_if_fail (TEXT_IS_FRAME (self)); 85 | g_return_if_fail (TEXT_IS_BLOCK (block)); 86 | 87 | text_node_append_child (TEXT_NODE (self), TEXT_NODE (block)); 88 | } 89 | 90 | void 91 | text_frame_prepend_block (TextFrame *self, 92 | TextBlock *block) 93 | { 94 | g_return_if_fail (TEXT_IS_FRAME (self)); 95 | g_return_if_fail (TEXT_IS_BLOCK (block)); 96 | 97 | text_node_prepend_child (TEXT_NODE (self), TEXT_NODE (block)); 98 | } 99 | 100 | static void 101 | text_frame_class_init (TextFrameClass *klass) 102 | { 103 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 104 | 105 | object_class->finalize = text_frame_finalize; 106 | object_class->get_property = text_frame_get_property; 107 | object_class->set_property = text_frame_set_property; 108 | } 109 | 110 | static void 111 | text_frame_init (TextFrame *self) 112 | { 113 | } 114 | -------------------------------------------------------------------------------- /src/model/frame.h: -------------------------------------------------------------------------------- 1 | /* frame.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "item.h" 17 | #include "block.h" 18 | 19 | G_BEGIN_DECLS 20 | 21 | #define TEXT_TYPE_FRAME (text_frame_get_type()) 22 | 23 | G_DECLARE_DERIVABLE_TYPE (TextFrame, text_frame, TEXT, FRAME, TextBlock) 24 | 25 | struct _TextFrameClass 26 | { 27 | TextBlockClass parent_class; 28 | }; 29 | 30 | TextFrame *text_frame_new (void); 31 | void text_frame_append_block (TextFrame *self, TextBlock *block); 32 | void text_frame_prepend_block (TextFrame *self, TextBlock *block); 33 | 34 | G_END_DECLS 35 | -------------------------------------------------------------------------------- /src/model/image.c: -------------------------------------------------------------------------------- 1 | /* image.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "image.h" 13 | 14 | struct _TextImage 15 | { 16 | TextOpaque parent_instance; 17 | gchar *src; 18 | }; 19 | 20 | 21 | G_DEFINE_FINAL_TYPE (TextImage, text_image, TEXT_TYPE_OPAQUE) 22 | 23 | enum { 24 | PROP_0, 25 | PROP_SRC, 26 | N_PROPS 27 | }; 28 | 29 | static GParamSpec *properties [N_PROPS]; 30 | 31 | TextImage * 32 | text_image_new (const gchar *src) 33 | { 34 | return g_object_new (TEXT_TYPE_IMAGE, 35 | "src", src, 36 | NULL); 37 | } 38 | 39 | static void 40 | text_image_finalize (GObject *object) 41 | { 42 | TextImage *self = (TextImage *)object; 43 | 44 | G_OBJECT_CLASS (text_image_parent_class)->finalize (object); 45 | } 46 | 47 | static void 48 | text_image_get_property (GObject *object, 49 | guint prop_id, 50 | GValue *value, 51 | GParamSpec *pspec) 52 | { 53 | TextImage *self = TEXT_IMAGE (object); 54 | 55 | switch (prop_id) 56 | { 57 | case PROP_SRC: 58 | g_value_set_string (value, self->src); 59 | break; 60 | default: 61 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 62 | } 63 | } 64 | 65 | static void 66 | text_image_set_property (GObject *object, 67 | guint prop_id, 68 | const GValue *value, 69 | GParamSpec *pspec) 70 | { 71 | TextImage *self = TEXT_IMAGE (object); 72 | 73 | switch (prop_id) 74 | { 75 | case PROP_SRC: 76 | if (self->src) 77 | g_free (self->src); 78 | self->src = g_value_dup_string (value); 79 | break; 80 | default: 81 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 82 | } 83 | } 84 | 85 | static void 86 | text_image_class_init (TextImageClass *klass) 87 | { 88 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 89 | 90 | object_class->finalize = text_image_finalize; 91 | object_class->get_property = text_image_get_property; 92 | object_class->set_property = text_image_set_property; 93 | 94 | properties [PROP_SRC] 95 | = g_param_spec_string ("src", 96 | "Source", 97 | "Source", 98 | NULL, 99 | G_PARAM_READWRITE|G_PARAM_CONSTRUCT); 100 | 101 | g_object_class_install_properties (object_class, N_PROPS, properties); 102 | } 103 | 104 | static void 105 | text_image_init (TextImage *self) 106 | { 107 | } 108 | -------------------------------------------------------------------------------- /src/model/image.h: -------------------------------------------------------------------------------- 1 | /* image.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "item.h" 17 | #include "opaque.h" 18 | 19 | G_BEGIN_DECLS 20 | 21 | #define TEXT_TYPE_IMAGE (text_image_get_type()) 22 | 23 | G_DECLARE_FINAL_TYPE (TextImage, text_image, TEXT, IMAGE, TextOpaque) 24 | 25 | TextImage *text_image_new (const gchar *src); 26 | 27 | G_END_DECLS 28 | -------------------------------------------------------------------------------- /src/model/item.c: -------------------------------------------------------------------------------- 1 | /* item.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "item.h" 13 | 14 | typedef struct 15 | { 16 | TextNode *renderer; 17 | } TextItemPrivate; 18 | 19 | G_DEFINE_TYPE_WITH_PRIVATE (TextItem, text_item, TEXT_TYPE_NODE) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | /** 29 | * text_item_new: 30 | * 31 | * Create a new #TextItem. 32 | * 33 | * Returns: (transfer full): a newly created #TextItem 34 | */ 35 | TextItem * 36 | text_item_new (void) 37 | { 38 | return g_object_new (TEXT_TYPE_ITEM, NULL); 39 | } 40 | 41 | void 42 | text_item_attach (TextItem *self, 43 | TextNode *attachment) 44 | { 45 | TextItemPrivate *priv = text_item_get_instance_private (self); 46 | 47 | g_return_if_fail (!priv->renderer); 48 | 49 | priv->renderer = g_object_ref (attachment); 50 | } 51 | 52 | TextNode * 53 | text_item_get_attachment (TextItem *self) 54 | { 55 | TextItemPrivate *priv = text_item_get_instance_private (self); 56 | return TEXT_NODE (priv->renderer); 57 | } 58 | 59 | void 60 | text_item_detach (TextItem *self) 61 | { 62 | TextItemPrivate *priv = text_item_get_instance_private (self); 63 | if (priv->renderer) 64 | g_clear_object (&priv->renderer); 65 | } 66 | 67 | static void 68 | text_item_finalize (GObject *object) 69 | { 70 | TextItem *self = (TextItem *)object; 71 | 72 | G_OBJECT_CLASS (text_item_parent_class)->finalize (object); 73 | } 74 | 75 | static void 76 | text_item_get_property (GObject *object, 77 | guint prop_id, 78 | GValue *value, 79 | GParamSpec *pspec) 80 | { 81 | TextItem *self = TEXT_ITEM (object); 82 | 83 | switch (prop_id) 84 | { 85 | default: 86 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 87 | } 88 | } 89 | 90 | static void 91 | text_item_set_property (GObject *object, 92 | guint prop_id, 93 | const GValue *value, 94 | GParamSpec *pspec) 95 | { 96 | TextItem *self = TEXT_ITEM (object); 97 | 98 | switch (prop_id) 99 | { 100 | default: 101 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 102 | } 103 | } 104 | 105 | static void 106 | text_item_class_init (TextItemClass *klass) 107 | { 108 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 109 | 110 | object_class->finalize = text_item_finalize; 111 | object_class->get_property = text_item_get_property; 112 | object_class->set_property = text_item_set_property; 113 | } 114 | 115 | static void 116 | text_item_init (TextItem *self) 117 | { 118 | } 119 | -------------------------------------------------------------------------------- /src/model/item.h: -------------------------------------------------------------------------------- 1 | /* item.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include "../tree/node.h" 16 | 17 | G_BEGIN_DECLS 18 | 19 | #define TEXT_TYPE_ITEM (text_item_get_type()) 20 | 21 | G_DECLARE_DERIVABLE_TYPE (TextItem, text_item, TEXT, ITEM, TextNode) 22 | 23 | struct _TextItemClass 24 | { 25 | TextNodeClass parent_class; 26 | }; 27 | 28 | TextItem *text_item_new (void); 29 | 30 | void 31 | text_item_attach (TextItem *self, 32 | TextNode *attachment); 33 | TextNode * 34 | text_item_get_attachment (TextItem *self); 35 | 36 | void 37 | text_item_detach (TextItem *self); 38 | 39 | G_END_DECLS 40 | -------------------------------------------------------------------------------- /src/model/mark.c: -------------------------------------------------------------------------------- 1 | /* mark.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "mark.h" 13 | 14 | G_DEFINE_BOXED_TYPE (TextMark, text_mark, text_mark_copy, text_mark_free) 15 | 16 | /** 17 | * text_mark_new: 18 | * 19 | * Creates a new #TextMark. 20 | * 21 | * Returns: (transfer full): A newly created #TextMark 22 | */ 23 | TextMark * 24 | text_mark_new (struct _TextDocument *document, 25 | TextParagraph *paragraph, 26 | int index, 27 | TextGravity gravity) 28 | { 29 | TextMark *self; 30 | 31 | self = g_slice_new0 (TextMark); 32 | self->document = document; 33 | self->paragraph = paragraph; 34 | self->index = index; 35 | self->gravity = gravity; 36 | 37 | return self; 38 | } 39 | 40 | /** 41 | * text_mark_copy: 42 | * @self: a #TextMark 43 | * 44 | * Makes a deep copy of a #TextMark. 45 | * 46 | * Returns: (transfer full): A newly created #TextMark with the same 47 | * contents as @self 48 | */ 49 | TextMark * 50 | text_mark_copy (TextMark *self) 51 | { 52 | TextMark *copy; 53 | 54 | g_return_val_if_fail (self, NULL); 55 | 56 | copy = text_mark_new (self->document, self->paragraph, self->index, self->gravity); 57 | 58 | return copy; 59 | } 60 | 61 | /** 62 | * text_mark_free: 63 | * @self: a #TextMark 64 | * 65 | * Frees a #TextMark allocated using text_mark_new() 66 | * or text_mark_copy(). 67 | */ 68 | void 69 | text_mark_free (TextMark *self) 70 | { 71 | g_return_if_fail (self); 72 | 73 | // g_clear_object (&self->parent); 74 | 75 | g_slice_free (TextMark, self); 76 | } 77 | -------------------------------------------------------------------------------- /src/model/mark.h: -------------------------------------------------------------------------------- 1 | /* mark.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include "paragraph.h" 16 | 17 | G_BEGIN_DECLS 18 | 19 | #define TEXT_TYPE_MARK (text_mark_get_type ()) 20 | 21 | typedef enum 22 | { 23 | TEXT_GRAVITY_LEFT, 24 | TEXT_GRAVITY_RIGHT, 25 | } TextGravity; 26 | 27 | typedef struct _TextMark TextMark; 28 | struct _TextDocument; 29 | 30 | struct _TextMark 31 | { 32 | struct _TextDocument *document; 33 | 34 | TextParagraph *paragraph; 35 | int index; // byte index (i.e. NOT unicode) 36 | TextGravity gravity; 37 | }; 38 | 39 | GType text_mark_get_type (void) G_GNUC_CONST; 40 | TextMark *text_mark_new (struct _TextDocument *document, TextParagraph *paragraph, int index, TextGravity gravity); 41 | TextMark *text_mark_copy (TextMark *self); 42 | void text_mark_free (TextMark *self); 43 | 44 | G_DEFINE_AUTOPTR_CLEANUP_FUNC (TextMark, text_mark_free) 45 | 46 | G_END_DECLS 47 | -------------------------------------------------------------------------------- /src/model/meson.build: -------------------------------------------------------------------------------- 1 | text_engine_sources += files([ 2 | 'item.c', 3 | 'run.c', 4 | 'block.c', 5 | 'frame.c', 6 | 'paragraph.c', 7 | 'mark.c', 8 | 'document.c', 9 | 'fragment.c', 10 | 'image.c', 11 | 'opaque.c' 12 | ]) 13 | 14 | model_headers = [ 15 | 'item.h', 16 | 'run.h', 17 | 'block.h', 18 | 'frame.h', 19 | 'paragraph.h', 20 | 'mark.h', 21 | 'document.h', 22 | 'fragment.h', 23 | 'image.h', 24 | 'opaque.h' 25 | ] 26 | 27 | install_headers(model_headers, subdir : header_dir / 'model') 28 | -------------------------------------------------------------------------------- /src/model/opaque.c: -------------------------------------------------------------------------------- 1 | /* opaque.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "opaque.h" 13 | 14 | typedef struct 15 | { 16 | int _padding; 17 | } TextOpaquePrivate; 18 | 19 | G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (TextOpaque, text_opaque, TEXT_TYPE_FRAGMENT) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | static void 29 | text_opaque_finalize (GObject *object) 30 | { 31 | TextOpaque *self = (TextOpaque *)object; 32 | TextOpaquePrivate *priv = text_opaque_get_instance_private (self); 33 | 34 | G_OBJECT_CLASS (text_opaque_parent_class)->finalize (object); 35 | } 36 | 37 | static void 38 | text_opaque_get_property (GObject *object, 39 | guint prop_id, 40 | GValue *value, 41 | GParamSpec *pspec) 42 | { 43 | TextOpaque *self = TEXT_OPAQUE (object); 44 | 45 | switch (prop_id) 46 | { 47 | default: 48 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 49 | } 50 | } 51 | 52 | static void 53 | text_opaque_set_property (GObject *object, 54 | guint prop_id, 55 | const GValue *value, 56 | GParamSpec *pspec) 57 | { 58 | TextOpaque *self = TEXT_OPAQUE (object); 59 | 60 | switch (prop_id) 61 | { 62 | default: 63 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 64 | } 65 | } 66 | 67 | const char* 68 | text_opaque_get_text (TextFragment *self) 69 | { 70 | return TEXT_OPAQUE_REPLACEMENT_CHAR; 71 | } 72 | 73 | static void 74 | text_opaque_class_init (TextOpaqueClass *klass) 75 | { 76 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 77 | 78 | object_class->finalize = text_opaque_finalize; 79 | object_class->get_property = text_opaque_get_property; 80 | object_class->set_property = text_opaque_set_property; 81 | 82 | TextFragmentClass *fragment_class = TEXT_FRAGMENT_CLASS (klass); 83 | 84 | fragment_class->get_text = text_opaque_get_text; 85 | } 86 | 87 | static void 88 | text_opaque_init (TextOpaque *self) 89 | { 90 | } 91 | -------------------------------------------------------------------------------- /src/model/opaque.h: -------------------------------------------------------------------------------- 1 | /* opaque.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "fragment.h" 17 | 18 | G_BEGIN_DECLS 19 | 20 | #define TEXT_TYPE_OPAQUE (text_opaque_get_type()) 21 | 22 | G_DECLARE_DERIVABLE_TYPE (TextOpaque, text_opaque, TEXT, OPAQUE, TextFragment) 23 | 24 | struct _TextOpaqueClass 25 | { 26 | TextFragmentClass parent_class; 27 | }; 28 | 29 | // Match the unicode replacement character than GTK uses 30 | static const char TEXT_OPAQUE_REPLACEMENT_CHAR[] = { '\xEF', '\xBF', '\xBC', '\0' }; 31 | 32 | G_END_DECLS 33 | -------------------------------------------------------------------------------- /src/model/paragraph.c: -------------------------------------------------------------------------------- 1 | /* paragraph.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "paragraph.h" 13 | 14 | struct _TextParagraph 15 | { 16 | TextBlock parent_instance; 17 | }; 18 | 19 | G_DEFINE_FINAL_TYPE (TextParagraph, text_paragraph, TEXT_TYPE_BLOCK) 20 | 21 | enum { 22 | PROP_0, 23 | N_PROPS 24 | }; 25 | 26 | static GParamSpec *properties [N_PROPS]; 27 | 28 | TextParagraph * 29 | text_paragraph_new (void) 30 | { 31 | return g_object_new (TEXT_TYPE_PARAGRAPH, NULL); 32 | } 33 | 34 | static void 35 | text_paragraph_finalize (GObject *object) 36 | { 37 | TextParagraph *self = (TextParagraph *)object; 38 | 39 | G_OBJECT_CLASS (text_paragraph_parent_class)->finalize (object); 40 | } 41 | 42 | static void 43 | text_paragraph_get_property (GObject *object, 44 | guint prop_id, 45 | GValue *value, 46 | GParamSpec *pspec) 47 | { 48 | TextParagraph *self = TEXT_PARAGRAPH (object); 49 | 50 | switch (prop_id) 51 | { 52 | default: 53 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 54 | } 55 | } 56 | 57 | static void 58 | text_paragraph_set_property (GObject *object, 59 | guint prop_id, 60 | const GValue *value, 61 | GParamSpec *pspec) 62 | { 63 | TextParagraph *self = TEXT_PARAGRAPH (object); 64 | 65 | switch (prop_id) 66 | { 67 | default: 68 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 69 | } 70 | } 71 | 72 | void 73 | text_paragraph_append_fragment (TextParagraph *self, 74 | TextFragment *fragment) 75 | { 76 | g_return_if_fail (TEXT_IS_PARAGRAPH (self)); 77 | g_return_if_fail (TEXT_IS_FRAGMENT (fragment)); 78 | 79 | text_node_append_child (TEXT_NODE (self), TEXT_NODE (fragment)); 80 | } 81 | 82 | /** 83 | * text_paragraph_get_text: 84 | * 85 | * Returns a duplicate string containing the contents of 86 | * this paragraph. The contents is valid as of the instant 87 | * the method is called and will not be updated. It is the 88 | * caller's responsibility to free the returned string. 89 | * 90 | * @self: The `TextParagraph` instance. 91 | * 92 | * Returns: A pointer to the text content of this paragraph 93 | */ 94 | char * 95 | text_paragraph_get_text (TextParagraph *self) 96 | { 97 | TextNode *child; 98 | GString *str; 99 | 100 | g_return_val_if_fail (TEXT_IS_PARAGRAPH (self), NULL); 101 | 102 | str = g_string_new (""); 103 | 104 | for (child = text_node_get_first_child (TEXT_NODE (self)); 105 | child != NULL; 106 | child = text_node_get_next (child)) 107 | { 108 | const gchar *run_text; 109 | 110 | g_assert (TEXT_IS_FRAGMENT (child)); 111 | 112 | run_text = text_fragment_get_text (TEXT_FRAGMENT (child)); 113 | g_string_append (str, run_text); 114 | } 115 | 116 | return g_string_free (str, FALSE); 117 | } 118 | 119 | int 120 | text_paragraph_get_length (TextParagraph *self) 121 | { 122 | TextNode *child; 123 | int length; 124 | 125 | g_return_val_if_fail (TEXT_IS_PARAGRAPH (self), -1); 126 | 127 | length = 0; 128 | 129 | for (child = text_node_get_first_child (TEXT_NODE (self)); 130 | child != NULL; 131 | child = text_node_get_next (child)) 132 | { 133 | length += text_fragment_get_length (TEXT_FRAGMENT (child)); 134 | } 135 | 136 | return length; 137 | } 138 | 139 | int 140 | text_paragraph_get_size_bytes (TextParagraph *self) 141 | { 142 | TextNode *child; 143 | int length; 144 | 145 | g_return_val_if_fail (TEXT_IS_PARAGRAPH (self), -1); 146 | 147 | length = 0; 148 | 149 | for (child = text_node_get_first_child (TEXT_NODE (self)); 150 | child != NULL; 151 | child = text_node_get_next (child)) 152 | { 153 | length += (int) strlen (text_fragment_get_text ((TEXT_FRAGMENT (child)))); 154 | } 155 | 156 | return length; 157 | } 158 | 159 | TextFragment * 160 | text_paragraph_get_item_at_index (TextParagraph *self, 161 | int byte_index, 162 | int *starting_index) 163 | { 164 | TextNode *child; 165 | int length; 166 | 167 | length = 0; 168 | 169 | g_return_val_if_fail (TEXT_IS_PARAGRAPH (self), NULL); 170 | 171 | if (byte_index == 0) 172 | { 173 | TextNode *first; 174 | first = text_node_get_first_child (TEXT_NODE (self)); 175 | 176 | if (starting_index) 177 | *starting_index = 0; 178 | return TEXT_FRAGMENT (first); 179 | } 180 | 181 | for (child = text_node_get_first_child (TEXT_NODE (self)); 182 | child != NULL; 183 | child = text_node_get_next (child)) 184 | { 185 | int delta_size; 186 | g_assert (TEXT_IS_FRAGMENT (child)); 187 | delta_size = text_fragment_get_size_bytes (TEXT_FRAGMENT (child)); 188 | 189 | // Index is considered part of a run if it is immediately 190 | // after the last character in the run. For example: 191 | // There is a cursor position at the end of a run 192 | // 193 | // `Once upon a time there was a little dog, ` 194 | // ^ 195 | // this index is part of the run / 196 | // 197 | if (length < byte_index && byte_index <= length + delta_size) 198 | { 199 | if (starting_index) 200 | *starting_index = length; 201 | return TEXT_FRAGMENT (child); 202 | } 203 | 204 | length += delta_size; 205 | } 206 | 207 | g_critical ("Invalid index: %d passed to text_paragraph_get_item_at_index ()\n", byte_index); 208 | 209 | if (starting_index) 210 | *starting_index = -1; 211 | return NULL; 212 | } 213 | 214 | static void 215 | text_paragraph_class_init (TextParagraphClass *klass) 216 | { 217 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 218 | 219 | object_class->finalize = text_paragraph_finalize; 220 | object_class->get_property = text_paragraph_get_property; 221 | object_class->set_property = text_paragraph_set_property; 222 | } 223 | 224 | static void 225 | text_paragraph_init (TextParagraph *self) 226 | { 227 | } 228 | -------------------------------------------------------------------------------- /src/model/paragraph.h: -------------------------------------------------------------------------------- 1 | /* paragraph.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "item.h" 17 | #include "block.h" 18 | #include "run.h" 19 | 20 | G_BEGIN_DECLS 21 | 22 | #define TEXT_TYPE_PARAGRAPH (text_paragraph_get_type()) 23 | 24 | G_DECLARE_FINAL_TYPE (TextParagraph, text_paragraph, TEXT, PARAGRAPH, TextBlock) 25 | 26 | TextParagraph *text_paragraph_new (void); 27 | void text_paragraph_append_fragment (TextParagraph *self, TextFragment *fragment); 28 | TextFragment *text_paragraph_get_item_at_index (TextParagraph *self, int byte_index, int *starting_index); 29 | int text_paragraph_get_length (TextParagraph *self); 30 | int text_paragraph_get_size_bytes (TextParagraph *self); 31 | char *text_paragraph_get_text (TextParagraph *self); 32 | 33 | G_END_DECLS 34 | -------------------------------------------------------------------------------- /src/model/run.c: -------------------------------------------------------------------------------- 1 | /* run.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "run.h" 13 | 14 | struct _TextRun 15 | { 16 | TextFragment parent_instance; 17 | char *text; 18 | gboolean is_bold; 19 | gboolean is_italic; 20 | gboolean is_underline; 21 | }; 22 | 23 | 24 | G_DEFINE_FINAL_TYPE (TextRun, text_run, TEXT_TYPE_FRAGMENT) 25 | 26 | enum { 27 | PROP_0, 28 | PROP_TEXT, 29 | N_PROPS 30 | }; 31 | 32 | static GParamSpec *properties [N_PROPS]; 33 | 34 | TextRun * 35 | text_run_new (const gchar *text) 36 | { 37 | return g_object_new (TEXT_TYPE_RUN, 38 | "text", text, 39 | NULL); 40 | } 41 | 42 | static void 43 | text_run_finalize (GObject *object) 44 | { 45 | TextRun *self = (TextRun *)object; 46 | 47 | G_OBJECT_CLASS (text_run_parent_class)->finalize (object); 48 | } 49 | 50 | static void 51 | text_run_get_property (GObject *object, 52 | guint prop_id, 53 | GValue *value, 54 | GParamSpec *pspec) 55 | { 56 | TextRun *self = TEXT_RUN (object); 57 | 58 | switch (prop_id) 59 | { 60 | case PROP_TEXT: 61 | g_value_set_string (value, self->text); 62 | break; 63 | default: 64 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 65 | } 66 | } 67 | 68 | static void 69 | text_run_set_property (GObject *object, 70 | guint prop_id, 71 | const GValue *value, 72 | GParamSpec *pspec) 73 | { 74 | TextRun *self = TEXT_RUN (object); 75 | 76 | switch (prop_id) 77 | { 78 | case PROP_TEXT: 79 | if (self->text) 80 | g_free (self->text); 81 | self->text = g_value_dup_string (value); 82 | break; 83 | default: 84 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 85 | } 86 | } 87 | 88 | gboolean 89 | text_run_get_style_bold (TextRun *self) 90 | { 91 | return self->is_bold; 92 | } 93 | 94 | void 95 | text_run_set_style_bold (TextRun *self, 96 | gboolean is_bold) 97 | { 98 | self->is_bold = is_bold; 99 | } 100 | 101 | gboolean 102 | text_run_get_style_italic (TextRun *self) 103 | { 104 | return self->is_italic; 105 | } 106 | 107 | void 108 | text_run_set_style_italic (TextRun *self, 109 | gboolean is_italic) 110 | { 111 | self->is_italic = is_italic; 112 | } 113 | 114 | gboolean 115 | text_run_get_style_underline (TextRun *self) 116 | { 117 | return self->is_underline; 118 | } 119 | 120 | void 121 | text_run_set_style_underline (TextRun *self, 122 | gboolean is_underline) 123 | { 124 | self->is_underline = is_underline; 125 | } 126 | 127 | const char* 128 | text_run_get_text (TextFragment *self) 129 | { 130 | return (TEXT_RUN (self))->text; 131 | } 132 | 133 | static void 134 | text_run_class_init (TextRunClass *klass) 135 | { 136 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 137 | 138 | object_class->finalize = text_run_finalize; 139 | object_class->get_property = text_run_get_property; 140 | object_class->set_property = text_run_set_property; 141 | 142 | properties [PROP_TEXT] 143 | = g_param_spec_string ("text", 144 | "Text", 145 | "Text", 146 | NULL, 147 | G_PARAM_READWRITE|G_PARAM_CONSTRUCT); 148 | 149 | g_object_class_install_properties (object_class, N_PROPS, properties); 150 | 151 | TextFragmentClass *fragment_class = TEXT_FRAGMENT_CLASS (klass); 152 | 153 | fragment_class->get_text = text_run_get_text; 154 | } 155 | 156 | static void 157 | text_run_init (TextRun *self) 158 | { 159 | } 160 | -------------------------------------------------------------------------------- /src/model/run.h: -------------------------------------------------------------------------------- 1 | /* run.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "item.h" 17 | #include "fragment.h" 18 | 19 | G_BEGIN_DECLS 20 | 21 | #define TEXT_TYPE_RUN (text_run_get_type()) 22 | 23 | G_DECLARE_FINAL_TYPE (TextRun, text_run, TEXT, RUN, TextFragment) 24 | 25 | TextRun *text_run_new (const gchar *text); 26 | 27 | gboolean text_run_get_style_bold (TextRun *self); 28 | void text_run_set_style_bold (TextRun *self, gboolean is_bold); 29 | 30 | gboolean text_run_get_style_italic (TextRun *self); 31 | void text_run_set_style_italic (TextRun *self, gboolean is_italic); 32 | 33 | gboolean text_run_get_style_underline (TextRun *self); 34 | void text_run_set_style_underline (TextRun *self, gboolean is_underline); 35 | 36 | G_END_DECLS 37 | -------------------------------------------------------------------------------- /src/text-engine-version.h.in: -------------------------------------------------------------------------------- 1 | /* text-engine-version.h.in 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #if !defined(TEXT_ENGINE_INSIDE) && !defined(TEXT_ENGINE_COMPILATION) 15 | # error "Only can be included directly." 16 | #endif 17 | 18 | /** 19 | * SECTION:text_engineversion 20 | * @short_description: text-engine version checking 21 | * 22 | * text-engine provides macros to check the version of the library 23 | * at compile-time 24 | */ 25 | 26 | /** 27 | * TEXT_ENGINE_MAJOR_VERSION: 28 | * 29 | * text-engine major version component (e.g. 1 if %TEXT_ENGINE_VERSION is 1.2.3) 30 | */ 31 | #define TEXT_ENGINE_MAJOR_VERSION (@MAJOR_VERSION@) 32 | 33 | /** 34 | * TEXT_ENGINE_MINOR_VERSION: 35 | * 36 | * text-engine minor version component (e.g. 2 if %TEXT_ENGINE_VERSION is 1.2.3) 37 | */ 38 | #define TEXT_ENGINE_MINOR_VERSION (@MINOR_VERSION@) 39 | 40 | /** 41 | * TEXT_ENGINE_MICRO_VERSION: 42 | * 43 | * text-engine micro version component (e.g. 3 if %TEXT_ENGINE_VERSION is 1.2.3) 44 | */ 45 | #define TEXT_ENGINE_MICRO_VERSION (@MICRO_VERSION@) 46 | 47 | /** 48 | * TEXT_ENGINE_VERSION 49 | * 50 | * text-engine version. 51 | */ 52 | #define TEXT_ENGINE_VERSION (@VERSION@) 53 | 54 | /** 55 | * TEXT_ENGINE_VERSION_S: 56 | * 57 | * text-engine version, encoded as a string, useful for printing and 58 | * concatenation. 59 | */ 60 | #define TEXT_ENGINE_VERSION_S "@VERSION@" 61 | 62 | #define TEXT_ENGINE_ENCODE_VERSION(major,minor,micro) \ 63 | ((major) << 24 | (minor) << 16 | (micro) << 8) 64 | 65 | /** 66 | * TEXT_ENGINE_VERSION_HEX: 67 | * 68 | * text-engine version, encoded as an hexadecimal number, useful for 69 | * integer comparisons. 70 | */ 71 | #define TEXT_ENGINE_VERSION_HEX \ 72 | (TEXT_ENGINE_ENCODE_VERSION (TEXT_ENGINE_MAJOR_VERSION, TEXT_ENGINE_MINOR_VERSION, TEXT_ENGINE_MICRO_VERSION)) 73 | 74 | /** 75 | * TEXT_ENGINE_CHECK_VERSION: 76 | * @major: required major version 77 | * @minor: required minor version 78 | * @micro: required micro version 79 | * 80 | * Compile-time version checking. Evaluates to %TRUE if the version 81 | * of text-engine is greater than the required one. 82 | */ 83 | #define TEXT_ENGINE_CHECK_VERSION(major,minor,micro) \ 84 | (TEXT_ENGINE_MAJOR_VERSION > (major) || \ 85 | (TEXT_ENGINE_MAJOR_VERSION == (major) && TEXT_ENGINE_MINOR_VERSION > (minor)) || \ 86 | (TEXT_ENGINE_MAJOR_VERSION == (major) && TEXT_ENGINE_MINOR_VERSION == (minor) && \ 87 | TEXT_ENGINE_MICRO_VERSION >= (micro))) 88 | -------------------------------------------------------------------------------- /src/text-engine.c: -------------------------------------------------------------------------------- 1 | /* text-engine.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "text-engine.h" 13 | 14 | #include "ui/inspector.h" 15 | 16 | #include 17 | 18 | void 19 | text_engine_init () 20 | { 21 | GdkDisplay *display; 22 | GtkCssProvider *provider; 23 | 24 | // Add a GTK Inspector page for debugging documents 25 | if (g_io_extension_point_lookup ("gtk-inspector-page")) 26 | g_io_extension_point_implement ("gtk-inspector-page", 27 | TEXT_TYPE_INSPECTOR, 28 | "text-engine", 29 | 10); 30 | 31 | // Add CSS Provider for internal stylesheet 32 | display = gdk_display_get_default (); 33 | provider = gtk_css_provider_new (); 34 | gtk_css_provider_load_from_resource (provider, "/com/mattjakeman/TextEngine/style.css"); 35 | gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider), 36 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); 37 | } 38 | -------------------------------------------------------------------------------- /src/text-engine.h: -------------------------------------------------------------------------------- 1 | /* text-engine.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | G_BEGIN_DECLS 17 | 18 | #define TEXT_ENGINE_INSIDE 19 | # include "text-engine-version.h" 20 | #undef TEXT_ENGINE_INSIDE 21 | 22 | void text_engine_init (); 23 | 24 | G_END_DECLS 25 | -------------------------------------------------------------------------------- /src/tree/README.md: -------------------------------------------------------------------------------- 1 | # Tree 2 | A flexible tree/graph implementation. Used by the model 3 | and layout stages to create and update intermediary 4 | trees in the rendering process. 5 | -------------------------------------------------------------------------------- /src/tree/meson.build: -------------------------------------------------------------------------------- 1 | text_engine_sources += files([ 2 | 'node.c', 3 | ]) 4 | 5 | tree_headers = [ 6 | 'node.h', 7 | ] 8 | 9 | install_headers(tree_headers, subdir : header_dir / 'tree') 10 | -------------------------------------------------------------------------------- /src/tree/node.c: -------------------------------------------------------------------------------- 1 | /* node.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "node.h" 13 | 14 | typedef struct 15 | { 16 | TextNode *parent; 17 | TextNode *prev; 18 | TextNode *next; 19 | 20 | TextNode *first_child; 21 | TextNode *last_child; 22 | int n_children; 23 | } TextNodePrivate; 24 | 25 | G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (TextNode, text_node, G_TYPE_OBJECT) 26 | 27 | enum { 28 | PROP_0, 29 | N_PROPS 30 | }; 31 | 32 | static GParamSpec *properties [N_PROPS]; 33 | 34 | /** 35 | * text_node_new: 36 | * 37 | * Create a new #TextNode. 38 | * 39 | * Returns: (transfer full): a newly created #TextNode 40 | */ 41 | TextNode * 42 | text_node_new (void) 43 | { 44 | return g_object_new (TEXT_TYPE_NODE, NULL); 45 | } 46 | 47 | static void 48 | text_node_dispose (GObject *object) 49 | { 50 | TextNode *iter; 51 | 52 | TextNode *self = (TextNode *)object; 53 | TextNodePrivate *priv = text_node_get_instance_private (self); 54 | 55 | for (iter = text_node_get_first_child (self); 56 | iter != NULL; 57 | iter = text_node_get_next (iter)) 58 | { 59 | g_object_unref (iter); 60 | } 61 | 62 | priv->first_child = NULL; 63 | priv->last_child = NULL; 64 | 65 | G_OBJECT_CLASS (text_node_parent_class)->dispose (object); 66 | } 67 | 68 | static void 69 | text_node_finalize (GObject *object) 70 | { 71 | TextNode *self = (TextNode *)object; 72 | TextNodePrivate *priv = text_node_get_instance_private (self); 73 | 74 | G_OBJECT_CLASS (text_node_parent_class)->finalize (object); 75 | } 76 | 77 | static void 78 | text_node_get_property (GObject *object, 79 | guint prop_id, 80 | GValue *value, 81 | GParamSpec *pspec) 82 | { 83 | TextNode *self = TEXT_NODE (object); 84 | 85 | switch (prop_id) 86 | { 87 | default: 88 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 89 | } 90 | } 91 | 92 | static void 93 | text_node_set_property (GObject *object, 94 | guint prop_id, 95 | const GValue *value, 96 | GParamSpec *pspec) 97 | { 98 | TextNode *self = TEXT_NODE (object); 99 | 100 | switch (prop_id) 101 | { 102 | default: 103 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 104 | } 105 | } 106 | 107 | TextNode * 108 | text_node_get_first_child (TextNode *self) 109 | { 110 | TextNodePrivate *priv = text_node_get_instance_private (self); 111 | return priv->first_child; 112 | } 113 | 114 | TextNode * 115 | text_node_get_last_child (TextNode *self) 116 | { 117 | TextNodePrivate *priv = text_node_get_instance_private (self); 118 | return priv->last_child; 119 | } 120 | 121 | TextNode * 122 | text_node_get_previous (TextNode *self) 123 | { 124 | TextNodePrivate *priv = text_node_get_instance_private (self); 125 | return priv->prev; 126 | } 127 | 128 | TextNode * 129 | text_node_get_next (TextNode *self) 130 | { 131 | TextNodePrivate *priv = text_node_get_instance_private (self); 132 | return priv->next; 133 | } 134 | 135 | TextNode * 136 | text_node_get_parent (TextNode *self) 137 | { 138 | TextNodePrivate *priv = text_node_get_instance_private (self); 139 | return priv->parent; 140 | } 141 | 142 | int 143 | text_node_get_num_children (TextNode *self) 144 | { 145 | TextNodePrivate *priv = text_node_get_instance_private (self); 146 | return priv->n_children; 147 | } 148 | 149 | static void 150 | _insert_between (TextNode *parent, 151 | TextNode *node, 152 | TextNode *before, 153 | TextNode *after) 154 | { 155 | TextNodePrivate *node_priv, *before_priv, *after_priv, *parent_priv; 156 | 157 | g_assert (node != NULL); 158 | g_assert (before != NULL); 159 | g_assert (after != NULL); 160 | g_assert (parent != NULL); 161 | 162 | node_priv = text_node_get_instance_private (node); 163 | before_priv = text_node_get_instance_private (before); 164 | after_priv = text_node_get_instance_private (after); 165 | parent_priv = text_node_get_instance_private (parent); 166 | 167 | node_priv->prev = before; 168 | before_priv->next = node; 169 | 170 | node_priv->next = after; 171 | after_priv->prev = node; 172 | 173 | parent_priv->n_children++; 174 | node_priv->parent = parent; 175 | } 176 | 177 | static int 178 | _get_index_of (TextNode *self, 179 | TextNode *child) 180 | { 181 | TextNode *iter; 182 | int index; 183 | 184 | index = 0; 185 | 186 | for (iter = text_node_get_first_child (self); 187 | iter != NULL; 188 | iter = text_node_get_next (iter)) 189 | { 190 | if (iter == child) 191 | return index; 192 | 193 | index++; 194 | } 195 | 196 | return -1; 197 | } 198 | 199 | void 200 | text_node_insert_child (TextNode *self, 201 | TextNode *child, 202 | int index) 203 | { 204 | int cmp_index; 205 | TextNode *before, *after, *iter; 206 | TextNodePrivate *priv, *child_priv, *before_priv, *after_priv; 207 | 208 | priv = text_node_get_instance_private (self); 209 | child_priv = text_node_get_instance_private (child); 210 | 211 | g_assert (index >= 0 && index <= priv->n_children); 212 | 213 | g_object_ref_sink (child); 214 | 215 | // No children 216 | if (priv->n_children == 0) 217 | { 218 | priv->first_child = child; 219 | priv->last_child = child; 220 | priv->n_children = 1; 221 | 222 | // TODO: Weak reference? 223 | child_priv->parent = self; 224 | return; 225 | } 226 | 227 | // Prepend 228 | if (index == 0) 229 | { 230 | after = priv->first_child; 231 | 232 | after_priv = text_node_get_instance_private (after); 233 | 234 | after_priv->prev = child; 235 | child_priv->next = after; 236 | child_priv->prev = NULL; 237 | 238 | priv->first_child = child; 239 | priv->n_children++; 240 | 241 | // TODO: Weak reference? 242 | child_priv->parent = self; 243 | return; 244 | } 245 | 246 | // Append 247 | if (index == priv->n_children) 248 | { 249 | before = priv->last_child; 250 | 251 | before_priv = text_node_get_instance_private (before); 252 | 253 | before_priv->next = child; 254 | child_priv->prev = before; 255 | child_priv->next = NULL; 256 | 257 | priv->last_child = child; 258 | priv->n_children++; 259 | 260 | // TODO: Weak reference? 261 | child_priv->parent = self; 262 | return; 263 | } 264 | 265 | // Insert (At Index) 266 | cmp_index = 0; 267 | iter = text_node_get_first_child (self); 268 | 269 | while (++cmp_index < index) { 270 | iter = text_node_get_next (iter); 271 | g_assert (iter != NULL); 272 | } 273 | 274 | // Insert between index-1 and index 275 | _insert_between (self, child, iter, text_node_get_next (iter)); 276 | } 277 | 278 | void 279 | text_node_prepend_child (TextNode *self, 280 | TextNode *child) 281 | { 282 | text_node_insert_child (self, child, 0); 283 | } 284 | 285 | void 286 | text_node_append_child (TextNode *self, 287 | TextNode *child) 288 | { 289 | TextNodePrivate *priv = text_node_get_instance_private (self); 290 | text_node_insert_child (self, child, priv->n_children); 291 | } 292 | 293 | void 294 | text_node_insert_child_before (TextNode *self, 295 | TextNode *child, 296 | TextNode *compare) 297 | { 298 | int index; 299 | 300 | index = _get_index_of (self, compare); 301 | 302 | if (index == -1) 303 | { 304 | g_critical ("Provided compare node is not a child of this text node."); 305 | return; 306 | } 307 | 308 | if ((index - 1) == 0) 309 | { 310 | g_object_ref_sink (child); 311 | text_node_prepend_child (self, child); 312 | return; 313 | } 314 | 315 | text_node_insert_child (self, child, index); 316 | } 317 | 318 | void 319 | text_node_insert_child_after (TextNode *self, 320 | TextNode *child, 321 | TextNode *compare) 322 | { 323 | int index; 324 | 325 | index = _get_index_of (self, compare); 326 | 327 | if (index == -1) 328 | { 329 | g_critical ("Provided compare node is not a child of this text node."); 330 | return; 331 | } 332 | 333 | text_node_insert_child (self, child, index+1); 334 | } 335 | 336 | TextNode * 337 | text_node_unparent_child (TextNode *self, 338 | TextNode *child) 339 | { 340 | TextNode *iter; 341 | TextNodePrivate *iter_priv; 342 | TextNodePrivate *other_priv; 343 | TextNodePrivate *parent_priv; 344 | 345 | g_return_val_if_fail (child != NULL, NULL); 346 | g_return_val_if_fail (TEXT_IS_NODE (child), NULL); 347 | g_return_val_if_fail (TEXT_IS_NODE (self), NULL); 348 | 349 | for (iter = text_node_get_first_child (self); 350 | iter != NULL; 351 | iter = text_node_get_next (iter)) 352 | { 353 | if (iter != child) 354 | continue; 355 | 356 | iter_priv = text_node_get_instance_private (iter); 357 | parent_priv = text_node_get_instance_private (self); 358 | 359 | if (iter_priv->prev) { 360 | other_priv = text_node_get_instance_private (iter_priv->prev); 361 | other_priv->next = iter_priv->next; 362 | } else { 363 | // we are the first child 364 | parent_priv->first_child = iter_priv->next; 365 | } 366 | 367 | if (iter_priv->next) { 368 | other_priv = text_node_get_instance_private (iter_priv->next); 369 | other_priv->prev = iter_priv->prev; 370 | } else { 371 | // we are the last child 372 | parent_priv->last_child = iter_priv->prev; 373 | } 374 | 375 | parent_priv->n_children--; 376 | 377 | return iter; 378 | } 379 | 380 | return NULL; 381 | } 382 | 383 | TextNode * 384 | text_node_unparent (TextNode *self) 385 | { 386 | TextNode *parent; 387 | 388 | g_return_val_if_fail (self != NULL, NULL); 389 | g_return_val_if_fail (TEXT_IS_NODE (self), NULL); 390 | 391 | parent = text_node_get_parent (self); 392 | 393 | if (parent == NULL) 394 | return self; 395 | else 396 | return text_node_unparent_child (parent, self); 397 | } 398 | 399 | void 400 | text_node_delete_child (TextNode *self, 401 | TextNode *child) 402 | { 403 | g_return_if_fail (self != NULL); 404 | g_return_if_fail (child != NULL); 405 | g_return_if_fail (TEXT_IS_NODE (self)); 406 | g_return_if_fail (TEXT_IS_NODE (child)); 407 | 408 | if (text_node_unparent_child (self, child)) 409 | g_object_unref (child); 410 | } 411 | 412 | void 413 | text_node_delete (TextNode *self) 414 | { 415 | g_return_if_fail (self != NULL); 416 | g_return_if_fail (TEXT_IS_NODE (self)); 417 | 418 | if (text_node_unparent (self)) 419 | g_object_unref (self); 420 | } 421 | 422 | void 423 | text_node_clear_child (TextNode *self, 424 | TextNode **child) 425 | { 426 | g_return_if_fail (self != NULL); 427 | g_return_if_fail (child != NULL); 428 | g_return_if_fail (TEXT_IS_NODE (self)); 429 | g_return_if_fail (TEXT_IS_NODE (*child)); 430 | 431 | text_node_delete_child (self, *child); 432 | *child = NULL; 433 | } 434 | 435 | void 436 | text_node_clear (TextNode **self) 437 | { 438 | g_return_if_fail (self != NULL); 439 | g_return_if_fail (TEXT_IS_NODE (*self)); 440 | 441 | text_node_delete (*self); 442 | *self = NULL; 443 | } 444 | 445 | static void 446 | text_node_class_init (TextNodeClass *klass) 447 | { 448 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 449 | 450 | object_class->finalize = text_node_finalize; 451 | object_class->dispose = text_node_dispose; 452 | object_class->get_property = text_node_get_property; 453 | object_class->set_property = text_node_set_property; 454 | } 455 | 456 | static void 457 | text_node_init (TextNode *self) 458 | { 459 | TextNodePrivate *priv = text_node_get_instance_private (self); 460 | priv->first_child = NULL; 461 | priv->last_child = NULL; 462 | priv->n_children = 0; 463 | } 464 | -------------------------------------------------------------------------------- /src/tree/node.h: -------------------------------------------------------------------------------- 1 | /* node.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | G_BEGIN_DECLS 17 | 18 | #define TEXT_TYPE_NODE (text_node_get_type()) 19 | 20 | G_DECLARE_DERIVABLE_TYPE (TextNode, text_node, TEXT, NODE, GObject) 21 | 22 | struct _TextNodeClass 23 | { 24 | GObjectClass parent_class; 25 | }; 26 | 27 | // Implementors Only 28 | TextNode *text_node_get_parent (TextNode *self); 29 | TextNode *text_node_get_next (TextNode *self); 30 | TextNode *text_node_get_previous (TextNode *self); 31 | TextNode *text_node_get_first_child (TextNode *self); 32 | TextNode *text_node_get_last_child (TextNode *self); 33 | int text_node_get_num_children (TextNode *self); 34 | 35 | void text_node_insert_child (TextNode *self, TextNode *child, int index); 36 | void text_node_prepend_child (TextNode *self, TextNode *child); 37 | void text_node_append_child (TextNode *self, TextNode *child); 38 | void text_node_insert_child_before (TextNode *self, TextNode *child, TextNode *compare); 39 | void text_node_insert_child_after (TextNode *self, TextNode *child, TextNode *compare); 40 | 41 | TextNode *text_node_unparent (TextNode *self); 42 | TextNode *text_node_unparent_child (TextNode *self, TextNode *child); 43 | void text_node_delete (TextNode *self); 44 | void text_node_delete_child (TextNode *self, TextNode *child); 45 | void text_node_clear (TextNode **self); 46 | void text_node_clear_child (TextNode *self, TextNode **child); 47 | 48 | G_END_DECLS 49 | -------------------------------------------------------------------------------- /src/ui/display.h: -------------------------------------------------------------------------------- 1 | /* display.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | #include "../model/frame.h" 17 | #include "../model/document.h" 18 | 19 | G_BEGIN_DECLS 20 | 21 | #define TEXT_TYPE_DISPLAY (text_display_get_type()) 22 | 23 | G_DECLARE_FINAL_TYPE (TextDisplay, text_display, TEXT, DISPLAY, GtkWidget) 24 | 25 | TextDisplay *text_display_new (TextDocument *document); 26 | 27 | G_END_DECLS 28 | -------------------------------------------------------------------------------- /src/ui/inspector.c: -------------------------------------------------------------------------------- 1 | /* inspector.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include "inspector.h" 13 | 14 | #include "display.h" 15 | #include "../model/document.h" 16 | #include "../model/run.h" 17 | #include "../model/image.h" 18 | 19 | struct _TextInspector 20 | { 21 | GtkWidget parent_instance; 22 | 23 | GObject *object; 24 | TextDocument *document; 25 | 26 | GtkWidget *vbox; 27 | GtkWidget *colview; 28 | }; 29 | 30 | G_DEFINE_FINAL_TYPE (TextInspector, text_inspector, GTK_TYPE_WIDGET) 31 | 32 | enum { 33 | PROP_0, 34 | PROP_TITLE, 35 | PROP_OBJECT, 36 | N_PROPS 37 | }; 38 | 39 | static GParamSpec *properties [N_PROPS]; 40 | 41 | #define TITLE "Text Engine" 42 | 43 | static void populate_data_from_frame (TextInspector *inspector); 44 | 45 | TextInspector * 46 | text_inspector_new (void) 47 | { 48 | return g_object_new (TEXT_TYPE_INSPECTOR, NULL); 49 | } 50 | 51 | static void 52 | text_inspector_finalize (GObject *object) 53 | { 54 | TextInspector *self = (TextInspector *)object; 55 | 56 | gtk_widget_unparent (self->vbox); 57 | 58 | G_OBJECT_CLASS (text_inspector_parent_class)->finalize (object); 59 | } 60 | 61 | static void 62 | text_inspector_get_property (GObject *object, 63 | guint prop_id, 64 | GValue *value, 65 | GParamSpec *pspec) 66 | { 67 | TextInspector *self = TEXT_INSPECTOR (object); 68 | 69 | switch (prop_id) 70 | { 71 | case PROP_TITLE: 72 | g_value_set_string (value, TITLE); 73 | break; 74 | case PROP_OBJECT: 75 | g_value_set_object (value, self->object); 76 | break; 77 | default: 78 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 79 | } 80 | } 81 | 82 | static void 83 | text_inspector_set_property (GObject *object, 84 | guint prop_id, 85 | const GValue *value, 86 | GParamSpec *pspec) 87 | { 88 | TextInspector *self = TEXT_INSPECTOR (object); 89 | 90 | switch (prop_id) 91 | { 92 | case PROP_OBJECT: 93 | self->object = g_value_get_object (value); 94 | 95 | if (TEXT_IS_DISPLAY (self->object)) 96 | { 97 | TextDocument *document; 98 | g_object_get (self->object, "document", &document, NULL); 99 | self->document = document; 100 | populate_data_from_frame (self); 101 | } 102 | break; 103 | default: 104 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 105 | } 106 | } 107 | 108 | GListModel * 109 | create_list_model (TextItem *item) 110 | { 111 | GListStore *store; 112 | TextNode *node; 113 | 114 | if (TEXT_IS_FRAGMENT (item)) 115 | return NULL; 116 | 117 | store = g_list_store_new (TEXT_TYPE_ITEM); 118 | 119 | for (node = text_node_get_first_child (TEXT_NODE (item)); 120 | node != NULL; 121 | node = text_node_get_next (node)) 122 | { 123 | g_assert (TEXT_IS_ITEM (node)); 124 | 125 | g_list_store_append (store, node); 126 | } 127 | 128 | return G_LIST_MODEL (store); 129 | } 130 | 131 | static void 132 | populate_data_from_frame (TextInspector *self) 133 | { 134 | GtkTreeListModel *tree_model; 135 | GtkSingleSelection *selection_model; 136 | GListStore *root; 137 | 138 | g_return_if_fail (TEXT_IS_INSPECTOR (self)); 139 | g_return_if_fail (TEXT_IS_DOCUMENT (self->document)); 140 | g_return_if_fail (TEXT_IS_FRAME (self->document->frame)); 141 | 142 | root = g_list_store_new (TEXT_TYPE_ITEM); 143 | g_list_store_append (root, TEXT_ITEM (self->document->frame)); 144 | 145 | tree_model = gtk_tree_list_model_new (G_LIST_MODEL (root), FALSE, TRUE, 146 | (GtkTreeListModelCreateModelFunc) create_list_model, 147 | NULL, NULL); 148 | 149 | 150 | selection_model = gtk_single_selection_new (G_LIST_MODEL (tree_model)); 151 | 152 | gtk_column_view_set_model (GTK_COLUMN_VIEW (self->colview), 153 | GTK_SELECTION_MODEL (selection_model)); 154 | } 155 | 156 | static void 157 | text_inspector_class_init (TextInspectorClass *klass) 158 | { 159 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 160 | 161 | object_class->finalize = text_inspector_finalize; 162 | object_class->get_property = text_inspector_get_property; 163 | object_class->set_property = text_inspector_set_property; 164 | 165 | properties [PROP_TITLE] 166 | = g_param_spec_string ("title", 167 | "Title", 168 | "Title", 169 | TITLE, 170 | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); 171 | 172 | properties [PROP_OBJECT] 173 | = g_param_spec_object ("object", 174 | "Object", 175 | "Object", 176 | G_TYPE_OBJECT, 177 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); 178 | 179 | 180 | g_object_class_install_properties (object_class, N_PROPS, properties); 181 | 182 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); 183 | 184 | gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); 185 | } 186 | 187 | void 188 | type_setup (GtkSignalListItemFactory *self, 189 | GtkListItem *listitem, 190 | gpointer user_data) 191 | { 192 | GtkWidget *label; 193 | GtkWidget *expander; 194 | 195 | label = gtk_label_new (""); 196 | expander = gtk_tree_expander_new (); 197 | 198 | gtk_tree_expander_set_child (GTK_TREE_EXPANDER (expander), label); 199 | 200 | gtk_list_item_set_child (listitem, expander); 201 | } 202 | 203 | void 204 | type_bind (GtkSignalListItemFactory *self, 205 | GtkListItem *listitem, 206 | gpointer user_data) 207 | { 208 | GtkWidget *label; 209 | GtkTreeExpander *expander; 210 | GtkTreeListRow *row; 211 | TextItem *item; 212 | const gchar *type; 213 | 214 | expander = GTK_TREE_EXPANDER (gtk_list_item_get_child (listitem)); 215 | row = GTK_TREE_LIST_ROW (gtk_list_item_get_item (listitem)); 216 | 217 | item = gtk_tree_list_row_get_item (row); 218 | 219 | g_assert (GTK_IS_TREE_EXPANDER (expander)); 220 | g_assert (GTK_IS_TREE_LIST_ROW (row)); 221 | g_assert (TEXT_IS_ITEM (item)); 222 | 223 | gtk_tree_expander_set_list_row (expander, row); 224 | 225 | label = gtk_tree_expander_get_child (expander); 226 | type = g_type_name_from_instance ((GTypeInstance *)item); 227 | gtk_label_set_text (GTK_LABEL (label), type); 228 | } 229 | 230 | void 231 | common_setup (GtkSignalListItemFactory *self, 232 | GtkListItem *listitem, 233 | gpointer user_data) 234 | { 235 | GtkWidget *label, *tag; 236 | GtkWidget *hbox; 237 | 238 | hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); 239 | 240 | tag = gtk_label_new (""); 241 | gtk_label_set_xalign (GTK_LABEL (tag), 0.5f); 242 | gtk_widget_add_css_class (tag, "inspector-tag"); 243 | gtk_box_append (GTK_BOX (hbox), tag); 244 | gtk_widget_set_visible (tag, FALSE); 245 | 246 | label = gtk_label_new (""); 247 | gtk_label_set_xalign (GTK_LABEL (label), 0); 248 | gtk_label_set_single_line_mode (GTK_LABEL (label), TRUE); 249 | gtk_box_append (GTK_BOX (hbox), label); 250 | 251 | gtk_list_item_set_child (listitem, hbox); 252 | } 253 | 254 | void 255 | text_bind (GtkSignalListItemFactory *self, 256 | GtkListItem *listitem, 257 | gpointer user_data) 258 | { 259 | GtkWidget *hbox, *tag, *label; 260 | GtkTreeListRow *row; 261 | TextItem *item; 262 | 263 | hbox = gtk_list_item_get_child (listitem); 264 | row = GTK_TREE_LIST_ROW (gtk_list_item_get_item (listitem)); 265 | 266 | item = gtk_tree_list_row_get_item (row); 267 | 268 | tag = gtk_widget_get_first_child ( hbox); 269 | label = gtk_widget_get_next_sibling (tag); 270 | 271 | g_assert (GTK_IS_TREE_LIST_ROW (row)); 272 | g_assert (TEXT_IS_ITEM (item)); 273 | 274 | gtk_widget_set_visible (tag, FALSE); 275 | 276 | if (TEXT_IS_RUN (item)) 277 | { 278 | const gchar *text; 279 | 280 | g_object_get (item, "text", &text, NULL); 281 | gtk_label_set_text (GTK_LABEL (label), text); 282 | } 283 | else if (TEXT_IS_IMAGE (item)) 284 | { 285 | const gchar *src; 286 | 287 | g_object_get (item, "src", &src, NULL); 288 | gtk_label_set_text (GTK_LABEL (label), src); 289 | 290 | gtk_widget_set_visible (tag, TRUE); 291 | gtk_label_set_text (GTK_LABEL (tag), "image"); 292 | } 293 | else 294 | { 295 | gtk_label_set_text (GTK_LABEL (label), NULL); 296 | } 297 | } 298 | 299 | void 300 | style_bind (GtkSignalListItemFactory *self, 301 | GtkListItem *listitem, 302 | gpointer user_data) 303 | { 304 | GtkWidget *hbox, *tag, *label; 305 | GtkTreeListRow *row; 306 | TextItem *item; 307 | 308 | hbox = gtk_list_item_get_child (listitem); 309 | row = GTK_TREE_LIST_ROW (gtk_list_item_get_item (listitem)); 310 | 311 | item = gtk_tree_list_row_get_item (row); 312 | 313 | tag = gtk_widget_get_first_child ( hbox); 314 | label = gtk_widget_get_next_sibling (tag); 315 | 316 | g_assert (GTK_IS_TREE_LIST_ROW (row)); 317 | g_assert (TEXT_IS_ITEM (item)); 318 | 319 | if (TEXT_IS_RUN (item)) 320 | { 321 | GString *string; 322 | char *text; 323 | 324 | string = g_string_new (""); 325 | if (text_run_get_style_bold (TEXT_RUN (item))) 326 | string = g_string_append (string, "bold "); 327 | if (text_run_get_style_italic (TEXT_RUN (item))) 328 | string = g_string_append (string, "italic "); 329 | if (text_run_get_style_underline (TEXT_RUN (item))) 330 | string = g_string_append (string, "underline "); 331 | 332 | text = g_string_free (string, FALSE); 333 | gtk_label_set_text (GTK_LABEL (label), text); 334 | g_free (text); 335 | } 336 | else 337 | { 338 | gtk_label_set_text (GTK_LABEL (label), NULL); 339 | } 340 | } 341 | 342 | static GtkWidget * 343 | setup_colview () 344 | { 345 | GtkWidget *colview; 346 | GtkColumnViewColumn *column; 347 | GtkListItemFactory *factory; 348 | 349 | colview = gtk_column_view_new (NULL); 350 | gtk_column_view_set_reorderable (GTK_COLUMN_VIEW (colview), FALSE); 351 | gtk_column_view_set_show_column_separators (GTK_COLUMN_VIEW (colview), TRUE); 352 | gtk_widget_set_vexpand (colview, TRUE); 353 | gtk_widget_add_css_class (colview, "data-table"); 354 | 355 | factory = gtk_signal_list_item_factory_new (); 356 | g_signal_connect (factory, "setup", G_CALLBACK (type_setup), NULL); 357 | g_signal_connect (factory, "bind", G_CALLBACK (type_bind), NULL); 358 | 359 | column = gtk_column_view_column_new ("Type", factory); 360 | gtk_column_view_column_set_expand (column, FALSE); 361 | gtk_column_view_column_set_resizable (column, TRUE); 362 | gtk_column_view_append_column (GTK_COLUMN_VIEW (colview), column); 363 | 364 | factory = gtk_signal_list_item_factory_new (); 365 | g_signal_connect (factory, "setup", G_CALLBACK (common_setup), NULL); 366 | g_signal_connect (factory, "bind", G_CALLBACK (text_bind), NULL); 367 | 368 | column = gtk_column_view_column_new ("Contents", factory); 369 | gtk_column_view_column_set_expand (column, TRUE); 370 | gtk_column_view_column_set_resizable (column, TRUE); 371 | gtk_column_view_append_column (GTK_COLUMN_VIEW (colview), column); 372 | 373 | factory = gtk_signal_list_item_factory_new (); 374 | g_signal_connect (factory, "setup", G_CALLBACK (common_setup), NULL); 375 | g_signal_connect (factory, "bind", G_CALLBACK (style_bind), NULL); 376 | 377 | column = gtk_column_view_column_new ("Style", factory); 378 | gtk_column_view_column_set_expand (column, TRUE); 379 | gtk_column_view_column_set_resizable (column, TRUE); 380 | gtk_column_view_append_column (GTK_COLUMN_VIEW (colview), column); 381 | 382 | return colview; 383 | } 384 | 385 | static void 386 | text_inspector_init (TextInspector *self) 387 | { 388 | GtkWidget *infobar; 389 | GtkWidget *label; 390 | GtkWidget *button; 391 | GtkWidget *separator; 392 | GtkWidget *scroll_area; 393 | 394 | self->vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); 395 | gtk_widget_set_parent (self->vbox, GTK_WIDGET (self)); 396 | 397 | infobar = g_object_new (GTK_TYPE_BOX, 398 | "orientation", GTK_ORIENTATION_HORIZONTAL, 399 | "margin-start", 6, 400 | "margin-end", 6, 401 | "margin-top", 6, 402 | "margin-bottom", 6, 403 | NULL); 404 | 405 | label = gtk_label_new ("Select a TextDisplay widget to view its document"); 406 | gtk_label_set_xalign (GTK_LABEL (label), 0); 407 | gtk_widget_set_hexpand (label, TRUE); 408 | gtk_widget_add_css_class (GTK_WIDGET (label), "heading"); 409 | 410 | button = gtk_button_new_with_label ("Refresh Model"); 411 | g_signal_connect_swapped (button, "clicked", G_CALLBACK (populate_data_from_frame), self); 412 | 413 | separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); 414 | 415 | gtk_box_append (GTK_BOX (infobar), label); 416 | gtk_box_append (GTK_BOX (infobar), button); 417 | 418 | gtk_box_append (GTK_BOX (self->vbox), infobar); 419 | 420 | gtk_box_append (GTK_BOX (self->vbox), separator); 421 | 422 | scroll_area = gtk_scrolled_window_new (); 423 | gtk_box_append (GTK_BOX (self->vbox), scroll_area); 424 | 425 | self->colview = setup_colview (); 426 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scroll_area), self->colview); 427 | } 428 | -------------------------------------------------------------------------------- /src/ui/inspector.h: -------------------------------------------------------------------------------- 1 | /* inspector.h 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #pragma once 13 | 14 | #include 15 | 16 | G_BEGIN_DECLS 17 | 18 | #define TEXT_TYPE_INSPECTOR (text_inspector_get_type()) 19 | 20 | G_DECLARE_FINAL_TYPE (TextInspector, text_inspector, TEXT, INSPECTOR, GtkWidget) 21 | 22 | TextInspector *text_inspector_new (void); 23 | 24 | G_END_DECLS 25 | -------------------------------------------------------------------------------- /src/ui/meson.build: -------------------------------------------------------------------------------- 1 | text_engine_sources += files([ 2 | 'display.c', 3 | 'inspector.c' 4 | ]) 5 | 6 | ui_headers = [ 7 | 'display.h', 8 | ] 9 | 10 | gnome = import('gnome') 11 | text_engine_sources += gnome.compile_resources('resources', 12 | 'resources.gresource.xml', 13 | source_dir: '.', 14 | c_name: 'resources' 15 | ) 16 | 17 | install_headers(ui_headers, subdir : header_dir / 'ui') 18 | -------------------------------------------------------------------------------- /src/ui/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style.css 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ui/style.css: -------------------------------------------------------------------------------- 1 | .inspector-tag { 2 | background-color: @blue_5; 3 | color: @fg_color; 4 | padding: 4px; 5 | border-radius: 4px; 6 | } -------------------------------------------------------------------------------- /test/insert.c: -------------------------------------------------------------------------------- 1 | /* insert.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | typedef struct { 20 | TextDocument *doc; 21 | TextEditor *editor; 22 | 23 | TextRun *run1; 24 | TextRun *run2; 25 | TextRun *run3; 26 | } InsertFixture; 27 | 28 | #define RUN1 "Once upon a time there was a little dog, " 29 | #define RUN2 "and his name was Rover." 30 | #define RUN3 "By J. R. R. Tolkien" 31 | 32 | static void 33 | insert_fixture_set_up (InsertFixture *fixture, 34 | gconstpointer user_data) 35 | { 36 | TextFrame *frame; 37 | TextParagraph *para1, *para2; 38 | TextRun *run1, *run2, *run3; 39 | 40 | frame = text_frame_new (); 41 | 42 | para1 = text_paragraph_new (); 43 | run1 = text_run_new (RUN1); 44 | run2 = text_run_new (RUN2); 45 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run1)); 46 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run2)); 47 | text_frame_append_block (frame, TEXT_BLOCK (para1)); 48 | 49 | para2 = text_paragraph_new (); 50 | run3 = text_run_new (RUN3); 51 | text_paragraph_append_fragment(para2, TEXT_FRAGMENT (run3)); 52 | text_frame_append_block (frame, TEXT_BLOCK (para2)); 53 | 54 | fixture->doc = text_document_new (); 55 | fixture->doc->frame = frame; 56 | 57 | fixture->editor = text_editor_new (fixture->doc); 58 | 59 | text_editor_move_first (fixture->editor, TEXT_EDITOR_CURSOR); 60 | 61 | fixture->run1 = run1; 62 | fixture->run2 = run2; 63 | fixture->run3 = run3; 64 | } 65 | 66 | static void 67 | insert_fixture_tear_down (InsertFixture *fixture, 68 | gconstpointer user_data) 69 | { 70 | g_object_unref (fixture->editor); 71 | g_object_unref (fixture->doc); 72 | } 73 | 74 | static void 75 | test_insert_test_start (InsertFixture *fixture, 76 | gconstpointer user_data) 77 | { 78 | // test inserting at the start of a run 79 | 80 | gchar *text; 81 | 82 | text_editor_insert_text(fixture->editor, TEXT_EDITOR_CURSOR, "Alas! "); 83 | 84 | // changed 85 | g_object_get (fixture->run1, "text", &text, NULL); 86 | g_assert_cmpstr (text, ==, "Alas! Once upon a time there was a little dog, "); 87 | 88 | // unchanged 89 | g_object_get (fixture->run2, "text", &text, NULL); 90 | g_assert_cmpstr (text, ==, RUN2); 91 | 92 | // unchanged 93 | g_object_get (fixture->run3, "text", &text, NULL); 94 | g_assert_cmpstr (text, ==, RUN3); 95 | 96 | // correct cursor index 97 | g_assert_cmpint (fixture->doc->cursor->index, ==, 6); 98 | } 99 | 100 | static void 101 | test_insert_test_middle (InsertFixture *fixture, 102 | gconstpointer user_data) 103 | { 104 | // test inserting in the middle of a run (common case) 105 | 106 | gchar *text; 107 | 108 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 26); 109 | text_editor_insert_text(fixture->editor, TEXT_EDITOR_CURSOR, "n't"); 110 | 111 | // changed 112 | g_object_get (fixture->run1, "text", &text, NULL); 113 | g_assert_cmpstr (text, ==, "Once upon a time there wasn't a little dog, "); 114 | 115 | // unchanged 116 | g_object_get (fixture->run2, "text", &text, NULL); 117 | g_assert_cmpstr (text, ==, RUN2); 118 | 119 | // unchanged 120 | g_object_get (fixture->run3, "text", &text, NULL); 121 | g_assert_cmpstr (text, ==, RUN3); 122 | 123 | // correct cursor index 124 | g_assert_cmpint (fixture->doc->cursor->index, ==, 29); 125 | } 126 | 127 | static void 128 | test_insert_test_end (InsertFixture *fixture, 129 | gconstpointer user_data) 130 | { 131 | // test inserting at the end of a run 132 | 133 | gchar *text; 134 | 135 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 41); 136 | text_editor_insert_text(fixture->editor, TEXT_EDITOR_CURSOR, "or at least I thought so..."); 137 | 138 | // changed 139 | g_object_get (fixture->run1, "text", &text, NULL); 140 | g_assert_cmpstr (text, ==, "Once upon a time there was a little dog, or at least I thought so..."); 141 | 142 | // unchanged 143 | g_object_get (fixture->run2, "text", &text, NULL); 144 | g_assert_cmpstr (text, ==, RUN2); 145 | 146 | // unchanged 147 | g_object_get (fixture->run3, "text", &text, NULL); 148 | g_assert_cmpstr (text, ==, RUN3); 149 | 150 | // correct cursor index 151 | g_assert_cmpint (fixture->doc->cursor->index, ==, 68); 152 | } 153 | 154 | static void 155 | test_insert_test_nothing (InsertFixture *fixture, 156 | gconstpointer user_data) 157 | { 158 | gchar *text; 159 | 160 | text_editor_insert_text(fixture->editor, TEXT_EDITOR_CURSOR, ""); 161 | 162 | // changed 163 | g_object_get (fixture->run1, "text", &text, NULL); 164 | g_assert_cmpstr (text, ==, RUN1); 165 | 166 | // unchanged 167 | g_object_get (fixture->run2, "text", &text, NULL); 168 | g_assert_cmpstr (text, ==, RUN2); 169 | 170 | // unchanged 171 | g_object_get (fixture->run3, "text", &text, NULL); 172 | g_assert_cmpstr (text, ==, RUN3); 173 | 174 | // correct cursor index 175 | g_assert_cmpint (fixture->doc->cursor->index, ==, 0); 176 | } 177 | 178 | int 179 | main (int argc, char *argv[]) 180 | { 181 | setlocale (LC_ALL, ""); 182 | 183 | g_test_init (&argc, &argv, NULL); 184 | 185 | // Define the tests. 186 | g_test_add ("/text-engine/editor/insert/test-start", InsertFixture, NULL, 187 | insert_fixture_set_up, test_insert_test_start, 188 | insert_fixture_tear_down); 189 | g_test_add ("/text-engine/editor/insert/test-middle", InsertFixture, NULL, 190 | insert_fixture_set_up, test_insert_test_middle, 191 | insert_fixture_tear_down); 192 | g_test_add ("/text-engine/editor/insert/test-end", InsertFixture, NULL, 193 | insert_fixture_set_up, test_insert_test_end, 194 | insert_fixture_tear_down); 195 | g_test_add ("/text-engine/editor/insert/test-nothing", InsertFixture, NULL, 196 | insert_fixture_set_up, test_insert_test_nothing, 197 | insert_fixture_tear_down); 198 | 199 | return g_test_run (); 200 | } 201 | 202 | -------------------------------------------------------------------------------- /test/mark.c: -------------------------------------------------------------------------------- 1 | /* mark.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | typedef struct { 20 | TextDocument *doc; 21 | TextEditor *editor; 22 | 23 | TextRun *run1; 24 | TextRun *run2; 25 | TextRun *run3; 26 | TextRun *run4; 27 | TextRun *run5; 28 | TextParagraph *para1; 29 | TextParagraph *para2; 30 | TextParagraph *para3; 31 | } MarkFixture; 32 | 33 | #define RUN1 "abcdefghij" 34 | #define RUN2 "1234567890" 35 | #define RUN3 "!@#$%^&*()" 36 | #define RUN4 "zxcvbnm,./" 37 | #define RUN5 "0987654321" 38 | 39 | static void 40 | mark_fixture_set_up (MarkFixture *fixture, 41 | gconstpointer user_data) 42 | { 43 | TextFrame *frame; 44 | TextParagraph *para1, *para2, *para3; 45 | TextRun *run1, *run2, *run3, *run4, *run5; 46 | 47 | frame = text_frame_new (); 48 | 49 | para1 = text_paragraph_new (); 50 | run1 = text_run_new (RUN1); 51 | run2 = text_run_new (RUN2); 52 | run3 = text_run_new (RUN3); 53 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run1)); 54 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run2)); 55 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run3)); 56 | text_frame_append_block (frame, TEXT_BLOCK (para1)); 57 | 58 | para2 = text_paragraph_new (); 59 | run4 = text_run_new (RUN4); 60 | text_paragraph_append_fragment(para2, TEXT_FRAGMENT (run4)); 61 | text_frame_append_block (frame, TEXT_BLOCK (para2)); 62 | 63 | para3 = text_paragraph_new (); 64 | run5 = text_run_new (RUN5); 65 | text_paragraph_append_fragment(para3, TEXT_FRAGMENT (run5)); 66 | text_frame_append_block (frame, TEXT_BLOCK (para3)); 67 | 68 | fixture->doc = text_document_new (); 69 | fixture->doc->frame = frame; 70 | 71 | fixture->editor = text_editor_new (fixture->doc); 72 | 73 | text_editor_move_first (fixture->editor, TEXT_EDITOR_CURSOR); 74 | 75 | fixture->run1 = run1; 76 | fixture->run2 = run2; 77 | fixture->run3 = run3; 78 | fixture->run4 = run4; 79 | fixture->run5 = run5; 80 | fixture->para1 = para1; 81 | fixture->para2 = para2; 82 | fixture->para3 = para3; 83 | } 84 | 85 | static void 86 | mark_fixture_tear_down (MarkFixture *fixture, 87 | gconstpointer user_data) 88 | { 89 | g_object_unref (fixture->editor); 90 | g_object_unref (fixture->doc); 91 | } 92 | 93 | static void 94 | test_delete_single_within (MarkFixture *fixture, 95 | gconstpointer user_data) 96 | { 97 | TextMark *mark; 98 | TextMark *cursor; 99 | TextGravity gravity; 100 | 101 | gravity = (TextGravity)user_data; 102 | cursor = fixture->doc->cursor; 103 | 104 | // Create mark 105 | mark = text_document_create_mark (fixture->doc, fixture->para1, 15, gravity); 106 | 107 | // Perform deletion 108 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 10); 109 | text_editor_delete (fixture->editor, TEXT_EDITOR_CURSOR, 5); 110 | 111 | // before: 112 | // cursor >< >< mark 113 | // abcdefghij1234567890!@#$%^&*() 114 | // after: 115 | // cursor >< mark 116 | // abcdefghij67890!@#$%^&*() 117 | 118 | g_assert_cmpint (mark->index, ==, 10); 119 | g_assert_true (mark->paragraph == fixture->para1); 120 | 121 | g_assert_cmpint (cursor->index, ==, 10); 122 | g_assert_true (cursor->paragraph == fixture->para1); 123 | } 124 | 125 | static void 126 | test_delete_single_after (MarkFixture *fixture, 127 | gconstpointer user_data) 128 | { 129 | TextMark *mark; 130 | TextMark *cursor; 131 | TextGravity gravity; 132 | 133 | gravity = (TextGravity)user_data; 134 | cursor = fixture->doc->cursor; 135 | 136 | // Create mark 137 | mark = text_document_create_mark (fixture->doc, fixture->para1, 18, gravity); 138 | 139 | // Perform deletion 140 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 10); 141 | text_editor_delete (fixture->editor, TEXT_EDITOR_CURSOR, 5); 142 | 143 | // before: 144 | // cursor >< >< mark 145 | // abcdefghij1234567890!@#$%^&*() 146 | // after: 147 | // cursor >< >< mark 148 | // abcdefghij67890!@#$%^&*() 149 | 150 | g_assert_cmpint (mark->index, ==, 13); 151 | g_assert_true (mark->paragraph == fixture->para1); 152 | 153 | g_assert_cmpint (cursor->index, ==, 10); 154 | g_assert_true (cursor->paragraph == fixture->para1); 155 | } 156 | 157 | static void 158 | test_delete_multi_start (MarkFixture *fixture, 159 | gconstpointer user_data) 160 | { 161 | TextMark *mark; 162 | TextMark *cursor; 163 | TextGravity gravity; 164 | 165 | gravity = (TextGravity)user_data; 166 | cursor = fixture->doc->cursor; 167 | 168 | // Create mark 169 | mark = text_document_create_mark (fixture->doc, fixture->para1, 18, gravity); 170 | 171 | // Perform deletion 172 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 15); 173 | text_editor_delete (fixture->editor, TEXT_EDITOR_CURSOR, 32); 174 | 175 | // before: 176 | // cursor >< >< mark 177 | // abcdefghij1234567890!@#$%^&*() 178 | // zxcvbnm,./ 179 | // 0987654321 180 | // after: 181 | // cursor >< mark 182 | // abcdefghij1234554321 183 | 184 | g_assert_cmpint (mark->index, ==, 15); 185 | g_assert_true (mark->paragraph == fixture->para1); 186 | 187 | g_assert_cmpint (cursor->index, ==, 15); 188 | g_assert_true (cursor->paragraph == fixture->para1); 189 | } 190 | 191 | static void 192 | test_delete_multi_inbetween (MarkFixture *fixture, 193 | gconstpointer user_data) 194 | { 195 | TextMark *mark; 196 | TextMark *cursor; 197 | TextGravity gravity; 198 | 199 | gravity = (TextGravity)user_data; 200 | cursor = fixture->doc->cursor; 201 | 202 | // Create mark 203 | mark = text_document_create_mark (fixture->doc, fixture->para2, 4, gravity); 204 | 205 | // Perform deletion 206 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 15); 207 | text_editor_delete (fixture->editor, TEXT_EDITOR_CURSOR, 32); 208 | 209 | // before: 210 | // cursor >< 211 | // abcdefghij1234567890!@#$%^&*() 212 | // >< mark 213 | // zxcvbnm,./ 214 | // 0987654321 215 | // after: 216 | // cursor >< mark 217 | // abcdefghij1234554321 218 | 219 | g_assert_cmpint (mark->index, ==, 15); 220 | g_assert_true (mark->paragraph == fixture->para1); 221 | 222 | g_assert_cmpint (cursor->index, ==, 15); 223 | g_assert_true (cursor->paragraph == fixture->para1); 224 | } 225 | 226 | static void 227 | test_delete_multi_end (MarkFixture *fixture, 228 | gconstpointer user_data) 229 | { 230 | TextMark *mark; 231 | TextMark *cursor; 232 | TextGravity gravity; 233 | 234 | gravity = (TextGravity)user_data; 235 | cursor = fixture->doc->cursor; 236 | 237 | // Create mark 238 | mark = text_document_create_mark (fixture->doc, fixture->para3, 2, gravity); 239 | 240 | // Perform deletion 241 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 15); 242 | text_editor_delete (fixture->editor, TEXT_EDITOR_CURSOR, 32); 243 | 244 | // before: 245 | // cursor >< 246 | // abcdefghij1234567890!@#$%^&*() 247 | // zxcvbnm,./ 248 | // >< mark 249 | // 0987654321 250 | // after: 251 | // cursor >< mark 252 | // abcdefghij1234554321 253 | 254 | g_assert_cmpint (mark->index, ==, 15); 255 | g_assert_true (mark->paragraph == fixture->para1); 256 | 257 | g_assert_cmpint (cursor->index, ==, 15); 258 | g_assert_true (cursor->paragraph == fixture->para1); 259 | } 260 | 261 | static void 262 | test_delete_multi_after (MarkFixture *fixture, 263 | gconstpointer user_data) 264 | { 265 | TextMark *mark; 266 | TextMark *cursor; 267 | TextGravity gravity; 268 | 269 | gravity = (TextGravity)user_data; 270 | cursor = fixture->doc->cursor; 271 | 272 | // Create mark 273 | mark = text_document_create_mark (fixture->doc, fixture->para3, 9, gravity); 274 | 275 | // Perform deletion 276 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 15); 277 | text_editor_delete (fixture->editor, TEXT_EDITOR_CURSOR, 32); 278 | 279 | // before: 280 | // cursor >< 281 | // abcdefghij1234567890!@#$%^&*() 282 | // zxcvbnm,./ 283 | // >< mark 284 | // 0987654321 285 | // after: 286 | // cursor >< >< mark 287 | // abcdefghij1234554321 288 | 289 | g_assert_cmpint (mark->index, ==, 19); 290 | g_assert_true (mark->paragraph == fixture->para1); 291 | 292 | g_assert_cmpint (cursor->index, ==, 15); 293 | g_assert_true (cursor->paragraph == fixture->para1); 294 | } 295 | 296 | static void 297 | test_insert_on (MarkFixture *fixture, 298 | gconstpointer user_data) 299 | { 300 | TextMark *mark; 301 | TextMark *cursor; 302 | 303 | cursor = fixture->doc->cursor; 304 | 305 | // Create mark 306 | mark = text_document_create_mark (fixture->doc, fixture->para1, 9, TEXT_GRAVITY_LEFT); 307 | 308 | // Perform insertion 309 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 9); 310 | text_editor_insert_text(fixture->editor, TEXT_EDITOR_CURSOR, "Hello"); 311 | 312 | // before: 313 | // cursor >< mark 314 | // abcdefghij1234567890!@#$%^&*() 315 | // after: 316 | // cursor >< mark 317 | // abcdefghiHelloj1234567890!@#$%^&*() 318 | 319 | // mark - left gravity 320 | g_assert_cmpint (mark->index, ==, 9); 321 | g_assert_true (mark->paragraph == fixture->para1); 322 | 323 | // cursor - right gravity 324 | g_assert_cmpint (cursor->index, ==, 14); 325 | g_assert_true (cursor->paragraph == fixture->para1); 326 | } 327 | 328 | static void 329 | test_insert_after (MarkFixture *fixture, 330 | gconstpointer user_data) 331 | { 332 | TextMark *mark; 333 | TextMark *cursor; 334 | TextGravity gravity; 335 | 336 | gravity = (TextGravity)user_data; 337 | cursor = fixture->doc->cursor; 338 | 339 | // Create mark 340 | mark = text_document_create_mark (fixture->doc, fixture->para1, 17, gravity); 341 | 342 | // Perform insertion 343 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 9); 344 | text_editor_insert_text(fixture->editor, TEXT_EDITOR_CURSOR, "Hello"); 345 | 346 | // before: 347 | // cursor >< >< mark 348 | // abcdefghij1234567890!@#$%^&*() 349 | // after: 350 | // cursor >< >< mark 351 | // abcdefghiHelloj1234567890!@#$%^&*() 352 | 353 | g_assert_cmpint (mark->index, ==, 22); 354 | g_assert_true (mark->paragraph == fixture->para1); 355 | 356 | g_assert_cmpint (cursor->index, ==, 14); 357 | g_assert_true (cursor->paragraph == fixture->para1); 358 | } 359 | 360 | static void 361 | test_split_on (MarkFixture *fixture, 362 | gconstpointer user_data) 363 | { 364 | TextMark *mark; 365 | TextMark *cursor; 366 | TextParagraph *new; 367 | 368 | cursor = fixture->doc->cursor; 369 | 370 | // Create mark 371 | mark = text_document_create_mark (fixture->doc, fixture->para1, 9, TEXT_GRAVITY_LEFT); 372 | 373 | // Perform split 374 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 9); 375 | text_editor_split (fixture->editor, TEXT_EDITOR_CURSOR); 376 | 377 | // before: 378 | // cursor >< mark 379 | // abcdefghij1234567890!@#$%^&*() 380 | // after: 381 | // >< mark 382 | // abcdefghi 383 | // >< cursor 384 | // j1234567890!@#$%^&*() 385 | 386 | new = TEXT_PARAGRAPH (text_node_get_next (TEXT_NODE (fixture->para1))); 387 | 388 | // mark - left gravity 389 | g_assert_cmpint (mark->index, ==, 9); 390 | g_assert_true (mark->paragraph == fixture->para1); 391 | 392 | // cursor - right gravity 393 | g_assert_cmpint (cursor->index, ==, 0); 394 | g_assert_true (cursor->paragraph == new); 395 | } 396 | 397 | static void 398 | test_split_after (MarkFixture *fixture, 399 | gconstpointer user_data) 400 | { 401 | TextMark *mark; 402 | TextMark *cursor; 403 | TextParagraph *new; 404 | 405 | cursor = fixture->doc->cursor; 406 | 407 | // Create mark 408 | mark = text_document_create_mark (fixture->doc, fixture->para1, 24, TEXT_GRAVITY_LEFT); 409 | 410 | // Perform split 411 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 9); 412 | text_editor_split (fixture->editor, TEXT_EDITOR_CURSOR); 413 | 414 | // before: 415 | // cursor >< >< mark 416 | // abcdefghij1234567890!@#$%^&*() 417 | // after: 418 | // abcdefghi 419 | // >< cursor >< mark 420 | // j1234567890!@#$%^&*() 421 | 422 | new = TEXT_PARAGRAPH (text_node_get_next (TEXT_NODE (fixture->para1))); 423 | 424 | // mark - left gravity 425 | g_assert_cmpint (mark->index, ==, 15); 426 | g_assert_true (mark->paragraph == new); 427 | 428 | // cursor - right gravity 429 | g_assert_cmpint (cursor->index, ==, 0); 430 | g_assert_true (cursor->paragraph == new); 431 | } 432 | 433 | int 434 | main (int argc, char *argv[]) 435 | { 436 | setlocale (LC_ALL, ""); 437 | 438 | g_test_init (&argc, &argv, NULL); 439 | 440 | // Delete tests 441 | g_test_add ("/text-engine/editor/mark/test-delete-single-within-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 442 | mark_fixture_set_up, test_delete_single_within, 443 | mark_fixture_tear_down); 444 | g_test_add ("/text-engine/editor/mark/test-delete-single-within-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 445 | mark_fixture_set_up, test_delete_single_within, 446 | mark_fixture_tear_down); 447 | g_test_add ("/text-engine/editor/mark/test-delete-single-after-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 448 | mark_fixture_set_up, test_delete_single_after, 449 | mark_fixture_tear_down); 450 | g_test_add ("/text-engine/editor/mark/test-delete-single-after-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 451 | mark_fixture_set_up, test_delete_single_after, 452 | mark_fixture_tear_down); 453 | 454 | g_test_add ("/text-engine/editor/mark/test-delete-multi-start-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 455 | mark_fixture_set_up, test_delete_multi_start, 456 | mark_fixture_tear_down); 457 | g_test_add ("/text-engine/editor/mark/test-delete-multi-start-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 458 | mark_fixture_set_up, test_delete_multi_start, 459 | mark_fixture_tear_down); 460 | g_test_add ("/text-engine/editor/mark/test-delete-multi-inbetween-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 461 | mark_fixture_set_up, test_delete_multi_inbetween, 462 | mark_fixture_tear_down); 463 | g_test_add ("/text-engine/editor/mark/test-delete-multi-inbetween-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 464 | mark_fixture_set_up, test_delete_multi_inbetween, 465 | mark_fixture_tear_down); 466 | g_test_add ("/text-engine/editor/mark/test-delete-multi-end-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 467 | mark_fixture_set_up, test_delete_multi_end, 468 | mark_fixture_tear_down); 469 | g_test_add ("/text-engine/editor/mark/test-delete-multi-end-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 470 | mark_fixture_set_up, test_delete_multi_end, 471 | mark_fixture_tear_down); 472 | g_test_add ("/text-engine/editor/mark/test-delete-multi-after-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 473 | mark_fixture_set_up, test_delete_multi_after, 474 | mark_fixture_tear_down); 475 | g_test_add ("/text-engine/editor/mark/test-delete-multi-after-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 476 | mark_fixture_set_up, test_delete_multi_after, 477 | mark_fixture_tear_down); 478 | 479 | // Insert tests 480 | g_test_add ("/text-engine/editor/mark/test-insert-on", MarkFixture, NULL, 481 | mark_fixture_set_up, test_insert_on, 482 | mark_fixture_tear_down); 483 | g_test_add ("/text-engine/editor/mark/test-insert-after-gravity-left", MarkFixture, (gconstpointer) TEXT_GRAVITY_LEFT, 484 | mark_fixture_set_up, test_insert_after, 485 | mark_fixture_tear_down); 486 | g_test_add ("/text-engine/editor/mark/test-insert-after-gravity-right", MarkFixture, (gconstpointer) TEXT_GRAVITY_RIGHT, 487 | mark_fixture_set_up, test_insert_after, 488 | mark_fixture_tear_down); 489 | 490 | // Replace tests 491 | // A replacement is simply a deletion followed 492 | // by an insertion, so covered by the above tests 493 | 494 | // Split tests 495 | g_test_add ("/text-engine/editor/mark/test-split-on", MarkFixture, NULL, 496 | mark_fixture_set_up, test_split_on, 497 | mark_fixture_tear_down); 498 | g_test_add ("/text-engine/editor/mark/test-split-after", MarkFixture, NULL, 499 | mark_fixture_set_up, test_split_after, 500 | mark_fixture_tear_down); 501 | 502 | return g_test_run (); 503 | } 504 | 505 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | deps = [ 2 | dependency('glib-2.0'), 3 | text_engine_dep 4 | ] 5 | 6 | tests = [ 7 | ['move', ['move.c']], 8 | ['insert', ['insert.c']], 9 | ['delete', ['delete.c']], 10 | ['replace', ['replace.c']], 11 | ['split', ['split.c']], 12 | ['mark', ['mark.c']], 13 | ] 14 | 15 | foreach t: tests 16 | test( 17 | t[0], 18 | executable(t[0], t[1], dependencies: deps), 19 | env: [ 20 | 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()), 21 | 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()) 22 | ], 23 | protocol: 'tap', 24 | ) 25 | endforeach 26 | 27 | -------------------------------------------------------------------------------- /test/move.c: -------------------------------------------------------------------------------- 1 | /* move.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | typedef struct { 20 | TextDocument *doc; 21 | TextEditor *editor; 22 | 23 | TextRun *run1; 24 | TextRun *run2; 25 | TextRun *run3; 26 | TextRun *run4; 27 | } MoveFixture; 28 | 29 | #define RUN1 "Once upon a time there was a little dog, " 30 | #define RUN2 "AND HIS NAME WAS ROVER." 31 | #define RUN3 "By J. R. R. Tolkien" 32 | #define RUN4 "Roverandom, 1920s" 33 | 34 | static void 35 | move_fixture_set_up_single (MoveFixture *fixture, 36 | gconstpointer user_data) 37 | { 38 | TextFrame *frame; 39 | TextParagraph *para1; 40 | TextRun *run1; 41 | 42 | frame = text_frame_new (); 43 | 44 | para1 = text_paragraph_new (); 45 | run1 = text_run_new (RUN1); 46 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run1)); 47 | text_frame_append_block (frame, TEXT_BLOCK (para1)); 48 | 49 | fixture->doc = text_document_new (); 50 | fixture->doc->frame = frame; 51 | 52 | fixture->editor = text_editor_new (fixture->doc); 53 | 54 | text_editor_move_first (fixture->editor, TEXT_EDITOR_CURSOR); 55 | 56 | fixture->run1 = run1; 57 | fixture->run2 = NULL; 58 | fixture->run3 = NULL; 59 | fixture->run4 = NULL; 60 | } 61 | 62 | #define RUN5 "This is some text that is pa" 63 | #define RUN6 "RT OF TWO DIFFE" 64 | #define RUN7 "rent runs" 65 | 66 | static void 67 | move_fixture_set_up_runs (MoveFixture *fixture, 68 | gconstpointer user_data) 69 | { 70 | TextFrame *frame; 71 | TextParagraph *para1; 72 | TextRun *run1, *run2, *run3; 73 | 74 | frame = text_frame_new (); 75 | 76 | para1 = text_paragraph_new (); 77 | run1 = text_run_new (RUN5); 78 | run2 = text_run_new (RUN6); 79 | run3 = text_run_new (RUN7); 80 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run1)); 81 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run2)); 82 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run3)); 83 | text_frame_append_block (frame, TEXT_BLOCK (para1)); 84 | 85 | fixture->doc = text_document_new (); 86 | fixture->doc->frame = frame; 87 | 88 | fixture->editor = text_editor_new (fixture->doc); 89 | 90 | text_editor_move_first (fixture->editor, TEXT_EDITOR_CURSOR); 91 | 92 | fixture->run1 = run1; 93 | fixture->run2 = run2; 94 | fixture->run3 = run3; 95 | fixture->run4 = NULL; 96 | } 97 | 98 | static void 99 | move_fixture_set_up_paragraphs (MoveFixture *fixture, 100 | gconstpointer user_data) 101 | { 102 | TextFrame *frame; 103 | TextParagraph *para1, *para2, *para3; 104 | TextRun *run1, *run2, *run3, *run4; 105 | 106 | frame = text_frame_new (); 107 | 108 | para1 = text_paragraph_new (); 109 | run1 = text_run_new (RUN1); 110 | run2 = text_run_new (RUN2); 111 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run1)); 112 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run2)); 113 | text_frame_append_block (frame, TEXT_BLOCK (para1)); 114 | 115 | para2 = text_paragraph_new (); 116 | run3 = text_run_new (RUN3); 117 | text_paragraph_append_fragment(para2, TEXT_FRAGMENT (run3)); 118 | text_frame_append_block (frame, TEXT_BLOCK (para2)); 119 | 120 | para3 = text_paragraph_new (); 121 | run4 = text_run_new (RUN4); 122 | text_paragraph_append_fragment(para3, TEXT_FRAGMENT (run4)); 123 | text_frame_append_block (frame, TEXT_BLOCK (para3)); 124 | 125 | fixture->doc = text_document_new (); 126 | fixture->doc->frame = frame; 127 | 128 | fixture->editor = text_editor_new (fixture->doc); 129 | 130 | text_editor_move_first (fixture->editor, TEXT_EDITOR_CURSOR); 131 | 132 | fixture->run1 = run1; 133 | fixture->run2 = run2; 134 | fixture->run3 = run3; 135 | fixture->run4 = run4; 136 | } 137 | 138 | static void 139 | move_fixture_tear_down (MoveFixture *fixture, 140 | gconstpointer user_data) 141 | { 142 | g_object_unref (fixture->editor); 143 | g_object_unref (fixture->doc); 144 | } 145 | 146 | static void 147 | test_left_guard (MoveFixture *fixture, 148 | gconstpointer user_data) 149 | { 150 | int amount; 151 | amount = (int)user_data; 152 | 153 | // test moving left at the start of the document 154 | text_editor_move_left (fixture->editor, TEXT_EDITOR_CURSOR, amount); 155 | 156 | // cursor position unchanged 157 | g_assert_cmpint (fixture->doc->cursor->index, ==, 0); 158 | } 159 | 160 | static void 161 | test_right_guard (MoveFixture *fixture, 162 | gconstpointer user_data) 163 | { 164 | int amount; 165 | amount = (int)user_data; 166 | 167 | // go to end 168 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 41); 169 | 170 | // test moving right at the end of the document 171 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, amount); 172 | 173 | // cursor position unchanged 174 | g_assert_cmpint (fixture->doc->cursor->index, ==, 41); 175 | } 176 | 177 | // We have three runs in a single paragraph. Capitalisation indicates 178 | // a different run for these test cases: 179 | // 180 | // index 28 181 | // / 182 | // `This is some text that is paRT OF TWO DIFFErent runs` 183 | // ^ 184 | // index 29 / 185 | // 186 | // When traversing leftwards from index 29, we cross into a new 187 | // run at index 28. 188 | 189 | static void 190 | test_left_traversal_across_run (MoveFixture *fixture, 191 | gconstpointer user_data) 192 | { 193 | int amount; 194 | amount = (int)user_data; 195 | 196 | // go to index 29 (run two) 197 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 29); 198 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run2)); 199 | 200 | // test moving left by amount 201 | text_editor_move_left (fixture->editor, TEXT_EDITOR_CURSOR, amount); 202 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run1)); 203 | 204 | g_assert_cmpint (fixture->doc->cursor->index, ==, 29 - amount); 205 | } 206 | 207 | static void 208 | test_right_traversal_across_run (MoveFixture *fixture, 209 | gconstpointer user_data) 210 | { 211 | int amount; 212 | amount = (int)user_data; 213 | 214 | // go to index 28 (run one) 215 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 28); 216 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run1)); 217 | 218 | // test moving right by amount 219 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, amount); 220 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run2)); 221 | 222 | g_assert_cmpint (fixture->doc->cursor->index, ==, 28 + amount); 223 | } 224 | 225 | // We have four runs across three paragraphs. Again, capitalisation 226 | // represents the start of a new run. 227 | // 228 | // first index in run 2 index 64 229 | // \ \ 230 | // p1: `Once upon a time there was a little dog, AND HIS NAME WAS ROVER.` 231 | // p2: `By J. R. R. Tolkien` 232 | // p3: `Roverandom, 1920s` 233 | // 234 | // The end of p1 is index 64, after the full stop. The start of p2 is 235 | // naturally index 0. When traversing one character from index 64, the 236 | // cursor should move to index 0. Traversing 10 characters would move to 237 | // index 9, etc. 238 | // 239 | // Traversing 21 characters should place the cursor at p3 index 0. 240 | 241 | static void 242 | test_left_traversal_across_paragraph (MoveFixture *fixture, 243 | gconstpointer user_data) 244 | { 245 | int amount; 246 | amount = (int)user_data; 247 | 248 | // move to start of p2 (run3) 249 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 65); 250 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run3)); 251 | g_assert_cmpint (fixture->doc->cursor->index, ==, 0); 252 | 253 | // move backwards by amount 254 | text_editor_move_left (fixture->editor, TEXT_EDITOR_CURSOR, amount); 255 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run2)); 256 | 257 | // check index 258 | g_assert_cmpint (fixture->doc->cursor->index, ==, (64 - (amount - 1))); 259 | } 260 | 261 | static void 262 | test_right_traversal_across_paragraph (MoveFixture *fixture, 263 | gconstpointer user_data) 264 | { 265 | int amount; 266 | amount = (int)user_data; 267 | 268 | // move to end of p1 (run2) 269 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 64); 270 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run2)); 271 | 272 | // move forwards by amount 273 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, amount); 274 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run3)); 275 | 276 | // check index 277 | g_assert_cmpint (fixture->doc->cursor->index, ==, amount - 1); 278 | } 279 | 280 | static void 281 | test_left_traversal_across_several_paragraphs (MoveFixture *fixture, 282 | gconstpointer user_data) 283 | { 284 | // move to start of p3 (run4) 285 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 85); 286 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run4)); 287 | 288 | // move left by 62 characters (to run1) 289 | text_editor_move_left (fixture->editor, TEXT_EDITOR_CURSOR, 62); 290 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run1)); 291 | 292 | // check index is 23 in p1 293 | g_assert_cmpint (fixture->doc->cursor->index, ==, 23); 294 | } 295 | 296 | static void 297 | test_right_traversal_across_several_paragraphs (MoveFixture *fixture, 298 | gconstpointer user_data) 299 | { 300 | // move to p3, index 2 (run4) 301 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 87); 302 | g_assert_true (text_editor_get_item (fixture->editor, TEXT_EDITOR_CURSOR) == TEXT_FRAGMENT (fixture->run4)); 303 | 304 | // check index is 2 in p3 305 | g_assert_cmpint (fixture->doc->cursor->index, ==, 2); 306 | } 307 | 308 | static void 309 | test_balanced_traversal (MoveFixture *fixture, 310 | gconstpointer user_data) 311 | { 312 | int amount; 313 | int old_index; 314 | TextParagraph *old_paragraph; 315 | 316 | amount = (int)user_data; 317 | 318 | old_index = fixture->doc->cursor->index; 319 | old_paragraph = fixture->doc->cursor->paragraph; 320 | 321 | // ensure that equal left and right movements return to the same position 322 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, amount); 323 | text_editor_move_left (fixture->editor, TEXT_EDITOR_CURSOR, amount); 324 | 325 | g_assert_true (old_index == fixture->doc->cursor->index); 326 | g_assert_true (old_paragraph == fixture->doc->cursor->paragraph); 327 | } 328 | 329 | int 330 | main (int argc, char *argv[]) 331 | { 332 | setlocale (LC_ALL, ""); 333 | 334 | g_test_init (&argc, &argv, NULL); 335 | 336 | // Document guard tests 337 | g_test_add ("/text-engine/editor/move/test-left-guard-one", MoveFixture, (void*)1, 338 | move_fixture_set_up_single, test_left_guard, 339 | move_fixture_tear_down); 340 | g_test_add ("/text-engine/editor/move/test-left-guard-ten", MoveFixture, (void*)10, 341 | move_fixture_set_up_single, test_left_guard, 342 | move_fixture_tear_down); 343 | g_test_add ("/text-engine/editor/move/test-right-guard-one", MoveFixture, (void*)1, 344 | move_fixture_set_up_single, test_right_guard, 345 | move_fixture_tear_down); 346 | g_test_add ("/text-engine/editor/move/test-right-guard-ten", MoveFixture, (void*)10, 347 | move_fixture_set_up_single, test_right_guard, 348 | move_fixture_tear_down); 349 | 350 | // Run boundary tests 351 | g_test_add ("/text-engine/editor/move/test-left-traversal-across-run-one", MoveFixture, (void*)1, 352 | move_fixture_set_up_runs, test_left_traversal_across_run, 353 | move_fixture_tear_down); 354 | g_test_add ("/text-engine/editor/move/test-left-traversal-across-run-ten", MoveFixture, (void*)10, 355 | move_fixture_set_up_runs, test_left_traversal_across_run, 356 | move_fixture_tear_down); 357 | g_test_add ("/text-engine/editor/move/test-right-traversal-across-run-one", MoveFixture, (void*)1, 358 | move_fixture_set_up_runs, test_right_traversal_across_run, 359 | move_fixture_tear_down); 360 | g_test_add ("/text-engine/editor/move/test-right-traversal-across-run-ten", MoveFixture, (void*)10, 361 | move_fixture_set_up_runs, test_right_traversal_across_run, 362 | move_fixture_tear_down); 363 | 364 | // Paragraph boundary tests 365 | g_test_add ("/text-engine/editor/move/test-left-traversal-across-paragraph-one", MoveFixture, (void*)1, 366 | move_fixture_set_up_paragraphs, test_left_traversal_across_paragraph, 367 | move_fixture_tear_down); 368 | g_test_add ("/text-engine/editor/move/test-right-traversal-across-paragraph-one", MoveFixture, (void*)1, 369 | move_fixture_set_up_paragraphs, test_right_traversal_across_paragraph, 370 | move_fixture_tear_down); 371 | g_test_add ("/text-engine/editor/move/test-left-traversal-across-paragraph-five", MoveFixture, (void*)5, 372 | move_fixture_set_up_paragraphs, test_left_traversal_across_paragraph, 373 | move_fixture_tear_down); 374 | g_test_add ("/text-engine/editor/move/test-right-traversal-across-paragraph-five", MoveFixture, (void*)5, 375 | move_fixture_set_up_paragraphs, test_right_traversal_across_paragraph, 376 | move_fixture_tear_down); 377 | 378 | // Multi-paragraph boundary tests 379 | g_test_add ("/text-engine/editor/move/test-left-traversal-across-several-paragraphs", MoveFixture, NULL, 380 | move_fixture_set_up_paragraphs, test_left_traversal_across_several_paragraphs, 381 | move_fixture_tear_down); 382 | g_test_add ("/text-engine/editor/move/test-right-traversal-across-several-paragraphs", MoveFixture, NULL, 383 | move_fixture_set_up_paragraphs, test_right_traversal_across_several_paragraphs, 384 | move_fixture_tear_down); 385 | 386 | // Balance tests 387 | g_test_add ("/text-engine/editor/move/test-balanced-traversal-one", MoveFixture, (void*)1, 388 | move_fixture_set_up_paragraphs, test_balanced_traversal, 389 | move_fixture_tear_down); 390 | g_test_add ("/text-engine/editor/move/test-balanced-traversal-five", MoveFixture, (void*)5, 391 | move_fixture_set_up_paragraphs, test_balanced_traversal, 392 | move_fixture_tear_down); 393 | g_test_add ("/text-engine/editor/move/test-balanced-traversal-ten", MoveFixture, (void*)10, 394 | move_fixture_set_up_paragraphs, test_balanced_traversal, 395 | move_fixture_tear_down); 396 | g_test_add ("/text-engine/editor/move/test-balanced-traversal-fifty", MoveFixture, (void*)50, 397 | move_fixture_set_up_paragraphs, test_balanced_traversal, 398 | move_fixture_tear_down); 399 | g_test_add ("/text-engine/editor/move/test-balanced-traversal-hundred", MoveFixture, (void*)100, 400 | move_fixture_set_up_paragraphs, test_balanced_traversal, 401 | move_fixture_tear_down); 402 | 403 | 404 | return g_test_run (); 405 | } 406 | 407 | -------------------------------------------------------------------------------- /test/split.c: -------------------------------------------------------------------------------- 1 | /* split.c 2 | * 3 | * Copyright 2022 Matthew Jakeman 4 | * 5 | * This file is dual-licensed under the terms of the Mozilla Public 6 | * License 2.0 and the Lesser General Public License 2.1 (or any 7 | * later version). 8 | * 9 | * SPDX-License-Identifier: MPL-2.0 OR LGPL-2.1-or-later 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | typedef struct { 20 | TextDocument *doc; 21 | TextEditor *editor; 22 | 23 | TextRun *run1; 24 | TextRun *run2; 25 | TextRun *run3; 26 | TextRun *run4; 27 | TextRun *run5; 28 | TextParagraph *para1; 29 | TextParagraph *para2; 30 | TextParagraph *para3; 31 | } SplitFixture; 32 | 33 | #define RUN1 "abcdefghij" 34 | #define RUN2 "1234567890" 35 | #define RUN3 "!@#$%^&*()" 36 | #define RUN4 "zxcvbnm,./" 37 | #define RUN5 "0987654321" 38 | 39 | static void 40 | split_fixture_set_up (SplitFixture *fixture, 41 | gconstpointer user_data) 42 | { 43 | TextFrame *frame; 44 | TextParagraph *para1, *para2, *para3; 45 | TextRun *run1, *run2, *run3, *run4, *run5; 46 | 47 | frame = text_frame_new (); 48 | 49 | para1 = text_paragraph_new (); 50 | run1 = text_run_new (RUN1); 51 | run2 = text_run_new (RUN2); 52 | run3 = text_run_new (RUN3); 53 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run1)); 54 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run2)); 55 | text_paragraph_append_fragment(para1, TEXT_FRAGMENT (run3)); 56 | text_frame_append_block (frame, TEXT_BLOCK (para1)); 57 | 58 | para2 = text_paragraph_new (); 59 | run4 = text_run_new (RUN4); 60 | text_paragraph_append_fragment(para2, TEXT_FRAGMENT (run4)); 61 | text_frame_append_block (frame, TEXT_BLOCK (para2)); 62 | 63 | para3 = text_paragraph_new (); 64 | run5 = text_run_new (RUN5); 65 | text_paragraph_append_fragment(para3, TEXT_FRAGMENT (run5)); 66 | text_frame_append_block (frame, TEXT_BLOCK (para3)); 67 | 68 | fixture->doc = text_document_new (); 69 | fixture->doc->frame = frame; 70 | 71 | fixture->editor = text_editor_new (fixture->doc); 72 | 73 | text_editor_move_first (fixture->editor, TEXT_EDITOR_CURSOR); 74 | 75 | fixture->run1 = run1; 76 | fixture->run2 = run2; 77 | fixture->run3 = run3; 78 | fixture->run4 = run4; 79 | fixture->run5 = run5; 80 | fixture->para1 = para1; 81 | fixture->para2 = para2; 82 | fixture->para3 = para3; 83 | } 84 | 85 | static void 86 | split_fixture_tear_down (SplitFixture *fixture, 87 | gconstpointer user_data) 88 | { 89 | g_object_unref (fixture->editor); 90 | g_object_unref (fixture->doc); 91 | } 92 | 93 | static void 94 | test_end_of_paragraph (SplitFixture *fixture, 95 | gconstpointer user_data) 96 | { 97 | gchar *text; 98 | int length; 99 | TextParagraph *new; 100 | 101 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 30); 102 | text_editor_split (fixture->editor, TEXT_EDITOR_CURSOR); 103 | 104 | // before: 105 | // abcdefghij1234567890!@#$%^&*() 106 | // zxcvbnm,./ 107 | // 0987654321 108 | // after: 109 | // abcdefghij1234567890!@#$%^&*() 110 | // <-- new paragraph 111 | // zxcvbnm,./ 112 | // 0987654321 113 | 114 | new = TEXT_PARAGRAPH (text_node_get_next (TEXT_NODE (fixture->para1))); 115 | 116 | // check paragraph contents 117 | text = text_paragraph_get_text (fixture->para1); 118 | g_assert_cmpstr (text, ==, "abcdefghij1234567890!@#$%^&*()"); 119 | g_free (text); 120 | 121 | text = text_paragraph_get_text (new); 122 | g_assert_cmpstr (text, ==, ""); 123 | g_free (text); 124 | 125 | text = text_paragraph_get_text (fixture->para2); 126 | g_assert_cmpstr (text, ==, "zxcvbnm,./"); 127 | g_free (text); 128 | 129 | // check new length 130 | length = text_paragraph_get_length (new); 131 | g_assert_cmpint (length, ==, 0); 132 | 133 | // assert cursor is on new paragraph 134 | g_assert_cmpint (fixture->doc->cursor->index, ==, 0); 135 | g_assert_true (fixture->doc->cursor->paragraph == new); 136 | } 137 | 138 | static void 139 | test_start_of_paragraph (SplitFixture *fixture, 140 | gconstpointer user_data) 141 | { 142 | gchar *text; 143 | int length; 144 | TextParagraph *new; 145 | 146 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 31); 147 | text_editor_split (fixture->editor, TEXT_EDITOR_CURSOR); 148 | 149 | // before: 150 | // abcdefghij1234567890!@#$%^&*() 151 | // zxcvbnm,./ 152 | // 0987654321 153 | // after: 154 | // abcdefghij1234567890!@#$%^&*() 155 | // <-- modified paragraph two 156 | // zxcvbnm,./ <-- new paragraph 157 | // 0987654321 158 | 159 | // check paragraph contents 160 | text = text_paragraph_get_text (fixture->para1); 161 | g_assert_cmpstr (text, ==, "abcdefghij1234567890!@#$%^&*()"); 162 | g_free (text); 163 | 164 | // check that paragraph two is now empty 165 | text = text_paragraph_get_text (fixture->para2); 166 | g_assert_cmpstr (text, ==, ""); 167 | g_free (text); 168 | 169 | // check paragraph two length 170 | length = text_paragraph_get_length (fixture->para2); 171 | g_assert_cmpint (length, ==, 0); 172 | 173 | // get newly-inserted paragraph 174 | new = TEXT_PARAGRAPH (text_node_get_next (TEXT_NODE (fixture->para2))); 175 | 176 | // check contents 177 | text = text_paragraph_get_text (new); 178 | g_assert_cmpstr (text, ==, "zxcvbnm,./"); 179 | g_free (text); 180 | 181 | // check length 182 | length = text_paragraph_get_length (new); 183 | g_assert_cmpint (length, ==, 10); 184 | 185 | // assert cursor is at start of new paragraph 186 | g_assert_cmpint (fixture->doc->cursor->index, ==, 0); 187 | g_assert_true (fixture->doc->cursor->paragraph == new); 188 | } 189 | 190 | static void 191 | test_middle_of_paragraph (SplitFixture *fixture, 192 | gconstpointer user_data) 193 | { 194 | gchar *text; 195 | int length; 196 | TextParagraph *new; 197 | 198 | text_editor_move_right (fixture->editor, TEXT_EDITOR_CURSOR, 15); 199 | text_editor_split (fixture->editor, TEXT_EDITOR_CURSOR); 200 | 201 | // before: 202 | // abcdefghij1234567890!@#$%^&*() 203 | // zxcvbnm,./ 204 | // 0987654321 205 | // after: 206 | // abcdefghij12345 <-- modified paragraph one 207 | // 67890!@#$%^&*() <-- new paragraph 208 | // zxcvbnm,./ 209 | // 0987654321 210 | 211 | new = TEXT_PARAGRAPH (text_node_get_next (TEXT_NODE (fixture->para1))); 212 | 213 | // check paragraph contents 214 | text = text_paragraph_get_text (fixture->para1); 215 | g_assert_cmpstr (text, ==, "abcdefghij12345"); 216 | g_free (text); 217 | 218 | text = text_paragraph_get_text (new); 219 | g_assert_cmpstr (text, ==, "67890!@#$%^&*()"); 220 | g_free (text); 221 | 222 | text = text_paragraph_get_text (fixture->para2); 223 | g_assert_cmpstr (text, ==, "zxcvbnm,./"); 224 | g_free (text); 225 | 226 | // check lengths 227 | length = text_paragraph_get_length (fixture->para1); 228 | g_assert_cmpint (length, ==, 15); 229 | 230 | length = text_paragraph_get_length (new); 231 | g_assert_cmpint (length, ==, 15); 232 | 233 | // assert cursor is at start of new paragraph 234 | g_assert_cmpint (fixture->doc->cursor->index, ==, 0); 235 | g_assert_true (fixture->doc->cursor->paragraph == new); 236 | } 237 | 238 | int 239 | main (int argc, char *argv[]) 240 | { 241 | setlocale (LC_ALL, ""); 242 | 243 | g_test_init (&argc, &argv, NULL); 244 | 245 | // Define the tests. 246 | g_test_add ("/text-engine/editor/split/test-end-of-paragraph", SplitFixture, NULL, 247 | split_fixture_set_up, test_end_of_paragraph, 248 | split_fixture_tear_down); 249 | g_test_add ("/text-engine/editor/split/test-start-of-paragraph", SplitFixture, NULL, 250 | split_fixture_set_up, test_start_of_paragraph, 251 | split_fixture_tear_down); 252 | g_test_add ("/text-engine/editor/split/test-middle-of-paragraph", SplitFixture, NULL, 253 | split_fixture_set_up, test_middle_of_paragraph, 254 | split_fixture_tear_down); 255 | 256 | return g_test_run (); 257 | } 258 | 259 | --------------------------------------------------------------------------------