├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE-APACHE ├── LICENSE-MIT.md ├── README.md ├── book.toml ├── docs ├── _FontAwesome │ ├── css │ │ └── font-awesome.css │ └── fonts │ │ ├── FontAwesome.ttf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── ayu-highlight.css ├── book.css ├── book.js ├── clipboard.min.js ├── favicon.png ├── highlight.css ├── highlight.js ├── img │ ├── 2 │ │ ├── diagram.png │ │ └── hello_world.png │ └── 3 │ │ ├── btn_boxer.png │ │ └── btn_diagram.png ├── index.html ├── jquery.js ├── pages │ ├── 1 │ │ └── index.html │ ├── 2 │ │ └── hello_world.html │ ├── 3 │ │ ├── index.html │ │ ├── objects.html │ │ ├── programming.html │ │ ├── review.html │ │ ├── state.html │ │ └── ui.html │ ├── 4 │ │ ├── concl.html │ │ ├── entries.html │ │ ├── horrorshow.html │ │ ├── index.html │ │ ├── programming.html │ │ └── structure.html │ └── 5 │ │ ├── binding_keys.html │ │ ├── creating_ui_structure.html │ │ ├── external_state.html │ │ ├── file_choosers.html │ │ ├── index.html │ │ ├── markdown_to_html.html │ │ ├── programming.html │ │ ├── programming_open_button.html │ │ ├── programming_save_button.html │ │ ├── review_conclusion.html │ │ ├── setting_modules.html │ │ ├── source_views.html │ │ ├── ui_misc_rs.html │ │ └── webviews.html ├── print.html ├── source_code │ ├── button_boxer │ │ ├── .gitignore │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── hello_world │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── html_article │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── store.js └── tomorrow-night.css └── src ├── SUMMARY.md ├── img ├── 2 │ ├── diagram.png │ └── hello_world.png └── 3 │ ├── btn_boxer.png │ └── btn_diagram.png ├── pages ├── 1 │ └── index.md ├── 2 │ └── hello_world.md ├── 3 │ ├── index.md │ ├── objects.md │ ├── programming.md │ ├── review.md │ ├── state.md │ └── ui.md ├── 4 │ ├── concl.md │ ├── entries.md │ ├── horrorshow.md │ ├── index.md │ ├── programming.md │ └── structure.md └── 5 │ ├── binding_keys.md │ ├── creating_ui_structure.md │ ├── external_state.md │ ├── file_choosers.md │ ├── index.md │ ├── markdown_to_html.md │ ├── programming.md │ ├── programming_open_button.md │ ├── programming_save_button.md │ ├── review_conclusion.md │ ├── setting_modules.md │ ├── source_views.md │ ├── ui_misc_rs.md │ └── webviews.md └── source_code ├── button_boxer ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs ├── hello_world ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs └── html_article ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /book 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | cache: cargo 4 | rust: 5 | - stable 6 | branches: 7 | only: 8 | - master 9 | before_script: 10 | - (cargo install mdbook --force || true) 11 | script: 12 | - mdbook build 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Перевод 2 | Если вы хотите помочь с переводом, то [перейдите сюда][here], чтобы ознакомиться с оригиналом. 3 | 4 | [here]: https://github.com/mmstick/gtkrs-tutorials/ 5 | 6 | ## GitHub issue 7 | 8 | Напишите о свой проблеме, будь то грамматическая или синтаксическая ошибка, 9 | несоответствие кода коду оригинального репозитория, незначительные изменения/исправления и т.д. 10 | 11 | ## Pull Requests 12 | При создании и рецензировании Pull Request'ов следует придерживаться трёх правил: 13 | 14 | 1. Понятно описывайте свою работу. Это значит, что сообщения коммитов и Pull Request'а должны быть в правильном формате и на русском языке. 15 | 2. Адекватно реагируйте на замечания в процессе рецензирования. Рецензирование - способ поддержания качества нашего проекта. 16 | 3. Не забывайте на обновление PR по результатам рецензирования. Вы лучше всего 17 | разбираетесь в том, что вы сделали, и кроме вас никто не займётся вливанием ваших правок в проект. 18 | 19 | ## Собственные примеры/проекты 20 | Если вы хотите помочь собственными примерами, то ознакомьтесь с содержанием книги, директориями и создайте PR. 21 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ 2 | 3 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4 | 5 | 1. Definitions. 6 | 7 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 8 | 9 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 10 | 11 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 12 | 13 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 14 | 15 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 16 | 17 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 18 | 19 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 20 | 21 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 22 | 23 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 24 | 25 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 26 | 27 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 28 | 29 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 30 | 31 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 32 | 33 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 34 | 35 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 36 | 37 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 38 | 39 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 40 | 41 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 42 | 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS 54 | 55 | APPENDIX: How to apply the Apache License to your work. 56 | 57 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 58 | 59 | Copyright [yyyy] [name of copyright owner] 60 | 61 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 62 | 63 | http://www.apache.org/licenses/LICENSE-2.0 64 | 65 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 66 | -------------------------------------------------------------------------------- /LICENSE-MIT.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rust на русском 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GRBE — Gtk-Rust на примерах 2 | 3 | [![Build Status](https://travis-ci.org/ruRust/gtk-rust-by-example.svg?branch=master)](https://travis-ci.org/ruRust/gtk-rust-by-example) [![][License]](#Лицензия) 4 | 5 | [License]: https://img.shields.io/crates/l/rustc-serialize.svg 6 | 7 | Изучайте разработку GUI приложений вместе в библиотекой [GTK][gtk] на языке программирования [Rust][rust]. 8 | 9 | [rust]: https://www.rust-lang.org/ru-RU/ 10 | [gtk]: https://github.com/gtk-rs/gtk/ 11 | 12 | ## Работа в процессе 13 | 14 | Перевод `GRBE` находится в процессе. 15 | Если вы заинтересованы в этом проекте и хотите помочь - смотрите [CONTRIBUTING.md](https://github.com/ruRust/gtk-rust-by-example/blob/master/CONTRIBUTING.md). 16 | 17 | ## Использование 18 | 19 | Если вы хотите прочитать "Gtk-Rust на примерах" онлайн, вы можете посетить [эту страницу][page]. 20 | 21 | [page]: http://rurust.github.io/gtk-rust-by-example 22 | 23 | Для того, чтобы запустить локально, выполните следующее: 24 | 25 | ```bash 26 | $ git clone git@github.com:ruRust/gtk-rust-by-example.git 27 | $ cd gtk-rust-by-example 28 | $ cargo install mdbook 29 | $ mdbook build && mdbook serve 30 | ``` 31 | Откройте браузер и перейдите на `http://localhost:3000`.Теперь книгу можно читать офлайн. 32 | 33 | 34 | ## Лицензия 35 | 36 | `Gtk-Rust на примерах` распространяется по двойной лицензии: лицензия [Apache 2.0][apache] и лицензия [MIT][mit]. 37 | 38 | [apache]: https://github.com/ruRust/gtk-rust-by-example/blob/master/LICENSE-APACHE 39 | [mit]: https://github.com/ruRust/gtk-rust-by-example/blob/master/LICENSE-MIT.md 40 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "Gtk-Rust by Example" 3 | description = "A description" 4 | author = "N.Ritchie && Community" 5 | 6 | [output.html.playpen] 7 | editable = false 8 | editor = "ace" -------------------------------------------------------------------------------- /docs/_FontAwesome/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/_FontAwesome/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /docs/_FontAwesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/_FontAwesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/_FontAwesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/_FontAwesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/_FontAwesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/_FontAwesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/_FontAwesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/_FontAwesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/ayu-highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | Based off of the Ayu theme 3 | Original by Dempfi (https://github.com/dempfi/ayu) 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | overflow-x: auto; 9 | background: #191f26; 10 | color: #e6e1cf; 11 | padding: 0.5em; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-quote, 16 | .hljs-meta { 17 | color: #5c6773; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-variable, 22 | .hljs-template-variable, 23 | .hljs-attribute, 24 | .hljs-attr, 25 | .hljs-regexp, 26 | .hljs-link, 27 | .hljs-selector-id, 28 | .hljs-selector-class { 29 | color: #ff7733; 30 | } 31 | 32 | .hljs-number, 33 | .hljs-builtin-name, 34 | .hljs-literal, 35 | .hljs-type, 36 | .hljs-params { 37 | color: #ffee99; 38 | } 39 | 40 | .hljs-string, 41 | .hljs-bullet { 42 | color: #b8cc52; 43 | } 44 | 45 | .hljs-title, 46 | .hljs-built_in, 47 | .hljs-section { 48 | color: #ffb454; 49 | } 50 | 51 | .hljs-keyword, 52 | .hljs-selector-tag, 53 | .hljs-symbol { 54 | color: #ff7733; 55 | } 56 | 57 | .hljs-name { 58 | color: #36a3d9; 59 | } 60 | 61 | .hljs-tag { 62 | color: #00568d; 63 | } 64 | 65 | .hljs-emphasis { 66 | font-style: italic; 67 | } 68 | 69 | .hljs-strong { 70 | font-weight: bold; 71 | } 72 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/favicon.png -------------------------------------------------------------------------------- /docs/highlight.css: -------------------------------------------------------------------------------- 1 | /* Base16 Atelier Dune Light - Theme */ 2 | /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */ 3 | /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ 4 | 5 | /* Atelier-Dune Comment */ 6 | .hljs-comment, 7 | .hljs-quote { 8 | color: #AAA; 9 | } 10 | 11 | /* Atelier-Dune Red */ 12 | .hljs-variable, 13 | .hljs-template-variable, 14 | .hljs-attribute, 15 | .hljs-tag, 16 | .hljs-name, 17 | .hljs-regexp, 18 | .hljs-link, 19 | .hljs-name, 20 | .hljs-selector-id, 21 | .hljs-selector-class { 22 | color: #d73737; 23 | } 24 | 25 | /* Atelier-Dune Orange */ 26 | .hljs-number, 27 | .hljs-meta, 28 | .hljs-built_in, 29 | .hljs-builtin-name, 30 | .hljs-literal, 31 | .hljs-type, 32 | .hljs-params { 33 | color: #b65611; 34 | } 35 | 36 | /* Atelier-Dune Green */ 37 | .hljs-string, 38 | .hljs-symbol, 39 | .hljs-bullet { 40 | color: #60ac39; 41 | } 42 | 43 | /* Atelier-Dune Blue */ 44 | .hljs-title, 45 | .hljs-section { 46 | color: #6684e1; 47 | } 48 | 49 | /* Atelier-Dune Purple */ 50 | .hljs-keyword, 51 | .hljs-selector-tag { 52 | color: #b854d4; 53 | } 54 | 55 | .hljs { 56 | display: block; 57 | overflow-x: auto; 58 | background: #f1f1f1; 59 | color: #6e6b5e; 60 | padding: 0.5em; 61 | } 62 | 63 | .hljs-emphasis { 64 | font-style: italic; 65 | } 66 | 67 | .hljs-strong { 68 | font-weight: bold; 69 | } 70 | -------------------------------------------------------------------------------- /docs/img/2/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/img/2/diagram.png -------------------------------------------------------------------------------- /docs/img/2/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/img/2/hello_world.png -------------------------------------------------------------------------------- /docs/img/3/btn_boxer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/img/3/btn_boxer.png -------------------------------------------------------------------------------- /docs/img/3/btn_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/docs/img/3/btn_diagram.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Введение - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 63 | 64 | 67 | 68 |
69 | 70 |
71 | 72 | 86 | 87 |
88 |

Gtk-Rust на примерах

89 |

Gtk-Rust by Example (GRBE) представляет собой неофициальную книгу по разработке GTK GUI, на языке программирования Rust, которая будет демонстрировать практическое использование библиотеки GTK на различных примерах.

90 |

Перед тем как начать, укажите версию библиотеки в вашем Cargo.toml файле:

91 |
[dependencies.gtk]
 92 | version = "0.3.0"
 93 | features = ["v3_22"]
 94 | 
95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 105 | 106 | 107 |
108 | 109 | 110 | 111 | 112 | 115 | 116 | 117 |
118 | 119 | 120 | 121 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /docs/pages/1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Введение - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 |
89 |

Gtk-Rust на примерах

90 |

Gtk-Rust by Example (GRBE) представляет собой неофициальную книгу по разработке GTK GUI, на языке программирования Rust, которая будет демонстрировать практическое использование библиотеки GTK на различных примерах.

91 |

Перед тем как начать, укажите версию библиотеки в вашем Cargo.toml файле:

92 |
[dependencies.gtk]
 93 | version = "0.3.0"
 94 | features = ["v3_22"]
 95 | 
96 | 97 |
98 | 99 | 100 | 101 | 102 | 103 | 106 | 107 | 108 |
109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 |
119 | 120 | 121 | 122 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/pages/3/review.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Заключение и обзор - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 |
89 |

Заключение и обзор

90 |

Если вы нажмёте на кнопку Ударить, счётчик должен уменьшиться и сообщение должно измениться. Нажатие по кнопке Лечить должно увеличить счётчик и также изменить сообщение.После запуска вашей программы с помощью cargo run, вы должны увидеть окно, которое выглядит так:

91 |

92 |

На этом этапе, вы должны хорошо понимать как работают: GtkBox, GtkButton и GtkLabel. Вы можете вернуться к предыдущему разделу, чтобы ещё раз уточнить некоторые моменты.

93 |

Практическое занятие

94 |

Setting Inputs w/ Buttons

95 |

There isn't much that you can do with just buttons and labels. If you want a practice challenge, try creating a program that displays a simple random math problem, and asks the user to use buttons to set the value. If they get it correct, modify a label to tell the user that what they entered was correct. This is an incredibly annoying interface design, so don't do this in the real world! 96 | Bonus: Timed Answers

97 |

Do the same as the above, but also take advantage of gtk::timeout_add() to decrement and update a timer label within the UI until the timer reaches zero.

98 | 99 |
100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 112 | 113 | 114 |
115 | 116 | 117 | 120 | 121 | 122 | 123 | 126 | 127 | 128 |
129 | 130 | 131 | 132 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /docs/pages/4/concl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Заключение и обзор - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 |
89 |

Заключение и обзор

90 |

head_pic

91 |

К данному времени у вас должно сложиться хорошее представление о том, как писать 92 | программы с использованием библиотеки GTK на Rust. К текущему моменту вы должны 93 | знать, как делать следующее:

94 |
    95 |
  • создавать, получать, присваивать значения объектам Label
  • 96 |
  • создавать, получать, присваивать значения объектам Entries
  • 97 |
  • создавать, получать, присваивать значения объектам TextBuffer
  • 98 |
  • создавать, присваивать буферы объектам TextViews
  • 99 |
  • создавать, настраивать стили внешнего вида, программировать объекты Button
  • 100 |
  • присваивать элементы объектам Box и контейнерам Paned
  • 101 |
  • устанавливать отступы и поля на виджетах
  • 102 |
  • работать с внешним состоянием
  • 103 |
104 |

Практические задачи

105 | 106 |
107 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 119 | 120 | 121 |
122 | 123 | 124 | 127 | 128 | 129 | 130 | 133 | 134 | 135 |
136 | 137 | 138 | 139 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /docs/pages/4/horrorshow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML-шаблоны Horrorshow - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 |
89 |

Horrorshow HTML-шаблоны

90 |

Хотя это и не связано с разработкой c использованием GTK, пакет horrorshow 91 | предоставляет удобные макроопределения, которые дают возможность эффективно 92 | генерировать HTML-строки в памяти, используя DSL (domain-specific language) 93 | совместно с Rust, который может быть запущен посредством использования символа 94 | (sigil) @.

95 |

 96 | # #![allow(unused_variables)]
 97 | #fn main() {
 98 | #[macro_use]
 99 | extern crate horrorshow;
100 | use horrorshow::helper::doctype;
101 | 
102 | let title = "Title";
103 | let content = "A string\nwith multiple\n\nlines";
104 | let html_string = format!(
105 |     "{}",
106 |     html!{
107 |         : doctype::HTML,
108 |         html {
109 |             head {
110 |                 style { : "#style { }" }
111 |             }
112 |             body {
113 |                 h1(id="style") { : title }
114 |                 @ for line in content.lines().filter(|x| !x.is_empty()) {
115 |                     p { : line }
116 |                 }
117 |             }
118 |         }
119 |     }
120 | );
121 | #}
122 | 123 |
124 | 125 | 126 | 127 | 130 | 131 | 132 | 133 | 136 | 137 | 138 |
139 | 140 | 141 | 144 | 145 | 146 | 147 | 150 | 151 | 152 |
153 | 154 | 155 | 156 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /docs/pages/4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Составитель статей из HTML - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 |
89 |

Составитель статей из HTML

90 |

head_pic

91 |
92 |

Исходный код для этой главы находится здесь.

93 |
94 |

В данной главе вы начнете писать полезные программы, используя поля для ввода 95 | и просмотра текста, для того чтобы дать возможность пользователю вводить нужные 96 | значения в текстовые поля и генерировать выходные значения из введенных 97 | значений после нажатия на кнопку. В добавок вы познакомитесь с 98 | макроопределением html!, который находится внутри пакета horrorshow. Вы 99 | будете писать программу, которая принимает введенные значения на левой панели 100 | и генерирует HTML на правом поле для просмотра текста.

101 |
102 |

Обратите внимание на то что, что в данной главе мы не рассматриваем вопрос 103 | хранения данных во внешнем источнике, как это было в прошлой главе. Все состояние, 104 | которое нас интересует, находится внутри GTK объектов, с которыми мы будем 105 | взаимодействовать.

106 |
107 | 108 |
109 | 110 | 111 | 112 | 115 | 116 | 117 | 118 | 121 | 122 | 123 |
124 | 125 | 126 | 129 | 130 | 131 | 132 | 135 | 136 | 137 |
138 | 139 | 140 | 141 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /docs/pages/5/review_conclusion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Обзор и подведение итогов - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 101 | 102 |
103 | 104 | 105 | 108 | 109 | 110 | 111 | 112 |
113 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/pages/5/ui_misc_rs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Модуль ui/misc.rs - Gtk-Rust by Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 64 | 65 | 68 | 69 |
70 | 71 |
72 | 73 | 87 | 88 |
89 |

Модуль ui/misc.rs

90 |

Мы реализуем несколько вспомогательных методов, которые при необходимости 91 | будут использоваться в проекте. Это две функции: одна для извлечения текста из 92 | GtkSourceBuffer, другая для установки названия у GtkHeaderBar с 93 | заданным Path.

94 |

 95 | # #![allow(unused_variables)]
 96 | #fn main() {
 97 | use gtk::*;
 98 | use sourceview::*;
 99 | use std::path::Path;
100 | 
101 | /// Присвоить заголовку в заголовочной панели ссылку на строковое
102 | /// представление пути к файлу. 
103 | pub fn set_title(headerbar: &HeaderBar, path: &Path) {
104 |     if let Some(filename) = path.file_name() {
105 |         let filename: &str = &filename.to_string_lossy();
106 |         headerbar.set_title(filename);
107 |     }
108 | }
109 | 
110 | /// Получить все внутреннее содержимое данного текстового буфера в виде
111 | /// строки.
112 | pub fn get_buffer(buffer: &Buffer) -> Option<String> {
113 |     let start = buffer.get_start_iter();
114 |     let end = buffer.get_end_iter();
115 |     buffer.get_text(&start, &end, true)
116 | }
117 | #}
118 | 119 |
120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 132 | 133 | 134 |
135 | 136 | 137 | 140 | 141 | 142 | 143 | 146 | 147 | 148 |
149 | 150 | 151 | 152 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /docs/source_code/button_boxer/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /docs/source_code/button_boxer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "button_boxer" 3 | version = "0.1.0" 4 | authors = ["Norman Ritchie "] 5 | 6 | [dependencies] 7 | 8 | [dependencies.gtk] 9 | version = "0.3.0" 10 | features = ["v3_22"] 11 | 12 | -------------------------------------------------------------------------------- /docs/source_code/button_boxer/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | use gtk::*; 3 | use std::process; 4 | use std::sync::Arc; 5 | use std::sync::atomic::{AtomicUsize, Ordering}; 6 | 7 | // Заданные сообщения, которые будут использоваться в UI 8 | // при определённых условиях. 9 | const MESSAGES: [&str; 3] = [ 10 | "Ой! Ты ударил меня!", 11 | "...", 12 | "Спасибо!", 13 | ]; 14 | 15 | #[repr(u8)] 16 | // Типаж с типом `u8`, который используется как индекс в массиве `MESSAGES`. 17 | enum Message { 18 | Hit, 19 | Dead, 20 | Heal, 21 | } 22 | 23 | fn main() { 24 | // Инициализируем GTK перед продолжением. 25 | if gtk::init().is_err() { 26 | eprintln!("failed to initialize GTK Application"); 27 | process::exit(1); 28 | } 29 | 30 | /* Установим начальное состояние для нашего компонента - `health`. 31 | * Воспользуемся `Arc`, для того, чтобы мы могли 32 | * использовать несколько programmable замыканий. 33 | */ 34 | let health = Arc::new(HealthComponent::new(10)); 35 | 36 | // Инициализируем начальное состояние UI. 37 | let app = App::new(&health); 38 | 39 | { 40 | // Запрограммируем кнопку `Ударить` чтобы уменьшить здоровье. 41 | let health = health.clone(); 42 | let message = app.content.message.clone(); 43 | let info = app.content.health.clone(); 44 | app.header.hit.connect_clicked(move |_| { 45 | let new_health = health.subtract(1); 46 | let action = if new_health == 0 { 47 | Message::Dead 48 | } else { 49 | Message::Hit 50 | }; 51 | message.set_label(MESSAGES[action as usize]); 52 | info.set_label(new_health.to_string().as_str()); 53 | }); 54 | } 55 | 56 | { 57 | // Запрограммируем кнопку `Лечить`, чтобы вернуть очки здоровья. 58 | let health = health.clone(); 59 | let message = app.content.message.clone(); 60 | let info = app.content.health.clone(); 61 | app.header.heal.connect_clicked(move |_| { 62 | let new_health = health.heal(5); 63 | message.set_label(MESSAGES[Message::Heal as usize]); 64 | info.set_label(new_health.to_string().as_str()); 65 | }); 66 | } 67 | 68 | // Сделаем все виджеты видимыми в UI. 69 | app.window.show_all(); 70 | 71 | // Запуск основного цикла GTK. 72 | gtk::main(); 73 | } 74 | 75 | pub struct HealthComponent(AtomicUsize); 76 | 77 | impl HealthComponent { 78 | fn new(initial: usize) -> HealthComponent { 79 | HealthComponent(AtomicUsize::new(initial)) 80 | } 81 | 82 | fn get_health(&self) -> usize { 83 | self.0.load(Ordering::SeqCst) 84 | } 85 | 86 | fn subtract(&self, value: usize) -> usize { 87 | let current = self.0.load(Ordering::SeqCst); 88 | let new = if current < value { 0 } else { current - value }; 89 | self.0.store(new, Ordering::SeqCst); 90 | new 91 | } 92 | 93 | fn heal(&self, value: usize) -> usize { 94 | let original = self.0.fetch_add(value, Ordering::SeqCst); 95 | original + value 96 | } 97 | } 98 | 99 | pub struct App { 100 | pub window: Window, 101 | pub header: Header, 102 | pub content: Content, 103 | } 104 | 105 | pub struct Header { 106 | pub container: HeaderBar, 107 | pub hit: Button, 108 | pub heal: Button, 109 | } 110 | 111 | pub struct Content { 112 | pub container: Box, 113 | pub health: Label, 114 | pub message: Label, 115 | } 116 | 117 | impl Content { 118 | fn new(health: &HealthComponent) -> Content { 119 | // Создадим вертикальную упаковку, чтобы хранить там все дочерние элементы. 120 | let container = Box::new(Orientation::Vertical, 0); 121 | 122 | // Информация о здоровье будет храниться в горизонтальной упаковке вместе с вертикальной. 123 | let health_info = Box::new(Orientation::Horizontal, 0); 124 | let health_label = Label::new("Текущее значение здоровья:"); 125 | let health = Label::new(health.get_health().to_string().as_str()); 126 | 127 | // Установим горизонтальное выравнивание для наших объектов. 128 | health_info.set_halign(Align::Center); 129 | health_label.set_halign(Align::Start); 130 | health.set_halign(Align::Start); 131 | 132 | // Добивим информацию о здоровье в дочернюю коробку. 133 | health_info.pack_start(&health_label, false, false, 5); 134 | health_info.pack_start(&health, true, true, 5); 135 | 136 | /* 137 | * Создадим метку, которая будет изменяться приложением 138 | * при выполнении удара или лечения. 139 | */ 140 | let message = Label::new("Привет"); 141 | 142 | // Добавим все в нашу вертикальную коробку. 143 | container.pack_start(&health_info, true, false, 0); 144 | container.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0); 145 | container.pack_start(&message, true, false, 0); 146 | 147 | Content { 148 | container, 149 | health, 150 | message, 151 | } 152 | } 153 | } 154 | 155 | impl App { 156 | fn new(health: &HealthComponent) -> App { 157 | // Создадим новое окно с типом `Toplevel`. 158 | let window = Window::new(WindowType::Toplevel); 159 | // Создадим заголовок и связанное с ним содержимое. 160 | let header = Header::new(); 161 | // Расположим содержимое в окне. 162 | let content = Content::new(health); 163 | 164 | // Set the headerbar as the title bar widget. 165 | window.set_titlebar(&header.container); 166 | // Установим описание для окна. 167 | window.set_title("Боксирующие кнопки"); 168 | // Установим класс для оконного менеджера. 169 | window.set_wmclass("app-name", "Боксирующие кнопки"); 170 | // Установим иконку, отображаемую приложением. 171 | Window::set_default_icon_name("имя-иконки"); 172 | // Добавим коробку с содержимым в окно. 173 | window.add(&content.container); 174 | 175 | // Запрограммируем выход из программы при нажатии кнопки. 176 | window.connect_delete_event(move |_, _| { 177 | main_quit(); 178 | Inhibit(false) 179 | }); 180 | 181 | // Вернём состояние нашего приложения. 182 | App { 183 | window, 184 | header, 185 | content, 186 | } 187 | } 188 | } 189 | 190 | impl Header { 191 | fn new() -> Header { 192 | // Создадим главный заголовочный бар содержащий виджет. 193 | let container = HeaderBar::new(); 194 | 195 | // Установим текст для отображения в секции для названия. 196 | container.set_title("Боксирующие кнопки"); 197 | // Сделаем активными элементы управления окна в этой панели. 198 | container.set_show_close_button(true); 199 | 200 | // Создадим кнопки: `ударить` и `лечить`. 201 | let hit = Button::new_with_label("Ударить"); 202 | let heal = Button::new_with_label("Лечить"); 203 | 204 | // Добавим соответствующие классы стилей к этим кнопкам. 205 | hit.get_style_context() 206 | .map(|c| c.add_class("destructive-action")); 207 | heal.get_style_context() 208 | .map(|c| c.add_class("suggested-action")); 209 | 210 | // Теперь добавим их в панель заголовка. 211 | container.pack_start(&hit); 212 | container.pack_end(&heal); 213 | 214 | // Вернём the header and all of it's state 215 | Header { 216 | container, 217 | hit, 218 | heal, 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /docs/source_code/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | version = "0.1.0" 4 | authors = ["Norman Ritchie "] 5 | 6 | [dependencies] 7 | 8 | [dependencies.gtk] 9 | version = "0.3.0" 10 | features = ["v3_22"] 11 | -------------------------------------------------------------------------------- /docs/source_code/hello_world/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | 3 | use gtk::*; 4 | 5 | // Объявим структуру `Application`. 6 | pub struct Application { 7 | pub window: Window, 8 | pub header: Header, 9 | } 10 | 11 | // Объявим структуру `Header`. 12 | pub struct Header { 13 | pub container: HeaderBar, 14 | } 15 | 16 | // Блок реализации. 17 | impl Application { 18 | fn new() -> Application { 19 | // Создадим новое окно с типом `Toplevel`. 20 | let window = Window::new(WindowType::Toplevel); 21 | // Создадим header bar и и связанный с ним контент. 22 | let header = Header::new(); 23 | 24 | // Укажем название заголовка виджета. 25 | window.set_titlebar(&header.container); 26 | // Укажем название для окна приложения. 27 | window.set_title("Простая программа"); 28 | // Установим класс для оконного менеджера. 29 | window.set_wmclass("simple-gtk", "Простая программа"); 30 | // Установим иконку, отображаемую приложением. 31 | Window::set_default_icon_name("имя иконки"); 32 | 33 | // Программа закроется, если нажата кнопка выхода. 34 | window.connect_delete_event(move |_, _| { 35 | main_quit(); 36 | Inhibit(false) 37 | }); 38 | 39 | // Возвращаем основное состояние приложения. 40 | Application { window, header } 41 | } 42 | } 43 | 44 | impl Header { 45 | fn new() -> Header { 46 | // Создадим виджет контейнера для главной панели заголовка. 47 | let container = HeaderBar::new(); 48 | // Установим отображаемый тект в секции для названия. 49 | container.set_title("Простая программа"); 50 | // Делаем активными элементы управления окна в этой панели. 51 | container.set_show_close_button(true); 52 | 53 | // Возвращаем заголовок и его состояние. 54 | Header { container } 55 | } 56 | } 57 | 58 | fn main() { 59 | // Инициализация GTK. 60 | if gtk::init().is_err() { 61 | eprintln!("Не удалось инициализировать GTK приложение."); 62 | return; 63 | } 64 | 65 | // Инициализация начального состояния UI. 66 | let app = Application::new(); 67 | 68 | // Делаем видимыми все виджеты с UI. 69 | app.window.show_all(); 70 | 71 | // Запуск основного цикла GTK. 72 | gtk::main(); 73 | } 74 | -------------------------------------------------------------------------------- /docs/source_code/html_article/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "html_article" 3 | version = "0.1.0" 4 | authors = ["Bulat Musin "] 5 | 6 | [dependencies] 7 | 8 | [dependencies.gtk] 9 | version = "0.3.0" 10 | features = ["v3_22"] 11 | 12 | -------------------------------------------------------------------------------- /docs/source_code/html_article/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | #[macro_use] 3 | extern crate horrorshow; 4 | 5 | use gtk::*; 6 | use std::process; 7 | 8 | fn main() { 9 | // Initialize GTK before proceeding. 10 | if gtk::init().is_err() { 11 | eprintln!("failed to initialize GTK Application"); 12 | process::exit(1); 13 | } 14 | 15 | // Initialize the UI's initial state 16 | let app = App::new(); 17 | 18 | { 19 | // Program the post button to take the inputs in the left pane, and update HTML code 20 | // within the right pane accordingly. Prepared to increment reference counters... 21 | let title = app.content.title.clone(); 22 | let tags = app.content.tags.clone(); 23 | let content = app.content.content.clone(); 24 | let right_pane = app.content.right_pane.clone(); 25 | app.header.post.connect_clicked(move |_| { 26 | let inputs = (title.get_text(), tags.get_text(), get_buffer(&content)); 27 | if let (Some(title), Some(tags), Some(content)) = inputs { 28 | right_pane.set_text(&generate_html(&title, &tags, &content)); 29 | } 30 | }); 31 | } 32 | 33 | // Make all the widgets within the UI visible. 34 | app.window.show_all(); 35 | 36 | // Start the GTK main event loop 37 | gtk::main(); 38 | } 39 | 40 | /// Obtain the entire text buffer's contents as a string. 41 | fn get_buffer(buffer: &TextBuffer) -> Option { 42 | let start = buffer.get_start_iter(); 43 | let end = buffer.get_end_iter(); 44 | buffer.get_text(&start, &end, true) 45 | } 46 | 47 | /// Generates the minified HTML that will be displayed in the right pane 48 | fn generate_html(title: &str, tags: &str, content: &str) -> String { 49 | format!{ 50 | "{}", 51 | html!{ 52 | article { 53 | header { 54 | h1 { : &title } 55 | div(class="tags") { 56 | @ for tag in tags.split(':') { 57 | div(class="tag") { : tag } 58 | } 59 | } 60 | } 61 | @ for line in content.lines().filter(|x| !x.is_empty()) { 62 | p { : line } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | pub struct App { 70 | pub window: Window, 71 | pub header: Header, 72 | pub content: Content, 73 | } 74 | 75 | pub struct Header { 76 | pub container: HeaderBar, 77 | pub post: Button, 78 | } 79 | 80 | pub struct Content { 81 | pub container: Paned, 82 | pub title: Entry, 83 | pub tags: Entry, 84 | pub content: TextBuffer, 85 | pub right_pane: TextBuffer, 86 | } 87 | 88 | impl App { 89 | fn new() -> App { 90 | // Create a new top level window. 91 | let window = Window::new(WindowType::Toplevel); 92 | // Create a the headerbar and it's associated content. 93 | let header = Header::new(); 94 | // Create the main content. 95 | let content = Content::new(); 96 | 97 | // Set the headerbar as the title bar widget. 98 | window.set_titlebar(&header.container); 99 | // Set the title of the window. 100 | window.set_title("HTML Articler"); 101 | // Set the window manager class. 102 | window.set_wmclass("html-articler", "HTML Articler"); 103 | // The icon the app will display. 104 | Window::set_default_icon_name("iconname"); 105 | // Set the default size of the window. 106 | window.set_default_size(800, 600); 107 | // Add the content to the window. 108 | window.add(&content.container); 109 | 110 | // Programs what to do when the exit button is used. 111 | window.connect_delete_event(move |_, _| { 112 | main_quit(); 113 | Inhibit(false) 114 | }); 115 | 116 | // Return our main application state 117 | App { window, header, content } 118 | } 119 | } 120 | 121 | impl Header { 122 | fn new() -> Header { 123 | // Creates the main header bar container widget. 124 | let container = HeaderBar::new(); 125 | 126 | // Sets the text to display in the title section of the header bar. 127 | container.set_title("HTML Articler"); 128 | // Enable the window controls within this headerbar. 129 | container.set_show_close_button(true); 130 | 131 | // Create a button that will post the HTML article. 132 | let post = Button::new_with_label("Post"); 133 | post.get_style_context().map(|x| x.add_class("suggested-action")); 134 | 135 | container.pack_end(&post); 136 | 137 | // Returns the header and all of it's state 138 | Header { container, post } 139 | } 140 | } 141 | 142 | impl Content { 143 | fn new() -> Content { 144 | // The main container will hold a left and right pane. The left pane is for user input, 145 | // whereas the right pane is for the generated output. 146 | let container = Paned::new(Orientation::Horizontal); 147 | let left_pane = Box::new(Orientation::Vertical, 5); 148 | let right_pane = TextBuffer::new(None); 149 | let right_pane_view = TextView::new_with_buffer(&right_pane); 150 | 151 | // The left pane will consist of a title entry, tags entry, and content text view. 152 | let title = Entry::new(); 153 | let tags = Entry::new(); 154 | let content = TextBuffer::new(None); 155 | let content_view = TextView::new_with_buffer(&content); 156 | 157 | // The label that we will display above the content box to describe it. 158 | let content_label = Label::new("Content"); 159 | content_label.set_halign(Align::Center); 160 | 161 | // Set placeholders within the entries to hint the user of the contents to enter. 162 | title.set_placeholder_text("Insert Title"); 163 | tags.set_placeholder_text("Insert Colon-Delimited Tags"); 164 | 165 | // Additionally set tooltips on the entries. Note that you may use either text or markup. 166 | title.set_tooltip_text("Insert the title of article here"); 167 | tags.set_tooltip_markup("tag_one:tag two: tag three"); 168 | 169 | // The right pane should disallow editing; and both editors should wrap by word. 170 | right_pane_view.set_editable(false); 171 | right_pane_view.set_wrap_mode(WrapMode::Word); 172 | content_view.set_wrap_mode(WrapMode::Word); 173 | 174 | // Wrap the text views within scrolled windows, so that they can scroll. 175 | let content_scroller = ScrolledWindow::new(None, None); 176 | let right_pane_scrolled = ScrolledWindow::new(None, None); 177 | content_scroller.add(&content_view); 178 | right_pane_scrolled.add(&right_pane_view); 179 | 180 | // Paddin' Widgets 181 | left_pane.set_border_width(5); 182 | right_pane_view.set_left_margin(5); 183 | right_pane_view.set_right_margin(5); 184 | right_pane_view.set_top_margin(5); 185 | right_pane_view.set_bottom_margin(5); 186 | content_view.set_left_margin(5); 187 | content_view.set_right_margin(5); 188 | content_view.set_top_margin(5); 189 | content_view.set_bottom_margin(5); 190 | 191 | // First add everything to the left pane box. 192 | left_pane.pack_start(&title, false, true, 0); 193 | left_pane.pack_start(&tags, false, true, 0); 194 | left_pane.pack_start(&content_label, false, false, 0); 195 | left_pane.pack_start(&content_scroller, true, true, 0); 196 | 197 | // Then add the left and right panes into the container 198 | container.pack1(&left_pane, true, true); 199 | container.pack2(&right_pane_scrolled, true, true); 200 | 201 | Content { container, title, tags, content, right_pane } 202 | } 203 | } -------------------------------------------------------------------------------- /docs/tomorrow-night.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Night Theme */ 2 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 3 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 4 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 5 | 6 | /* Tomorrow Comment */ 7 | .hljs-comment { 8 | color: #969896; 9 | } 10 | 11 | /* Tomorrow Red */ 12 | .hljs-variable, 13 | .hljs-attribute, 14 | .hljs-tag, 15 | .hljs-regexp, 16 | .ruby .hljs-constant, 17 | .xml .hljs-tag .hljs-title, 18 | .xml .hljs-pi, 19 | .xml .hljs-doctype, 20 | .html .hljs-doctype, 21 | .css .hljs-id, 22 | .css .hljs-class, 23 | .css .hljs-pseudo { 24 | color: #cc6666; 25 | } 26 | 27 | /* Tomorrow Orange */ 28 | .hljs-number, 29 | .hljs-preprocessor, 30 | .hljs-pragma, 31 | .hljs-built_in, 32 | .hljs-literal, 33 | .hljs-params, 34 | .hljs-constant { 35 | color: #de935f; 36 | } 37 | 38 | /* Tomorrow Yellow */ 39 | .ruby .hljs-class .hljs-title, 40 | .css .hljs-rule .hljs-attribute { 41 | color: #f0c674; 42 | } 43 | 44 | /* Tomorrow Green */ 45 | .hljs-string, 46 | .hljs-value, 47 | .hljs-inheritance, 48 | .hljs-header, 49 | .hljs-name, 50 | .ruby .hljs-symbol, 51 | .xml .hljs-cdata { 52 | color: #b5bd68; 53 | } 54 | 55 | /* Tomorrow Aqua */ 56 | .hljs-title, 57 | .css .hljs-hexcolor { 58 | color: #8abeb7; 59 | } 60 | 61 | /* Tomorrow Blue */ 62 | .hljs-function, 63 | .python .hljs-decorator, 64 | .python .hljs-title, 65 | .ruby .hljs-function .hljs-title, 66 | .ruby .hljs-title .hljs-keyword, 67 | .perl .hljs-sub, 68 | .javascript .hljs-title, 69 | .coffeescript .hljs-title { 70 | color: #81a2be; 71 | } 72 | 73 | /* Tomorrow Purple */ 74 | .hljs-keyword, 75 | .javascript .hljs-function { 76 | color: #b294bb; 77 | } 78 | 79 | .hljs { 80 | display: block; 81 | overflow-x: auto; 82 | background: #1d1f21; 83 | color: #c5c8c6; 84 | padding: 0.5em; 85 | -webkit-text-size-adjust: none; 86 | } 87 | 88 | .coffeescript .javascript, 89 | .javascript .xml, 90 | .tex .hljs-formula, 91 | .xml .javascript, 92 | .xml .vbscript, 93 | .xml .css, 94 | .xml .hljs-cdata { 95 | opacity: 0.5; 96 | } 97 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Введение](pages/1/index.md) 4 | - [Hello World](pages/2/hello_world.md) 5 | - [Боксирующие кнопки](pages/3/index.md) 6 | - [Упаковки, кнопки и метки](pages/3/objects.md) 7 | - [Сопровождение внешнего состояния](pages/3/state.md) 8 | - [Создание структуры UI](pages/3/ui.md) 9 | - [Программирование UI](pages/3/programming.md) 10 | - [Заключение и обзор](pages/3/review.md) 11 | - [Составитель статей из HTML](pages/4/index.md) 12 | - [Поля, панели, прокручиваемые окна](pages/4/entries.md) 13 | - [HTML-шаблоны Horrorshow](pages/4/horrorshow.md) 14 | - [Создание структуры UI](pages/4/structure.md) 15 | - [Программирование UI](pages/4/programming.md) 16 | - [Заключение и обзор](pages/4/concl.md) 17 | - [Простой редактор Common Mark](pages/5/index.md) 18 | - [Выбор файла, просмотр кода и web-страниц](pages/5/source_views.md) 19 | - [Настройка модулей](pages/5/setting_modules.md) 20 | - [Модуль ui/misc.rs](pages/5/ui_misc_rs.md) 21 | - [Создание структуры пользовательского интерфейса](pages/5/creating_ui_structure.md) 22 | - [Работа с внешним состоянием](pages/5/external_state.md) 23 | - [Создание событий](pages/5/programming.md) 24 | - [Markdown в HTML](pages/5/markdown_to_html.md) 25 | - [Обновление WebViews](pages/5/webviews.md) 26 | - [Диалог выбора файла](pages/5/file_choosers.md) 27 | - [Программирование кнопки Open](pages/5/programming_open_button.md) 28 | - [Программирование кнопки Save](pages/5/programming_save_button.md) 29 | - [Обработка сочетаний клавиш](pages/5/binding_keys.md) 30 | - [Обзор и подведение итогов](pages/5/review_conclusion.md) 31 | -------------------------------------------------------------------------------- /src/img/2/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/src/img/2/diagram.png -------------------------------------------------------------------------------- /src/img/2/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/src/img/2/hello_world.png -------------------------------------------------------------------------------- /src/img/3/btn_boxer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/src/img/3/btn_boxer.png -------------------------------------------------------------------------------- /src/img/3/btn_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang-ru/gtk-rust-by-example/6744a7e35143056e52c912fb5ad46f8857e4f18c/src/img/3/btn_diagram.png -------------------------------------------------------------------------------- /src/pages/1/index.md: -------------------------------------------------------------------------------- 1 | # Gtk-Rust на примерах 2 | 3 | Gtk-Rust by Example (GRBE) представляет собой неофициальную книгу по разработке GTK GUI, на [языке программирования Rust][rust], которая будет демонстрировать практическое использование [библиотеки GTK][gtk] на различных примерах. 4 | 5 | [rust]: https://www.rust-lang.org/ru-RU/ 6 | [gtk]: https://github.com/gtk-rs/gtk/ 7 | 8 | Перед тем как начать, укажите версию библиотеки в вашем `Cargo.toml` файле: 9 | ``` 10 | [dependencies.gtk] 11 | version = "0.3.0" 12 | features = ["v3_22"] 13 | ``` 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/2/hello_world.md: -------------------------------------------------------------------------------- 1 | # Окно с панелью заголовка 2 | 3 | 4 | 5 | > Исходный код программы для этой главы можно найти [здесь](https://github.com/ruRust/gtk-rust-by-example/tree/master/src/source_code/hello_world). 6 | 7 | В этой главе мы создадим простое GTK приложение с панелью заголовка. Этот пример познакомит Вас с основами создания структуры приложения, которое будет содержать виджет и окно с заголовком. 8 | 9 | ## Создание структуры приложения 10 | 11 | Следующая диаграмма показывает, как мы будет проектировать структуру для нашего GTK приложения. 12 | Можно и не следовать этой диаграмме для достижения главного результата, однако, проектирование структуры пользовательского интерфейса может быть полезной пищей для ума. 13 | 14 | 15 | 16 | В примере ниже, мы создадим структуру с **GtkWindow** и **Header**: 17 | 18 | ```rust 19 | extern crate gtk; 20 | 21 | use gtk::*; 22 | 23 | // Объявим структуру `Application`. 24 | pub struct Application { 25 | pub window: Window, 26 | pub header: Header, 27 | } 28 | ``` 29 | 30 | **Header** - это еще одна структура, которая будет содержать **GtkHeaderbar** и все виджеты внутри этой панели заголовка. 31 | 32 | ```rust 33 | // Объявим структуру `Header`. 34 | pub struct Header { 35 | pub container: HeaderBar, 36 | } 37 | ``` 38 | 39 | ## Создание пользовательского интерфейса (UI) 40 | 41 | Далее, мы создадим интерфейс с помощью Rust и расположим его в "ново-объявленных" структурах. 42 | 43 | Во-первых, структура нашего приложения будет содержать в себе все другие структуры нашего пользовательского интерфейса. В примере ниже находится код с комментариями, описывающие каждый метод, который мы будем использовать для конфигурации. 44 | 45 | Мы создадим **GtkWindow** и "присоединим" каждый элемент UI. Структура `Header` будет располагать в себе другие **GtkHeaderBar**. Далее, напишем функцию для выхода из приложения. 46 | Важно также установить описание для окна и `wmclass`, который будет отображаться оконными менеджерами, а с помощью функции `Window::set_default_icon_name()` настроим отображаемую иконку. 47 | 48 | ```rust 49 | // Блок реализации. 50 | impl Application { 51 | fn new() -> Application { 52 | // Создадим новое окно с типом `Toplevel`. 53 | let window = Window::new(WindowType::Toplevel); 54 | // Создадим панель заголовка и связанный с ним контент. 55 | let header = Header::new(); 56 | 57 | // Укажем название заголовка виджета. 58 | window.set_titlebar(&header.container); 59 | // Укажем название для окна приложения. 60 | window.set_title("Простая программа"); 61 | // Установим класс для оконного менеджера. 62 | window.set_wmclass("simple-gtk", "Простая программа"); 63 | // Установим иконку, отображаемую приложением. 64 | Window::set_default_icon_name("имя_иконки"); 65 | 66 | // Программа закроется, если нажата кнопка выхода. 67 | window.connect_delete_event(move |_, _| { 68 | main_quit(); 69 | Inhibit(false) 70 | }); 71 | 72 | // Возвращаем основное состояние приложения. 73 | Application { window, header } 74 | } 75 | } 76 | ``` 77 | Здесь располагается блок реализации для структуры **Header**, которая на данный момент содержит только **GtkHeaderBar**. Важно указать описание для этой панели заголовка, чтобы она отображалась, а также сделать активными элементы управления окном, поскольку они отключены по-умолчанию. 78 | 79 | ```rust 80 | impl Header { 81 | fn new() -> Header { 82 | // Создадим виджет контейнера для главной панели заголовка. 83 | let container = HeaderBar::new(); 84 | // Установим отображаемый тект в секции для названия. 85 | container.set_title("Simple GTK"); 86 | // Делаем активными элементы управления окна в этой панели. 87 | container.set_show_close_button(true); 88 | 89 | // Возвращаем заголовок и его состояние. 90 | Header { container } 91 | } 92 | } 93 | ``` 94 | ## Инициализация и запуск приложения 95 | 96 | Теперь, когда мы готовы, нам нужно просто инициализировать GTK, создать структуру приложения, показать все виджеты внутри этой структуры и запустить главный цикл событий GTK. 97 | 98 | ```rust 99 | fn main() { 100 | // Инициализация GTK. 101 | if gtk::init().is_err() { 102 | eprintln!("Не удалось инициализировать GTK приложение."); 103 | return; 104 | } 105 | 106 | // Инициализация начального состояния UI. 107 | let app = Application::new(); 108 | 109 | // Делаем видимыми все виджеты с UI. 110 | app.window.show_all(); 111 | 112 | // Запуск основного цикла GTK. 113 | gtk::main(); 114 | } 115 | ``` 116 | 117 | После того, как основной поток вошёл в цикл событий, он будет взаимодействовать с каждым вызванным виджетом для действий, например: метод `connect_delete_event()`, который мы использовали выше, чтобы запрограммировать кнопку выхода для закрытия программы. 118 | 119 | ## Результат 120 | 121 | После того, как вы написали код, соберите и запустите программу, которая выглядит вот так: 122 | ```bash 123 | cargo build && cargo run 124 | ``` 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/pages/3/index.md: -------------------------------------------------------------------------------- 1 | # Боксирующие кнопки 2 | 3 | 4 | 5 | > Исходный код программы для этой главы можно найти [здесь](https://github.com/ruRust/gtk-rust-by-example/tree/master/src/source_code/button_boxer). 6 | 7 | В этом разделе мы рассмотрим, как структурировать ваш UI с помощью `упаковок` (box), изменять `метку` (label) и запрограммировать нажатие по кнопке. А также, к концу вы поймёте, как: 8 | 9 | 1. Добавлять дочерние элементы в панель заголовка; 10 | 2. Добавлять классы стилей к кнопке; 11 | 3. Управлять состоянием приложения с помощью атомарных счётчиков **Arc** и выравнивать виджеты внутри контейнера. 12 | 13 | > Напомним, что каждый GTK объект, который можно получить из контейнера **gtk** - заворачивают, 14 | > так что не стоит беспокоится о правилах заимствования Rust. Объекты GTK считают ссылки, 15 | > поэтому, когда вам нужно передать GTK объект через несколько замыканий, вы можете 16 | > сделать это с помощью клонирования новой ссылки. 17 | 18 | ## Необходимые знания 19 | 20 | Прежде чем продолжить изучение урока, убедитесь что вы понимаете что такое атомарность и атомарный счётчик ссылок (**Arc**). Эти понятия критически важны для возможности разделять состояние между несколькими замыканиями или потоками. 21 | Изучить этот вопрос можно в документации модулей `std::sync` и `std::sync::atomic`. Так же, вы можете ознакомиться с безблокировочной конкурентностью (lockless concurrency) [в этом документе](https://assets.bitbashing.io/papers/lockless.pdf). 22 | -------------------------------------------------------------------------------- /src/pages/3/objects.md: -------------------------------------------------------------------------------- 1 | # Упаковки, кнопки и метки 2 | 3 | Цель этого раздела заключается в том, чтобы дать объяснение объектам, которые будут использованы до того, как мы применим их на практике в следующих разделах. 4 | 5 | ## GtkBox 6 | 7 | **GtkBox** является фактически эквивалентом UI вектора в Rust и должен быть объявлен с помощью **Orientation**, который определяет, что должны ли элементы быть выровнены слева направо или сверху вниз. Для тех, кто имеет опыт разработки с современным дизайном HTML5/CSS3, **GtkBox** эквивалентен гибким упаковкам -- они могут расширяться на всё пространство, а виджеты, содержащиеся внутри, тоже могут расширяться в соответствии с правилами, применяемыми при дочерней упаковке. 8 | 9 | ## Создание упаковки 10 | 11 | В следующем примере, мы создадим горизонтальную и вертикальную упаковку с нулевым отступом между дочерними элементами, содержащимися в упаковке. После создания упаковки, вы можете задать виджеты, используя метод `pack_*`. 12 | 13 | ```rust 14 | let padding_between_children = 0; 15 | let horizontal_box = Box::new(Orientation::Horizontal, padding_between_children); 16 | let vertical_box = Box::new(Orientation::Vertical, padding_between_children); 17 | ``` 18 | 19 | ## Упаковка упаковки 20 | 21 | Вы могли заметить, что метод `pack_*` принимает большое количество параметров. Первым параметром должна быть ссылка на виджет, которую вы добавляете в контейнер. Вторым и третьим параметрами объявляют параметры заполнения соответственно. Последним параметром объявляют - как много единиц пространства должно быть между дочерними элементами в упаковке. 22 | 23 | > To further elaborate on the expand and fill parameters, expand defines whether the 24 | > given widget should attempt to use all of the extra space that it can. Each widget that has 25 | > the expand parameter set will equally share that extra space. Meanwhile, fill defines whether 26 | > the extra spaced should actually have that widget fill to cover that extra space, or should 27 | > merely use that extra space as padding. 28 | 29 | ```rust 30 | health_info.pack_start(&health_label, false, false, 5); 31 | health_info.pack_start(&health, true, true, 5); 32 | ``` 33 | 34 | 35 | ## GtkLabel 36 | 37 | **GtkLabel** - это простой виджет, который состоит исключительно из текста. Название говорит само за себя. Всё, что вам нужно запомнить - это как создать метку (label) и изменить её. 38 | 39 | ```rust 40 | let information_label = Label::new("Specific Information: "); 41 | let value = Label::new("Linux"); 42 | value.set_label("Redox"); 43 | 44 | let horizontal_box = Box::new(Orientation::Horizontal, 5); 45 | horizontal_box.pack_start(&information_label, false, false, 0); 46 | horizontal_box.pack_start(&value, true, false, 0); 47 | ``` 48 | 49 | ## GtkButton 50 | 51 | ### Создание кнопок 52 | 53 | **GtkButton** - это простая кнопка, содержащая текстовую метку (label) и/или изображения для представления действия, которое должно быть выполнено при нажатии кнопки. 54 | 55 | ```rust 56 | let text_button = Button::new_with_label("Ok"); 57 | let image_button = Button::new_from_icon_name("имя-иконки", 32); 58 | ``` 59 | 60 | ### Дизайн кнопок 61 | 62 | Виджеты в GTK можно оформить так, чтобы они отличались от других виджетов в пользовательском интерфейсе (UI). В частности, кнопки поддерживают два класса стилей: destructive-action и suggested-action. Если в вашем UI есть особенная кнопка, которая должна отличаться, вы можете установить её так: 63 | 64 | ```rust 65 | // Добавьте соответствующие классы стилей к этим кнопкам. 66 | delete_button.get_style_context().map(|c| c.add_class("destructive-action")); 67 | new_button.get_style_context().map(|c| c.add_class("suggested-action")); 68 | ``` 69 | 70 | Каждый **GtkWidget** предоставляет метод **get_style_context()**, который возвращает 71 | **Option**, тем самым предоставляя метод **add_class()**, который используется чтобы установить класс стиля. Понимаете это? Хорошо. Наиболее важные классы кнопок, которые нужно знать - это `destructive-action` и `suggested-action`. Как правило, destructive action окрашивает кнопку в красный цвет, между тем, suggested action использует синий цвет. Актуальный цвет будет зависеть от того, какая тема GTK используется вами. 72 | 73 | -------------------------------------------------------------------------------- /src/pages/3/programming.md: -------------------------------------------------------------------------------- 1 | # Программирование UI 2 | 3 | На этом этапе, мы сможем соединить всё вместе. Сначала мы установим стандартное значение здоровья для программы. Это значение будет использоваться для инициализации состояния структуры приложения. Затем, мы напишем код для кнопки удара и лечения, которые будут должны изменять значение содержимого в главном окне. 4 | 5 | ## Перед тем, как мы начнём 6 | 7 | В нашем распоряжении будет несколько строк, которые будут использованы в зависимости от действия. Это массив **MESSAGES**, к которому мы будем обращаться с помощью типажа с типом **u8**, который будет использован для получения индексов в массиве. 8 | 9 | ```rust 10 | // Заданные сообщения, которые будут использоваться в UI 11 | // при определённых условиях. 12 | const MESSAGES: [&str; 3] = ["Ой! Ты ударил меня!", "...", "Спасибо!"]; 13 | 14 | #[repr(u8)] 15 | // Типаж с типом `u8`, который используется как индекс в массиве `MESSAGES`. 16 | enum Message { Hit, Dead, Heal } 17 | ``` 18 | 19 | Для тех, кто плохо разбирается в Rust, атрибут `#[repr(u8)]` определяет, что следующие элементы будут представлены типом **u8** в памяти. По умолчанию, варианты для типажей начинаются с нуля, поэтому **Hit** это `0`, тогда как **Heal** это `2`. Если вы хотите сделать это явным, вы можете написать это как: 20 | 21 | ```rust 22 | #[repr(u8)] 23 | enum Message { Hit = 0, Dead = 1, Heal = 2 } 24 | ``` 25 | 26 | ## Инициализация компонента Health и структурирование приложения 27 | 28 | После инициализации GTK, мы можем создать наш компонент `health`, который будет обёрнут внутри атомарного счётчика (**Arc**). Если вы запомнили предыдущий код, то на самом деле внутреннее значение это **AtomicUsize**, который служит нашим счетчиком `health`. Это значение будет передаваться через несколько замыканий, следовательно требуется для счётчика ссылок. 29 | 30 | ```rust 31 | let health = Arc::new(HealthComponent::new(10)); 32 | ``` 33 | 34 | Используя это значение, мы создадим структуру UI нашего приложения. Обратите внимание, что `&health` автоматически ссылается как **&HealthComponent**, даже если завёрнут в **Arc**. 35 | 36 | ```rust 37 | let app = App::new(&health); 38 | ``` 39 | 40 | ## Запрограммируем кнопку удара 41 | 42 | Находясь здесь, всё что нам надо - это написать код наших виджетов. Именно здесь мы будем передавать оба компонента `health` и другие различные виджеты UI через замыкания. Начнём с кнопки лечения. Нам просто нужно сказать программе: "Что произойдет при нажатии на кнопку" ? 43 | Типаж **ButtonExt** предоставляет метод **connect_clicked()** именно для этого. 44 | 45 | > Обратите внимание, что виджеты в GTK обычно проходят через их замыкания, поэтому, если 46 | > вы хотите управлять вызовом виджета, вы можете сделать это используя выбранное значение 47 | > через замыкание. Мы не нуждаемся в этой функциональности, поэтому просто проигнорируем 48 | > значение. 49 | > ```rust 50 | > widget.connect_action(move |widget| {}); 51 | > ``` 52 | 53 | ```rust 54 | { 55 | // Запрограммируем кнопку `Ударить` чтобы уменьшить здоровье. 56 | let health = health.clone(); 57 | let message = app.content.message.clone(); 58 | let info = app.content.health.clone(); 59 | app.header.hit.clone().connect_clicked(move |_| { 60 | let new_health = health.subtract(1); 61 | let action = if new_health == 0 { Message::Dead } else { Message::Hit }; 62 | message.set_label(MESSAGES[action as usize]); 63 | info.set_label(new_health.to_string().as_str()); 64 | }); 65 | } 66 | ``` 67 | 68 | В коде выше, мы создали анонимную область, чтобы мы могли содержать наши клонированные ссылки. 69 | Каждый вызов **clone()** просто увеличивает счётчик ссылок и делает значение доступным, чтобы использовать его еще раз позже. 70 | 71 | После вычитания из компонента health, если health равен `0`, то мы должны вернуть **Message::Dead**, иначе, сообщением будет **MessageHit**. После того, как мы овладели этой информацией, это просто вопрос обновления метки с новым значением. 72 | 73 | ## Запрограммируем кнопку лечения 74 | 75 | Это работает почти также, поэтому мы можем скопировать и вставить код выше, а затем изменить его, чтобы удовлетворить наши потребности. 76 | 77 | ```rust 78 | { 79 | // Запрограммируем кнопку `Лечить`, чтобы вернуть очки здоровья. 80 | let health = health.clone(); 81 | let message = app.content.message.clone(); 82 | let info = app.content.health.clone(); 83 | app.header.heal.clone().connect_clicked(move |_| { 84 | let new_health = health.heal(5); 85 | message.set_label(MESSAGES[Message::Heal as usize]); 86 | info.set_label(new_health.to_string().as_str()); 87 | }); 88 | } 89 | ``` 90 | 91 | ## В общей сложности 92 | 93 | После программирования UI, вы можете завершить код, выполнив следующее: 94 | 95 | ```rust 96 | // Сделаем все виджеты видимыми в UI. 97 | app.window.show_all(); 98 | 99 | // Запуск основного цикла GTK. 100 | gtk::main(); 101 | ``` 102 | 103 | Ваш исходный код должен быть таким: 104 | 105 | ```rust 106 | // Заданные сообщения, которые будут использоваться в UI 107 | // при определённых условиях. 108 | const MESSAGES: [&str; 3] = ["Ouch! You hit me!", "...", "Thanks!"]; 109 | 110 | #[repr(u8)] 111 | // Типаж с типом `u8`, который используется как индекс в массиве `MESSAGES`. 112 | enum Message { Hit, Dead, Heal } 113 | 114 | fn main() { 115 | // Инициализируем GTK перед продолжением. 116 | if gtk::init().is_err() { 117 | eprintln!("failed to initialize GTK Application"); 118 | process::exit(1); 119 | } 120 | 121 | /* Установим начальное состояние для нашего компонента - `health`. 122 | * Воспользуемся `Arc`, для того, чтобы мы могли 123 | * использовать несколько programmable замыканий. 124 | */ 125 | let health = Arc::new(HealthComponent::new(10)); 126 | 127 | // Инициализируем начальное состояние UI. 128 | let app = App::new(&health); 129 | 130 | { 131 | // Запрограммируем кнопку `Ударить` чтобы уменьшить здоровье. 132 | let health = health.clone(); 133 | let message = app.content.message.clone(); 134 | let info = app.content.health.clone(); 135 | app.header.hit.clone().connect_clicked(move |_| { 136 | let new_health = health.subtract(1); 137 | let action = if new_health == 0 { Message::Dead } else { Message::Hit }; 138 | message.set_label(MESSAGES[action as usize]); 139 | info.set_label(new_health.to_string().as_str()); 140 | }); 141 | } 142 | 143 | { 144 | // Запрограммируем кнопку `Лечить`, чтобы вернуть очки здоровья. 145 | let health = health.clone(); 146 | let message = app.content.message.clone(); 147 | let info = app.content.health.clone(); 148 | app.header.heal.clone().connect_clicked(move |_| { 149 | let new_health = health.heal(5); 150 | message.set_label(MESSAGES[Message::Heal as usize]); 151 | info.set_label(new_health.to_string().as_str()); 152 | }); 153 | } 154 | 155 | // Сделаем все виджеты видимыми в UI. 156 | app.window.show_all(); 157 | 158 | // Запуск основного цикла GTK. 159 | gtk::main(); 160 | } 161 | ``` -------------------------------------------------------------------------------- /src/pages/3/review.md: -------------------------------------------------------------------------------- 1 | ## Заключение и обзор 2 | 3 | Если вы нажмёте на кнопку `Ударить`, счётчик должен уменьшиться и сообщение должно измениться. Нажатие по кнопке `Лечить` должно увеличить счётчик и также изменить сообщение.После запуска вашей программы с помощью `cargo run`, вы должны увидеть окно, которое выглядит так: 4 | 5 | 6 | 7 | На этом этапе, вы должны хорошо понимать как работают: **GtkBox**, **GtkButton** и **GtkLabel**. Вы можете вернуться к предыдущему разделу, чтобы ещё раз уточнить некоторые моменты. 8 | 9 | ## Практическое занятие 10 | ### Setting Inputs w/ Buttons 11 | 12 | There isn't much that you can do with just buttons and labels. If you want a practice challenge, try creating a program that displays a simple random math problem, and asks the user to use buttons to set the value. If they get it correct, modify a label to tell the user that what they entered was correct. This is an incredibly annoying interface design, so don't do this in the real world! 13 | Bonus: Timed Answers 14 | 15 | Do the same as the above, but also take advantage of `gtk::timeout_add()` to decrement and update a timer label within the UI until the timer reaches zero. -------------------------------------------------------------------------------- /src/pages/3/state.md: -------------------------------------------------------------------------------- 1 | # Сопровождение внешнего состояния 2 | 3 | В этой главе у нас будет некоторое состояние, которым мы будем управлять с помощью UI. Поэтому нам необходим способ хранения и загрузки значения из этого состояния. Программа, которую мы хотим написать, имеет один компонент: значение здоровья. 4 | 5 | Как оказалось, мы напрямую можем воспользоваться атомарными примитивами, таким как **AtomicUsize**, чтобы хранить значение для совместного использования нескольких неизменяемых замыканий. Этим атомарным значением можно управлять, не требуя изменяемого доступа к внутреннему значению. Таким образом, мы можем передавать неизменяемые ссылки на это значение и изменять его даже когда оно уже одолжено в нескольких местах одновременно. 6 | 7 | ```rust 8 | pub struct HealthComponent(AtomicUsize); 9 | ``` 10 | 11 | Пока мы здесь, можем продолжить и написать некоторую логику для этой структуры в блоке реализации, используя следующие методы для здоровья:`initializing`,`subtracting` и `healing`. 12 | 13 | ```rust 14 | impl HealthComponent { 15 | fn new(initial: usize) -> HealthComponent { HealthComponent(AtomicUsize::new(initial)) } 16 | 17 | fn get_health(&self) -> usize { self.0.load(Ordering::SeqCst) } 18 | 19 | fn subtract(&self, value: usize) -> usize { 20 | let current = self.0.load(Ordering::SeqCst); 21 | let new = if current < value { 0 } else { current - value }; 22 | self.0.store(new, Ordering::SeqCst); 23 | new 24 | } 25 | 26 | fn heal(&self, value: usize) -> usize { 27 | let original = self.0.fetch_add(value, Ordering::SeqCst); 28 | original + value 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /src/pages/3/ui.md: -------------------------------------------------------------------------------- 1 | # Создание структуры пользовательского интерфейса (UI) 2 | 3 | Используя структуру предыдущей главы в качестве образца, мы можем расширить её, чтобы включить новые элементы UI, которые будем использовать в нашей программе. Важно отметить, что необходимо хранить только те элементы, которые вы собираетесь запрограммировать позже, после создания структуры UI. 4 | 5 | В этой программе мы добавим два виджета **GtkButtons** к панели заголовка и воспользуемся вертикальным и горизонтальным элементом **GtkBox** с некоторыми метками (labels), чтобы отобразить информацию о текущем состоянии нашего приложения. Следующее изображение является нашей новой диаграммой структур. 6 | 7 | 8 | 9 | Что означает следующее в Rust: 10 | 11 | ```rust 12 | pub struct App { 13 | pub window: Window, 14 | pub header: Header, 15 | pub content: Content, 16 | } 17 | 18 | pub struct Header { 19 | pub container: HeaderBar, 20 | pub hit: Button, 21 | pub heal: Button, 22 | } 23 | 24 | pub struct Content { 25 | pub container: Box, 26 | pub health: Label, 27 | pub message: Label, 28 | } 29 | ``` 30 | 31 | ## Создание структуры App 32 | 33 | Следуя последнему уроку, начнём с нашей структуры **App**. Метод **new()** должен принимать ссылку на **&HealthComponent** в качестве вводимого значения в UI. Заметим, что мы добавили новую переменную **content** типа **Context**, которая принимает эту ссылку на health. 34 | 35 | ```rust 36 | impl App { 37 | fn new(health: &HealthComponent) -> App { 38 | // Создадим новое окно с типом `Toplevel`. 39 | let window = Window::new(WindowType::Toplevel); 40 | // Создадим заголовок и связанное с ним содержимое. 41 | let header = Header::new(); 42 | // Расположим содержимое в окне. 43 | let content = Content::new(health); 44 | 45 | // Установим панель заголовка как описание виджета. 46 | window.set_titlebar(&header.container); 47 | // Установим описание для окна. 48 | window.set_title("Боксирующие кнопки"); 49 | // Установим класс для оконного менеджера. 50 | window.set_wmclass("app-name", "Боксирующие кнопки"); 51 | // Установим иконку, отображаемую приложением. 52 | Window::set_default_icon_name("имя-иконки"); 53 | // Добавим коробку с содержимым в окно. 54 | window.add(&content.container); 55 | 56 | // Запрограммируем выход из программы при нажатии кнопки. 57 | window.connect_delete_event(move |_, _| { 58 | main_quit(); 59 | Inhibit(false) 60 | }); 61 | 62 | // Вернём состояние нашего приложения. 63 | App { 64 | window, 65 | header, 66 | content, 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | ## Создание Header 73 | 74 | Затем мы так же реализуем метод для нашего заголовка, который должен содержать два элемента **GtkButtons** -- кнопка удара и лечения. Также обратите внимание, что мы устанавливаем некоторые классы стилей этим кнопкам, чтобы дать им более информативную визуальную способность. 75 | 76 | ```rust 77 | impl Header { 78 | fn new() -> Header { 79 | // Создадим главный заголовочный бар содержащий виджет. 80 | let container = HeaderBar::new(); 81 | 82 | // Установим текст для отображения в секции для названия. 83 | container.set_title("Боксирующие кнопки"); 84 | // Сделаем активными элементы управления окна в этой панели. 85 | container.set_show_close_button(true); 86 | 87 | // Создадим кнопки: `ударить` и `лечить`. 88 | let hit = Button::new_with_label("Ударить"); 89 | let heal = Button::new_with_label("Лечить"); 90 | 91 | // Добавим соответствующие классы стилей к этим кнопкам. 92 | hit.get_style_context() 93 | .map(|c| c.add_class("destructive-action")); 94 | heal.get_style_context() 95 | .map(|c| c.add_class("suggested-action")); 96 | 97 | // Теперь добавим их в панель заголовка. 98 | container.pack_start(&hit); 99 | container.pack_end(&heal); 100 | 101 | // Вернём the header and all of it's state 102 | Header { 103 | container, 104 | hit, 105 | heal, 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | ## Создание Content 112 | 113 | Теперь пришло время создать содержимое для нашего окна. При создании своего интерфейса (UI) с древовидной диаграммой, вы почти достигните **GtkBoxes**. При инициализации, эта упаковка должна быть указана с **Horizontal** или **Vertical** ориентацией. 114 | 115 | Безусловно, вы достигните виджета **GtkBoxes** для настройки UI. Он может быть создан с **Horizontal** или **Vertical** выравниванием. В эти упаковки вы добавите все ваши виджеты, где они будут сложены в соответствии с выравниванием упаковки, к которой они присоединены. 116 | 117 | Мы должны создать вертикальную упаковку, которая содержит два дочерних элемента: вертикальный виджет **GtkBox** содержащий метку и значение, а затем ниже простой виджет **GtkLabel**. 118 | 119 | ```rust 120 | impl Content { 121 | fn new(health: &HealthComponent) -> Content { 122 | // Создадим вертикальную упаковку, чтобы хранить там все дочерние элементы. 123 | let container = Box::new(Orientation::Vertical, 0); 124 | 125 | // Информация о здоровье будет храниться в горизонтальной упаковке вместе с вертикальной. 126 | let health_info = Box::new(Orientation::Horizontal, 0); 127 | let health_label = Label::new("Текущее значение здоровья:"); 128 | let health = Label::new(health.get_health().to_string().as_str()); 129 | 130 | // Установим горизонтальное выравнивание для наших объектов. 131 | health_info.set_halign(Align::Center); 132 | health_label.set_halign(Align::Start); 133 | health.set_halign(Align::Start); 134 | 135 | // Добивим информацию о здоровье в дочернюю коробку. 136 | health_info.pack_start(&health_label, false, false, 5); 137 | health_info.pack_start(&health, true, true, 5); 138 | 139 | /* 140 | * Создадим метку, которая будет изменяться приложением 141 | * при выполнении удара или лечения. 142 | */ 143 | let message = Label::new("Привет"); 144 | 145 | // Добавим все в нашу вертикальную коробку. 146 | container.pack_start(&health_info, true, false, 0); 147 | container.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0); 148 | container.pack_start(&message, true, false, 0); 149 | 150 | Content { 151 | container, 152 | health, 153 | message, 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | ### Устанавливаем выравнивание 160 | 161 | Возможно, вы заметили, что приведённый код выше устанавливает горизонтальные выравнивания. 162 | По желанию, с помощью методов `set_halign()` и `set_valign()`, можно установить выравнивание для виджетов. 163 | 164 | ```rust 165 | // Установим горизонтальное выравнивание для наших объектов. 166 | health_info.set_halign(Align::Center); 167 | health_label.set_halign(Align::Start); 168 | health.set_halign(Align::Start); 169 | ``` -------------------------------------------------------------------------------- /src/pages/4/concl.md: -------------------------------------------------------------------------------- 1 | Заключение и обзор 2 | ========== 3 | ![head_pic](https://mmstick.github.io/gtkrs-tutorials/images/ch03_complete.png) 4 | 5 | К данному времени у вас должно сложиться хорошее представление о том, как писать 6 | программы с использованием библиотеки GTK на Rust. К текущему моменту вы должны 7 | знать, как делать следующее: 8 | - создавать, получать, присваивать значения объектам **Label** 9 | - создавать, получать, присваивать значения объектам **Entries** 10 | - создавать, получать, присваивать значения объектам **TextBuffer** 11 | - создавать, присваивать буферы объектам **TextViews** 12 | - создавать, настраивать стили внешнего вида, программировать объекты **Button** 13 | - присваивать элементы объектам **Box** и контейнерам **Paned** 14 | - устанавливать отступы и поля на виджетах 15 | - работать с внешним состоянием 16 | 17 | ## Практические задачи -------------------------------------------------------------------------------- /src/pages/4/entries.md: -------------------------------------------------------------------------------- 1 | # Поля, панели, прокручиваемые окна и просмотр текста 2 | 3 | ## GtkPaned 4 | 5 | Это контейнеры, которые могут быть ориентированы вертикально или горизонтально, 6 | представляют собой два элемента, размер которых может изменяться. 7 | Размер этих двух элементов может быть изменен простым нажатием и перемещением 8 | разделяющей полосы между ними. 9 | 10 | ```rust 11 | let container = Paned::new(Orientation::Horizontal); 12 | let left_widget = ...; 13 | let right_widget = ...; 14 | container.pack1(&left_widget, true, true); 15 | container.pack2(&right_widget, true, true); 16 | ``` 17 | 18 | ## GtkEntry 19 | Элементы позволяют пользовательскому интерфейсу принимать строку текста как 20 | входное значение, что может быть использовано другими виджетами для выполнения 21 | некоторых действий, используя данный текст как входные данные. 22 | ```rust 23 | let entry = Entry::new(); 24 | entry.set_text("Some Text"); 25 | if let Some(text) = entry.get_text() { 26 | println!("{}", text); 27 | } 28 | ``` 29 | ## GtkTextView 30 | 31 | Текстовые панели нужны для двух вещей: 32 | - способность показывать многострочный текст 33 | - возможность пользователю вводить многострочный текст 34 | Текстовая панель может быть настроена так, что ее содержимое нельзя 35 | редактировать, если есть такая необходимость. Также есть возможность 36 | настраивать работу с переносами текста. Текстовые панели не умеют работать 37 | с форматированным текстом, однако вполне могут быть использованы как 38 | редактор кода. Если вы хотите, чтобы текст был показан в виде HTML, смотрите 39 | **GtkWebView**, если же вы хотите получить редактор кода, смотрите 40 | **GtkSourceView**. 41 | 42 | > Заметьте, что часто бывает лучше создать и привязать **GtkTextBuffer** 43 | к вашему текстовому полю вручную, чтобы получить указатель на буфер, 44 | который вы можете хранить, и избежать непрямого обращения, когда вы 45 | программируете ваш пользовательский интерфейс (UI). Имея указатель на буфер, 46 | можно легко получить доступ к тексту, который содержится в текстовой панели. 47 | 48 | ```rust 49 | // Буфер для текстовой панели с None в качестве параметра, потому что мы не 50 | // собираемся определять никаких текстовых тэгов для этого буфера. 51 | let text_buffer = TextBuffer::new(None); 52 | // После этого мы должны присвоить буфер новой текстовой панели, которая будет 53 | // самостоятельно обновлять себя при добавлении или удалении текста из буфера. 54 | let text_view = TextView::new_with_buffer(&text_buffer); 55 | ``` 56 | 57 | Извлечение текста из **GtkTextBuffer** требует некоторой сноровки, так что 58 | мы привели пример функции, которую вы можете использовать для того, чтобы 59 | получить содержимое буфера в виде строки (String). Вы можете указать 60 | определенный участок текста, который будет извлечен из буфера. 61 | ```rust 62 | /// Получить все содержимое буфера в строковом представлении. 63 | fn get_buffer(buffer: &TextBuffer) -> Option { 64 | let start = buffer.get_start_iter(); 65 | let end = buffer.get_end_iter(); 66 | buffer.get_text(&start, &end, true) 67 | } 68 | ``` 69 | 70 | ## GtkScrolledWindow 71 | Это одноэлементные контейнеры которые предоставляют прокручиваемые окна внутри 72 | них. Часто бывает удобным сочетать их вместе с текстовыми полями, которые 73 | возможно прокручивать. Это как раз то, что мы хотим сделать в этой главе. 74 | ```rust 75 | let scrolled_window = ScrolledWindow::new(None, None); 76 | scrolled_window.add(&text_view); 77 | ``` -------------------------------------------------------------------------------- /src/pages/4/horrorshow.md: -------------------------------------------------------------------------------- 1 | # Horrorshow HTML-шаблоны 2 | 3 | Хотя это и не связано с разработкой c использованием GTK, пакет horrorshow 4 | предоставляет удобные макроопределения, которые дают возможность эффективно 5 | генерировать HTML-строки в памяти, используя DSL (_domain-specific language_) 6 | совместно с Rust, который может быть запущен посредством использования символа 7 | (_sigil_) `@`. 8 | 9 | ```rust 10 | #[macro_use] 11 | extern crate horrorshow; 12 | use horrorshow::helper::doctype; 13 | 14 | let title = "Title"; 15 | let content = "A string\nwith multiple\n\nlines"; 16 | let html_string = format!( 17 | "{}", 18 | html!{ 19 | : doctype::HTML, 20 | html { 21 | head { 22 | style { : "#style { }" } 23 | } 24 | body { 25 | h1(id="style") { : title } 26 | @ for line in content.lines().filter(|x| !x.is_empty()) { 27 | p { : line } 28 | } 29 | } 30 | } 31 | } 32 | ); 33 | ``` -------------------------------------------------------------------------------- /src/pages/4/index.md: -------------------------------------------------------------------------------- 1 | # Составитель статей из HTML 2 | 3 | ![head_pic](https://mmstick.github.io/gtkrs-tutorials/images/ch03_complete.png) 4 | 5 | > Исходный код для этой главы находится [здесь][chapter_src]. 6 | 7 | В данной главе вы начнете писать полезные программы, используя поля для ввода 8 | и просмотра текста, для того чтобы дать возможность пользователю вводить нужные 9 | значения в текстовые поля и генерировать выходные значения из введенных 10 | значений после нажатия на кнопку. В добавок вы познакомитесь с 11 | макроопределением `html!`, который находится внутри пакета _horrorshow_. Вы 12 | будете писать программу, которая принимает введенные значения на левой панели 13 | и генерирует HTML на правом поле для просмотра текста. 14 | 15 | > Обратите внимание на то что, что в данной главе мы не рассматриваем вопрос 16 | хранения данных во внешнем источнике, как это было в прошлой главе. Все состояние, 17 | которое нас интересует, находится внутри GTK объектов, с которыми мы будем 18 | взаимодействовать. 19 | 20 | [chapter_src]: https://github.com/ruRust/gtk-rust-by-example/tree/master/src/source_code/html_article -------------------------------------------------------------------------------- /src/pages/4/programming.md: -------------------------------------------------------------------------------- 1 | # Программирование UI 2 | 3 | Теперь, когда имеются виджеты, которые должны взаимодействовать друг с другом, 4 | вы можете подумать, что данная часть является небольшой, но сложной в реализации. 5 | Если вы так думаете, то вы полностью ошибаетесь, потому что данная часть будет 6 | самой легкой частью программы для реализации. Начнем с функции `main`, которую 7 | мы возьмем из первой главы в качестве шаблона. 8 | 9 | ```rust 10 | fn main() { 11 | // Инициализация GTK перед началом работы. 12 | if gtk::init().is_err() { 13 | eprintln!("failed to initialize GTK Application"); 14 | process::exit(1); 15 | } 16 | 17 | // Инициализация начального состояния пользовательского интерфейса. 18 | let app = App::new(); 19 | 20 | // Напишите код работы ваших виджетов здесь. 21 | 22 | // Сделать видимыми все виджеты пользовательского интерфейса 23 | app.window.show_all(); 24 | 25 | // Запустим главный цикл событий (_event loop_) GTK. 26 | gtk::main(); 27 | } 28 | ``` 29 | 30 | Мы запрограммируем кнопку **Post** так, чтобы она принимала элементы **title** 31 | и **tags**, также как и буфер текстовой панели **content**. Далее мы пропустим 32 | строки из этих виджетов через HTML-макроопределение _horrorshow_ и напишем 33 | получившийся результат в текстовый буфер **right_pane**. Код для программирования 34 | кнопки выглядит так: 35 | 36 | ```rust 37 | { 38 | // Программирование кнопки **Post** на принятие входных значений из 39 | // левой панели, произведение необходимого обновление HTML-кода на 40 | // правой панели. Подготовка к увеличение значения счетчиков... 41 | let title = app.content.title.clone(); 42 | let tags = app.content.tags.clone(); 43 | let content = app.content.content.clone(); 44 | let right_pane = app.content.right_pane.clone(); 45 | app.header.post.connect_clicked(move |_| { 46 | let inputs = (title.get_text(), tags.get_text(), get_buffer(&content)); 47 | if let (Some(title), Some(tags), Some(content)) = inputs { 48 | right_pane.set_text(&generate_html(&title, &tags, &content)); 49 | } 50 | }); 51 | } 52 | ``` 53 | 54 | Заметьте, получить текста из элемента очень просто. Для этого нужно всего лишь 55 | вызвать метод **get_text()**, который возвращает **Option**. Получение 56 | текста из текстового буфера немного сложнее, поэтому вам необходимо использовать 57 | функцию, которая была рекомендована в начале этой главы. Эта функция написана так: 58 | ```rust 59 | /// Получить содержимое текстового буфера в виде строки. 60 | fn get_buffer(buffer: &TextBuffer) -> Option { 61 | let start = buffer.get_start_iter(); 62 | let end = buffer.get_end_iter(); 63 | buffer.get_text(&start, &end, true) 64 | } 65 | ``` 66 | 67 | Вы также заметите интересный шаблон проектирования (паттерн) в Rust, которая 68 | сильно упростила нам проверку наличия всех входных данных при получение входных 69 | данных, все это произошло перед тем как что-то сделали с входными значениями. 70 | Синтаксис **if let** в Rust работает не только с шаблонами (паттернами), 71 | но и с кортежами (_tuple_), так что вы можете проверять несколько входных 72 | данных в кортеже одновременно, так же, как вы бы делали это в **match**. 73 | ```rust 74 | let inputs = (title.get_text(), tags.get_text(), get_buffer(&content)); 75 | if let (Some(title), Some(tags), Some(content)) = inputs { 76 | right_pane.set_text(&generate_html(&title, &tags, &content)); 77 | } 78 | ``` 79 | Нам еще предстоит определить функцию **generate_html**, и это будет 80 | завершающей частью реализации приложения. Самым простым способом 81 | использования макроопределения **html!** является его подстановка в качестве 82 | аргумента в макроопределение **format!**. Наша функция будет выглядеть так, 83 | хотя вы вольны реализовать HTML-макроопределение по своему усмотрению. 84 | ```rust 85 | /// Генерирует HTML, который будет показан на правой панели. 86 | fn generate_html(title: &str, tags: &str, content: &str) -> String { 87 | format!{ 88 | "{}", 89 | html!{ 90 | article { 91 | header { 92 | h1 { : &title } 93 | div(class="tags") { 94 | @ for tag in tags.split(':') { 95 | div(class="tag") { : tag } 96 | } 97 | } 98 | } 99 | @ for line in content.lines().filter(|x| !x.is_empty()) { 100 | p { : line } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | ``` 107 | Синтаксис приведенного выше кода должен быть довольно читаемым. Мы создаем 108 | пару тэгов **article**, которая содержит в себе пару тэгов **header** и 109 | параграф **p** для каждой непустой линии из входных данных, полученных из 110 | текстового буфера **content**. Внутри тэгов **header** есть заголовок **h1**, 111 | который использует текст из поля для ввода названия как свой текст. Также там 112 | есть элемент **div**, который содержит список тэгов, разделенных двоеточиями. 113 | ![head_pic](https://mmstick.github.io/gtkrs-tutorials/images/ch03_complete.png) 114 | 115 | Имея все это на своих местах, у вас должна получиться работающая программа, 116 | выглядящая как на изображении. 117 | -------------------------------------------------------------------------------- /src/pages/5/binding_keys.md: -------------------------------------------------------------------------------- 1 | # Обработка сочетаний клавиш 2 | Добавим обработку некоторых сочетаний клавиш. 3 | - **F11** - перейти в полноэкранный режим. 4 | - **Ctrl+S** - сохранить файл. 5 | 6 | ## Полноэкранный режим и **App::connect_events()** 7 | Вы хотим знать, когда мы должны вызывать метод **Window::fulscreen()** или 8 | метод **Window::unfullscreen()**, поэтому нам важно сохранять состояние 9 | нахождения в полноэкранном режиме в специальной логической переменной 10 | **AtomicBool**. Эта переменная будет передана в **App::key_events()**, который 11 | также примет ссылку на **ActiveMetadata** текущего файла для проведения 12 | необходимых операций с файлом, например, сохранение. 13 | ```rust 14 | pub fn connect_events(self) -> ConnectedApp { 15 | // Внешнее состояние, которое доступно для событий. 16 | let current_file = Arc::new(RwLock::new(None)); 17 | // Отслеживать то, находится ли программа в полноэкранном режиме. 18 | let fullscreen = Arc::new(AtomicBool::new(false)); 19 | 20 | { 21 | let save = &self.header.save; 22 | let save_as = &self.header.save_as; 23 | 24 | // Присоединить все события, которые данные программа будет 25 | // обрабатывать. 26 | self.editor_changed(current_file.clone(), &save.clone()); 27 | self.open_file(current_file.clone()); 28 | self.save_event(&save.clone(), &save.clone(), current_file.clone(), false); 29 | self.save_event(&save, &save_as, current_file.clone(), true); 30 | self.key_events(current_file, fullscreen); 31 | } 32 | 33 | // Обернуть `App` в `ConnectedApp` для того, чтобы дать возможность 34 | // разработчику выполнять программу. 35 | ConnectedApp(self) 36 | } 37 | ``` 38 | ## Реализация метода **App::key_events()** 39 | То место, где необходимо использовать пакет **gdk**: он поможет нам определить 40 | клавиши, на которые нажал пользователь, и были ли активированы некоторые 41 | состояния (подобно удерживанию Ctrl во время ввода). 42 | 43 | Используя **connect_key_press_event()** в главном окне, мы можем обрабатывать 44 | нажатия клавиш. Вам нужно сопоставлять полученное состояние с определенными 45 | клавишами и выполнять функциональность, которая вам нужна. 46 | ```rust 47 | /// Обрабатывает нажатия определенных сочетаний клавиш. 48 | fn key_events( 49 | &self, 50 | current_file: Arc>>, 51 | fullscreen: Arc, 52 | ) { 53 | // Получить необходимые ссылки заранее. 54 | let editor = self.content.source.buff.clone(); 55 | let headerbar = self.header.container.clone(); 56 | let save_button = self.header.save.clone(); 57 | 58 | // Каждое нажатие кнопки вызовет эту функцию. 59 | self.window.connect_key_press_event(move |window, gdk| { 60 | match gdk.get_keyval() { 61 | // Перевести программу в полноэкранный режим при нажатии на F11. 62 | key::F11 => if fullscreen.fetch_xor(true, Ordering::SeqCst) { 63 | window.unfullscreen(); 64 | } else { 65 | window.fullscreen(); 66 | }, 67 | // Сохранить файл при нажатии Ctrl+S 68 | key if key == 's' as u32 && gdk.get_state().contains(CONTROL_MASK) => { 69 | save(&editor, &headerbar, &save_button, ¤t_file, false); 70 | } 71 | _ => (), 72 | } 73 | Inhibit(false) 74 | }); 75 | } 76 | ``` -------------------------------------------------------------------------------- /src/pages/5/external_state.md: -------------------------------------------------------------------------------- 1 | # Работа с внешним состоянием 2 | Данная часть рассматривает модуль **state.rs**, который будет содержать в себе 3 | типы, используемые для работы с не-GTK внешним состоянием. Это будет очень 4 | просто, потому что Markdown-редактор не хранит большой объем данных во внешнем 5 | хранилище. 6 | ## Что нам нужно? 7 | Имеются два ключевых компонента, которые должны нас заботить в течение времени 8 | жизни программы 9 | - путь к редактируемому файлу 10 | - хеш-сумма содержимого файла для сравнения текущего содержимого редактора с 11 | содержимым файла для того чтобы знать, когда активировать/деактивировать 12 | кнопку **Save**. 13 | ## ActiveMetadata 14 | Так появилась структура **ActiveMetadata**. Она содержит **PathBuf**, 15 | используемый для хранения пути к редактируемому файлу, и 64-байтный 16 | хеш, который будет сгенерирован функцией **keccak512()** из пакета 17 | [tiny-keccak](https://docs.rs/tiny-keccak/1.4.0/tiny_keccak/fn.keccak512.html) 18 | ```rust 19 | use std::path::{Path, PathBuf}; 20 | use tiny_keccak::keccak512; 21 | 22 | pub struct ActiveMetadata { 23 | path: PathBuf, 24 | sum: [u8; 64], 25 | } 26 | ``` 27 | Реализация методов для этого типа будет простой. Мы должны создать: 28 | - новый экземпляр метаданных 29 | - функцию для получения ссылки на путь к файлу 30 | - функцию для проверки того, что хеш-сумма содержимого в редакторе такая же, 31 | как и хранимая в структуре, функцию для обновления значения хеш-суммы при 32 | сохранении файла 33 | ```rust 34 | impl ActiveMetadata { 35 | pub fn new(path: PathBuf, data: &[u8]) -> ActiveMetadata { 36 | ActiveMetadata { path, sum: keccak512(data) } 37 | } 38 | 39 | pub fn get_path<'a>(&'a self) -> &'a Path { &self.path } 40 | 41 | pub fn get_dir(&self) -> Option { self.path.parent().map(|p| p.to_path_buf()) } 42 | 43 | pub fn is_same_as(&self, data: &[u8]) -> bool { &keccak512(data)[..] == &self.sum[..] } 44 | 45 | pub fn set_sum(&mut self, data: &[u8]) { self.sum = keccak512(data); } 46 | } 47 | ``` -------------------------------------------------------------------------------- /src/pages/5/file_choosers.md: -------------------------------------------------------------------------------- 1 | # Диалог выбора файла 2 | 3 | > Заметьте, что это модуль dialogs.rs 4 | 5 | GTK Rust API не используют преимущества типажа **Drop** в Rust, поэтому при появлении диалога, он будет всегда находится на вашем экране. К счастью, мы можем решить эту проблему самостоятельно, создав тип для **GtkFileChooserDialogs** и реализовать типаж **Drop**, чтобы разрушить внутренний диалог после сброса типа. 6 | 7 | ## Создание OpenDialog 8 | 9 | Начнём с создания простой кортёжной структуры. 10 | 11 | ```rust 12 | pub struct OpenDialog(FileChooserDialog); 13 | ``` 14 | 15 | Реализуем простой метод **new()** для этой структуры и создадим внутренний **FileChooserDialog**. 16 | 17 | ```rust 18 | impl OpenDialog { 19 | pub fn new(path: Option) -> OpenDialog { 20 | // Создадим новый диалог выбора файлов, чтобы открыть их. 21 | let open_dialog = FileChooserDialog::new( 22 | Some("Open"), 23 | Some(&Window::new(WindowType::Popup)), 24 | FileChooserAction::Open, 25 | ); 26 | 27 | // Добавим кнопки отмены и открытия для этого диалога. 28 | open_dialog.add_button("Cancel", ResponseType::Cancel.into()); 29 | open_dialog.add_button("Open", ResponseType::Ok.into()); 30 | 31 | // Установим стандартный путь открытия файла. 32 | path.map(|p| open_dialog.set_current_folder(p)); 33 | 34 | OpenDialog(open_dialog) 35 | } 36 | } 37 | ``` 38 | 39 | Смысл **FileChooserDialog** состоит в том, чтобы дать имя диалогу и предоставить новое окно с типом **Popup**, а затем выбрать соответствующее **FileChooserAction**, 40 | который нужен вашим критериям при использовании. В этом случае, мы 41 | создадим диалог **Open**, чтобы открыть файл. 42 | 43 | После создания установите label для двух 44 | кнопок внутри диалога и установите нужный **ResponseTypes** с этими кнопками. 45 | Очень важно знать, нажал ли пользователь **Cancel** или **Ok**. 46 | 47 | Как только закончим, нам необходимо обернуть тип в нашем **OpenDialog**. 48 | 49 | ## Создание SaveDialog 50 | 51 | Диалог Save практически идентичен. 52 | 53 | ```rust 54 | pub struct SaveDialog(FileChooserDialog); 55 | 56 | impl SaveDialog { 57 | pub fn new(path: Option) -> SaveDialog { 58 | // Инициализируем новый даилог встроенный в всплывающее окно. 59 | let save_dialog = FileChooserDialog::new( 60 | Some("Save As"), 61 | Some(&Window::new(WindowType::Popup)), 62 | FileChooserAction::Save, 63 | ); 64 | 65 | // Добавим кнопки cancel и save к диалогу. 66 | save_dialog.add_button("Cancel", ResponseType::Cancel.into()); 67 | save_dialog.add_button("Save", ResponseType::Ok.into()); 68 | 69 | // Установим стандартный путь открытия файла. 70 | path.map(|p| save_dialog.set_current_folder(p)); 71 | 72 | SaveDialog(save_dialog) 73 | } 74 | } 75 | ``` 76 | 77 | ## Реализация типажа Drop 78 | 79 | Объекты в GTK разрушаются с помощью метода **destroy()**. Если вы хотите разрушить наш диалог после того, как мы сбросили их обращение (handles), мы можем сделать это автоматически реализуя типаж **Drop** на обоих типах. 80 | 81 | ```rust 82 | impl Drop for OpenDialog { 83 | fn drop(&mut self) { self.0.destroy(); } 84 | } 85 | 86 | impl Drop for SaveDialog { 87 | fn drop(&mut self) { self.0.destroy(); } 88 | } 89 | ``` 90 | 91 | Глупо, но просто. Мы вызываем метод **destroy()** 92 | для внутреннего значения наших новых типов. 93 | 94 | ## Реализация полезного метода 95 | 96 | Следующий метод может быть добавлен в блок impl для обоих типов и они 97 | упрощают запуск и получают данные на выходе, которые мы хотим. 98 | 99 | ```rust 100 | pub fn run(&self) -> Option { 101 | if self.0.run() == ResponseType::Ok.into() { 102 | self.0.get_filename() 103 | } else { 104 | None 105 | } 106 | } 107 | ``` 108 | 109 | В основном мы показываем/запускаем диалог и проверяем вывод, чтобы определить, 110 | получили ли мы ответ **Ok**. Если ответ **Ok**, мы просто пытаемся вернуть имя файла, которое существует или не существует. 111 | 112 | ## Весь код 113 | 114 | ```rust 115 | use gtk::*; 116 | use std::path::PathBuf; 117 | 118 | /// Обёрнутый FileChooserDialog автоматически разрушающийся при сбрасывании. 119 | pub struct OpenDialog(FileChooserDialog); 120 | 121 | /// Обёрнутый FileChooserDialog автоматически разрушающийся при сбрасывании. 122 | pub struct SaveDialog(FileChooserDialog); 123 | 124 | impl OpenDialog { 125 | pub fn new() -> OpenDialog { 126 | // Создадим новый диалог выбора файлов, чтобы открыть их. 127 | let open_dialog = FileChooserDialog::new( 128 | Some("Open"), 129 | Some(&Window::new(WindowType::Popup)), 130 | FileChooserAction::Open, 131 | ); 132 | 133 | // Добавим кнопки cancel и open для этого диалога. 134 | open_dialog.add_button("Cancel", ResponseType::Cancel.into()); 135 | open_dialog.add_button("Open", ResponseType::Ok.into()); 136 | 137 | OpenDialog(open_dialog) 138 | } 139 | 140 | pub fn run(&self) -> Option { 141 | if self.0.run() == ResponseType::Ok.into() { 142 | self.0.get_filename() 143 | } else { 144 | None 145 | } 146 | } 147 | } 148 | 149 | impl SaveDialog { 150 | pub fn new() -> SaveDialog { 151 | // Инициализируем новый диалог, встроенный в всплывающее окно. 152 | let save_dialog = FileChooserDialog::new( 153 | Some("Save As"), 154 | Some(&Window::new(WindowType::Popup)), 155 | FileChooserAction::Save, 156 | ); 157 | 158 | // Добавим кнопки cancel и save к диалогу. 159 | save_dialog.add_button("Cancel", ResponseType::Cancel.into()); 160 | save_dialog.add_button("Save", ResponseType::Ok.into()); 161 | 162 | SaveDialog(save_dialog) 163 | } 164 | 165 | pub fn run(&self) -> Option { 166 | if self.0.run() == ResponseType::Ok.into() { 167 | self.0.get_filename() 168 | } else { 169 | None 170 | } 171 | } 172 | } 173 | 174 | impl Drop for OpenDialog { 175 | fn drop(&mut self) { self.0.destroy(); } 176 | } 177 | 178 | impl Drop for SaveDialog { 179 | fn drop(&mut self) { self.0.destroy(); } 180 | } 181 | ``` 182 | -------------------------------------------------------------------------------- /src/pages/5/index.md: -------------------------------------------------------------------------------- 1 | # Простой редактор Common Mark 2 | ![head_pic](https://mmstick.github.io/gtkrs-tutorials/images/ch04_complete.png) 3 | 4 | > Исходный код для этой главы находится [здесь][chapter_src]. 5 | 6 | В данной главе мы создадим полезную программу для редактирования текста с 7 | синтаксисом Markdown, в частности, версии Common Mark, и покажем этот текст 8 | в реальном времени, с помощью предпросмотра HTML. 9 | 10 | Мы также будем использовать окно для открытия и сохранения файлов, 11 | будем хэшировать буфер, чтобы знать, когда кнопка Save должна быть 12 | активирована. Когда это будет завершено, вы должны будете понимать не только 13 | как использовать пакет **gtk**, но и **gdk**, **pango**, **webkit2gtk**, 14 | **sourceview**. 15 | 16 | > Эта глава была написана использованием готовой программы, которую мы 17 | > собираемся собрать. Можно сказать, что это обучающее пособие реализует само 18 | > себя. 19 | 20 | > HTML можно писать внутри данного диалекта Markdown. Это позволяет смешивать 21 | > HTML и Markdown в одном редакторе, отображение будет показываться справа. 22 | 23 | ## Исходные требования 24 | Очень полезно знать, как использовать типы **RwLock** и **Mutex** перед тем 25 | как продвигаться дальше, так как данные типы будут использоваться для работы с 26 | внешним состоянием. Это позволит переменным быть заимствованными по изменяемой 27 | ссылке в нескольких потоках и замыканиях, тогда как в другом случае эти 28 | переменные были бы неизменяемыми. 29 | 30 | ## Зависимости 31 | Ваши зависимости должны быть следующими: 32 | ```toml 33 | [dependencies] 34 | gdk = "0.6" 35 | horrorshow = "0.6.2" 36 | pango = "0.2.0" 37 | pulldown-cmark = "0.1.0" 38 | tiny-keccak = "1.4.0" 39 | webkit2gtk = "0.2" 40 | 41 | [dependencies.sourceview] 42 | features = ["v3_18"] 43 | version = "0.2" 44 | 45 | [dependencies.gtk] 46 | features = ["v3_22"] 47 | version = "0.2" 48 | ``` 49 | ### puldown-cmark 50 | Кто-то может заметить, что мы добавили новые пакеты. В Google можно узнать 51 | про пакет [pulldown-cmark][], который предоставляет хороший парсер CommonMark 52 | и поэтому дает возможность преобразовывать Markdown в HTML. 53 | 54 | ### tiny-keccak 55 | Мы будем использовать алгоритм хеширования для того, чтобы делать кнопку Save 56 | активной или неактивной. Проще говоря, мы будем хешировать буфер при каждом 57 | нажатии клавиш и сравнивать полученный хеш с сохраненным. Если хеши совпадают, 58 | то кнопка Save должна быть активной, иначе - неактивной. 59 | 60 | ### sourceview 61 | Благодаря этому пакету у нас появится доступ к виджету **GtkSourceView**, 62 | который мы будем использовать для создания редактора кода, используемом для 63 | редактирования Markdown. 64 | 65 | ### webkit2gtk 66 | Благодаря этому пакету у нас появится виджет **GtkWebView**, который 67 | будет показывать "живое" представление нашего редактируемого Markdown. 68 | 69 | ### pango 70 | Благодаря этому пакету мы сможем изменять шрифт в **GtkSourceView**. 71 | 72 | ### gdk 73 | Мы будем использовать его для обработки некоторых нажатий клавиш. 74 | 75 | [chapter_src]: https://github.com/ruRust/gtk-rust-by-example/tree/master/src/source_code/simple_editor_common_mark 76 | [pulldown-cmark]: https://github.com/google/pulldown-cmark 77 | -------------------------------------------------------------------------------- /src/pages/5/markdown_to_html.md: -------------------------------------------------------------------------------- 1 | # Markdown to HTML 2 | 3 | До того как мы реализуем метод **connect_changed()**, сначала нужно 4 | реализовать модуль **preview**, который будет использовать метод, чтобы получить 5 | HTML-строку для передачи в web-представление. 6 | 7 | Два шага, чтобы преобразовать Markdown в HTML. Первый, включает в себя простое преобразование Markdown в HTML, но и этого недостаточно. 8 | Вам также необходимо интегрировать это в дополнительный HTML и сделать поддержку 9 | подсветки синтаксиса с помощью JavaScript. Не беспокойтесь, потому что мы будем использовать **highlight.js**, который позаботится за нас об этом. 10 | 11 | ## Преобразование Markdown в HTML 12 | 13 | К счастью, у корпорации Google существует пакет (crate) 14 | [pulldown-cmark](https://github.com/google/pulldown-cmark). Обратите внимание, что 15 | он реализован в качестве парсера для повышения эффективности. Осталось предоставить Markdown текст в качестве **&str** в структуру **Parser** и указать изменяемую ссылку **String**, чтобы вернуть HTML. 16 | 17 | ```rust 18 | use pulldown_cmark::{html, Parser}; 19 | 20 | /// Входит Markdown текст; выходит HTML текст. 21 | fn mark_to_html(markdown: &str) -> String { 22 | let parser = Parser::new(&markdown); 23 | let mut buffer = String::new(); 24 | html::push_html(&mut buffer, parser); 25 | buffer 26 | } 27 | ``` 28 | 29 | ## Применение стиля к нашему HTML 30 | 31 | Мы не хотим останавливаться на достигнутом, поэтому будем использовать функцию (указанную выше) внутри нашей публичной функии **render()**, чтобы интегрировать CSS вместе с JavaScript и получить ожидаемый вывод HTML в web просмотр. 32 | 33 | > Заметьте, что мы предоставляем HTML из нашего markdown в секцию **body** 34 | > HTML страницы и имеем завернутую строку **Raw**, чтобы сказать 35 | > макросу **horrorshow** не уходить от внутреннего текста. Если вы предпочитаете 36 | > большее кол-во стилей, тогда вы можете применить дополнительные стили для 37 | > вашего текста. 38 | 39 | ```rust 40 | use horrorshow::Raw; 41 | use horrorshow::helper::doctype; 42 | 43 | /// Входит Markdown текст; выходит стильный HTML текст. 44 | pub fn render(markdown: &str) -> String { 45 | format!( 46 | "{}", 47 | html!( 48 | : doctype::HTML; 49 | html { 50 | head { 51 | link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css") {} 52 | script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js") {} 53 | script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/rust.min.js") {} 54 | script { 55 | : Raw("hljs.initHighlightingOnLoad()") 56 | } 57 | style { 58 | : "body { width: 80%; margin: 0 auto }"; 59 | : "img { max-width: 80% }" 60 | } 61 | } 62 | body { 63 | : Raw(&mark_to_html(markdown)); 64 | } 65 | } 66 | ) 67 | ) 68 | } 69 | ``` -------------------------------------------------------------------------------- /src/pages/5/programming.md: -------------------------------------------------------------------------------- 1 | # Создание событий 2 | 3 | Именно здесь мы начнем творить волшебство для нашего пользовательского интерфейса. Посмотрите снова на модуль **app.rs**, потому что сейчас мы добавим метод **App::connect_events()**. 4 | 5 | ## ConnectedApp 6 | 7 | Однако, прежде чем мы начнём, заранее создадим обёртку **ConnectedApp**, реализующую метод **then_execute()**, который мы видели ранее. 8 | Цель будет преобразовывать **App** в **ConnectedApp** после того, как в 9 | **App** будет вызван метод **App::connect_events()**. 10 | 11 | ```rust 12 | /// Обёртка `App`, обеспечивывающая запуск программы. 13 | pub struct ConnectedApp(App); 14 | 15 | impl ConnectedApp { 16 | /// Покажем окно и запустим главный цикл событий GTK. 17 | pub fn then_execute(self) { 18 | self.0.window.show_all(); 19 | gtk::main(); 20 | } 21 | } 22 | ``` 23 | 24 | ## connect_events() 25 | 26 | Продолжим и наконец-то добавим метод **App::connect_events()** в блок **impl** 27 | с типом **App**, в котором объявим право на владение **App** и вернём **ConnectedApp** в конец. 28 | 29 | ```rust 30 | /// Создадим внешнее состояние и сравним всю функциональность UI. 31 | impl App { 32 | pub fn new() -> App { ... } 33 | 34 | pub fn connect_events(self) -> ConnectedApp { 35 | // Внешнее состояние для обмена событиями. 36 | let current_file = Arc::new(RwLock::new(None)); 37 | 38 | // Соединим все события, с которыми будет взаимодействовать UI. 39 | self.editor_changed( 40 | current_file.clone(), 41 | &self.header.save.clone() 42 | ); 43 | 44 | // Завернём `App` внутрь `ConnectedApp`, чтобы 45 | // выполнить программу. 46 | ConnectedApp(self) 47 | } 48 | } 49 | ``` 50 | 51 | С помощью типа **ActiveMetadata**, который мы создали ранее, создадим переменную 52 | **RwLock'd** **current_file**, которая содержит **Option\**. 53 | По умолчанию, установленное значение равно **None**, т.к. сначала не будет доступного файла для отслеживания. 54 | 55 | Первым событием будет изменение исходного буфера, который мы реализуем в методе **App::editor_changed()**. Этот метод будет принимать ссылку на **RwLock'd** **current_file**, 56 | а также ссылку на кнопку **Save**. Значение проходит через кнопку сохранения и изменяется в зависимости от *чувствительности* содержимого буфера. 57 | 58 | > Если кнопка неактивна, то вы не можете нажать на неё. 59 | -------------------------------------------------------------------------------- /src/pages/5/programming_open_button.md: -------------------------------------------------------------------------------- 1 | # Программирование кнопки Open 2 | Самое время запрограммировать логику работы кнопок **Open**, **Save**, 3 | **Save As** в нашем пользовательском интерфейсе. Начнём с реализации кнопки 4 | **Open**, создав новый метод **open_file()**, который будет вызван из 5 | метода **connect_events()**. 6 | 7 | ## Кнопка Open 8 | Функция кнопки **open_file()** получит доступ к переменной **current_file**, 9 | которая будет обновлена после успешного открытия выбранного файла. 10 | ```rust 11 | pub fn connect_events(self) -> ConnectedApp { 12 | // Внешнее состояние, которое доступно для событий. 13 | let current_file = Arc::new(RwLock::new(None)); 14 | 15 | // Подсоединить события, обрабатываемые пользовательским интерфейсом. 16 | self.editor_changed(current_file.clone(), &self.header.save.clone()); 17 | self.open_file(current_file.clone()); 18 | 19 | // Обернуть `ConnectedApp` вокруг `App` для того, чтобы дать возможность 20 | // разработчику выполнить программу. 21 | ConnectedApp(self) 22 | } 23 | ``` 24 | ### connect_clicked() 25 | Метод **open_file()** будет получать: 26 | - ссылки на буфер редактора и писать информацию из открытого файла в буфер. 27 | - ссылки на панель web просмотра, так что мы сможем обновлять её после 28 | открытия файла. 29 | - ссылку на заголовочную панель, так что мы сможем обновлять название. 30 | - кнопку **Open**, так что мы сможем отобразить событие **connect_clicked()** 31 | на неё. 32 | ```rust 33 | fn open_file(&self, current_file: Arc>>) { 34 | let editor = self.content.source.buff.clone(); 35 | let preview = self.content.preview.clone(); 36 | let headerbar = self.header.container.clone(); 37 | self.header.open.connect_clicked(move |_| { 38 | // Программировать кнопку здесь. 39 | }); 40 | } 41 | ``` 42 | ## Создание OpenDialog 43 | Создадим новый **OpenDialog** внутри **connect_clicked()**. Открывая 44 | диалоговое окно, мы попытаемся передать родительскую директорию в 45 | **current_file**, если она существует, поэтому окно для открытия файла по 46 | умолчанию использует данную директорию. 47 | > Заметьте, я здесь использую **if let Some(ref path)**, а не просто **map** 48 | из-за ограничений на заимствования - если нельзя добиться того, что **map** 49 | будет правильно заимствовать, следует использовать **match** или **if let**. 50 | 51 | ```rust 52 | // Создать диалоговое окно для открытия файла, используя родительскую 53 | // директорию в качестве предпочитаемой, если он установлен. 54 | let open_dialog = OpenDialog::new({ 55 | let lock = current_file.read().unwrap(); 56 | if let Some(ref path) = *lock { 57 | path.get_dir() 58 | } else { 59 | None 60 | } 61 | }); 62 | ``` 63 | ## Запуск диалогового окна 64 | После получения переменной **open_dialog**, мы можем запустить диалог, а также: 65 | - захватить выбранный путь к файлу 66 | - считать данные из файла в буфер 67 | - обновить панель для web просмотра 68 | - обновить название 69 | 70 | ```rust 71 | // Запускает диалоговое окно и открывает файл, если он был выбран. 72 | if let Some(new_file) = open_dialog.run() { 73 | if let Ok(mut file) = File::open(&new_file) { 74 | // Считать содержимое файла в находящийся в памяти буфер. 75 | let mut contents = String::new(); 76 | let _ = file.read_to_string(&mut contents); 77 | 78 | // Обновить название. 79 | set_title(&headerbar, &new_file); 80 | if let Some(parent) = new_file.parent() { 81 | let subtitle: &str = &parent.to_string_lossy(); 82 | headerbar.set_subtitle(subtitle); 83 | } 84 | 85 | // Установить публично доступный путь к файлу. 86 | *current_file.write().unwrap() = 87 | Some(ActiveMetadata::new(new_file, &contents.as_bytes())); 88 | 89 | // Обновить содержимое редактора и предпросмотровую панель. 90 | editor.set_text(&contents); 91 | preview.load_html(&render(&contents), None); 92 | } 93 | } 94 | ``` 95 | ## Полный код 96 | ```rust 97 | fn open_file(&self, current_file: Arc>>) { 98 | let editor = self.content.source.buff.clone(); 99 | let preview = self.content.preview.clone(); 100 | let headerbar = self.header.container.clone(); 101 | self.header.open.connect_clicked(move |_| { 102 | // Создать диалоговое окно для открытия файла, используя родительскую 103 | // директорию в качестве предпочитаемой, если он установлен. 104 | let open_dialog = OpenDialog::new({ 105 | let lock = current_file.read().unwrap(); 106 | if let Some(ref path) = *lock { 107 | path.get_dir() 108 | } else { 109 | None 110 | } 111 | }); 112 | 113 | // Запускает диалоговое окно и открывает файл, если он был выбран. 114 | if let Some(new_file) = open_dialog.run() { 115 | if let Ok(mut file) = File::open(&new_file) { 116 | // Считать содержимое файла в находящийся в памяти буфер. 117 | let mut contents = String::new(); 118 | let _ = file.read_to_string(&mut contents); 119 | 120 | // Обновить название. 121 | set_title(&headerbar, &new_file); 122 | if let Some(parent) = new_file.parent() { 123 | let subtitle: &str = &parent.to_string_lossy(); 124 | headerbar.set_subtitle(subtitle); 125 | } 126 | 127 | // Установить публично доступный путь к файлу. 128 | *current_file.write().unwrap() = 129 | Some(ActiveMetadata::new(new_file, &contents.as_bytes())); 130 | 131 | // Обновить содержимое редактора и предпросмотровую панель. 132 | editor.set_text(&contents); 133 | preview.load_html(&render(&contents), None); 134 | } 135 | } 136 | }); 137 | } 138 | ``` -------------------------------------------------------------------------------- /src/pages/5/programming_save_button.md: -------------------------------------------------------------------------------- 1 | # Программирование кнопки Save 2 | Кнопки **Save** и **Save As** имеют более сложную реализацию, частично из-за 3 | того, что нужно иногда менять состояние кнопки **Save**, а также из-за того, 4 | что кнопка **Save** не должна открывать диалоговое окно когда имеется активный 5 | файл - она должна немедленно сохранять его на диск. 6 | ## App:save_event() 7 | Объявим метод **App::save_event()**, который мы будем использовать в методе 8 | **App::connect_events()** и поместим детали реализации в функцию **save()** 9 | модуля **save.rs**. 10 | 11 | Ключевые параметры, которые нам нужны: 12 | - параметр **save_button**, который мы будем программировать. 13 | - кнопка **Save**, с именем **actual_button**. 14 | - доступ к **ActiveMetadata** текущего файла. 15 | - обозначение того, является ли параметр **save_button** кнопкой 16 | **Save As** или нет. 17 | ```rust 18 | // Используется для программирования кнопок **Save** и **Save As**. 19 | fn save_event( 20 | &self, 21 | save_button: &Button, 22 | actual_button: &Button, 23 | current_file: Arc>>, 24 | save_as: bool, 25 | ) { 26 | let editor = self.content.source.buff.clone(); 27 | let headerbar = self.header.container.clone(); 28 | let save_button = save_button.clone(); 29 | actual_button.connect_clicked( 30 | move |_| save(&editor, &headerbar, &save_button, ¤t_file, save_as), 31 | ); 32 | } 33 | ``` 34 | ## Обновленный App:connect_event() 35 | Далее, нужно написать метод **App::connect_events()** так, чтобы мы могли 36 | передать в него два новых метода: 37 | - первый - программирует кнопку **Save**. 38 | - второй - программирует кнопку **Save As**. 39 | ```rust 40 | /// Создать внешнее состояние и отобразить всю функциональность UI на 41 | /// пользовательский интерфейс. 42 | pub fn connect_events(self) -> ConnectedApp { 43 | // Внешнее состояние, доступное для событий. 44 | let current_file = Arc::new(RwLock::new(None)); 45 | // Отслеживать, открыта ли программа на весь экран или нет. 46 | let fullscreen = Arc::new(AtomicBool::new(false)); 47 | 48 | { 49 | let save = &self.header.save; 50 | let save_as = &self.header.save_as; 51 | 52 | // Присоединить все события, который наш пользовательский интерфейс 53 | // будет обрабатывать. 54 | self.editor_changed(current_file.clone(), &save.clone()); 55 | self.open_file(current_file.clone()); 56 | self.save_event(&save.clone(), &save.clone(), current_file.clone(), false); 57 | self.save_event(&save, &save_as, current_file.clone(), true); 58 | } 59 | 60 | // Обернуть `ConnectedApp` вокруг `App` для того, чтобы дать возможность 61 | // разработчику выполнять программу. 62 | ConnectedApp(self) 63 | } 64 | ``` 65 | ### Реализация модуля **save.rs** 66 | Начнём с добавления необходимых импортов: 67 | ```rust 68 | use super::SaveDialog; 69 | use super::misc::*; 70 | use gtk::*; 71 | use sourceview::*; 72 | use state::ActiveMetadata; 73 | use std::fs::OpenOptions; 74 | use std::io::{self, Write}; 75 | use std::sync::RwLock; 76 | ``` 77 | Мы хотим знать, был ли сохранён новый файл, перезаписан ли текущий или 78 | сохранение было прервано.Создадим перечисление для представления различных 79 | состояний: 80 | ```rust 81 | pub enum SaveAction { 82 | New(ActiveMetadata), 83 | Saved, 84 | Canceled, 85 | } 86 | ``` 87 | ## Запись данных и получение SaveAction 88 | Приватная функция **write_data()** будет использована для записи данного 89 | буфера (**data**) в файл и оповещения о результате действия по сохранению 90 | файла. Если это была кнопка **Save** и сейчас существует **ActiveMetadata**, 91 | тогда данные будут просто записаны в существующий файл и возвращен 92 | **Ok(SaveAction::Saved)**. 93 | 94 | В противном случае, если была нажата кнопка **Save As**, или была нажата 95 | кнопка **Save**, но не было **ActiveMetadata**, тогда будет запущен 96 | **SaveDialog** для получения нового файла и возвращения 97 | **Ok(SaveAction::New(ActiveMetadata))**. Мы возвратим 98 | **Ok(SaveAction::Cancelled)**, если пользователь нажал на кнопку отмены в 99 | диалоговом окне. 100 | ```rust 101 | /// Сохраняет данные в файл, находящийся по предоставленному пути. Если путь 102 | /// является **None**, окно сохранения файла будет запущено для получения пути 103 | /// от пользователя. Если будет запущено диалоговое окно, данная функция 104 | /// возвратит **Ok(Some(path))**, иначе - **Ok(None)**. Значение **Err** 105 | /// указывает на связанную с вводом/выводом ошибку, которая произошла при 106 | /// попытке сохранения файла. 107 | fn write_data(path: Option<&ActiveMetadata>, data: &[u8]) -> io::Result { 108 | if let Some(path) = path { 109 | // Сохранить данные в предоставленный файл, предварительно усекая его. 110 | let mut file = 111 | OpenOptions::new().create(true).write(true).truncate(true).open(path.get_path())?; 112 | file.write_all(&data)?; 113 | return Ok(SaveAction::Saved); 114 | } 115 | 116 | let save_dialog = SaveDialog::new(None); 117 | if let Some(new_path) = save_dialog.run() { 118 | let mut file = 119 | OpenOptions::new().create(true).write(true).truncate(false).open(&new_path)?; 120 | file.write_all(data)?; 121 | Ok(SaveAction::New(ActiveMetadata::new(new_path, data))) 122 | } else { 123 | Ok(SaveAction::Canceled) 124 | } 125 | } 126 | ``` 127 | ### Написание функции **save()** 128 | Напишем функцию **save()** в этом модуле. Первым шагом получим текст из 129 | буфера. Потом предоставим метаинформацию о файле в зависимости от того, была 130 | нажата кнопка **Save As** или **Save**. После этого обработает полученный 131 | **SaveAction**: 132 | - вариант **New** предоставит новую метаинформацию, которую мы сохраним как 133 | текущую и обновим название. 134 | - вариант **Saved** оповестит нас о том, что мы должны вычислить хеш текста, 135 | который был записан на диск, и обновить текущую хеш-сумму файла для отражения 136 | нового состояния файла. 137 | ```rust 138 | pub fn save( 139 | editor: &Buffer, 140 | headerbar: &HeaderBar, 141 | save: &Button, 142 | current_file: &RwLock>, 143 | save_as: bool, 144 | ) { 145 | if let Some(text) = get_buffer(editor) { 146 | // Если мы программируем кнопку **Save As**, то мы не будем 147 | // использовать текущий путь. В противном случае мы сохраним текст в 148 | // редакторе в находящийся по текущему пути файл, если этот путь имеется. 149 | let result = if save_as { 150 | write_data(None, text.as_bytes()) 151 | } else { 152 | write_data(current_file.read().unwrap().as_ref(), text.as_bytes()) 153 | }; 154 | 155 | // Сейчас мы подберем соответствие к выведенному функцией **save()** 156 | // результату. Мы будем обрабатывать случай, когда возвращенное 157 | // значение соответствует шаблону **Ok(Some(ActiveMetadata))**, 158 | // устанавливая название заголовочной панели, и путь, который мы 159 | // получили, в качестве текущего файла. 160 | match result { 161 | Ok(SaveAction::New(file)) => { 162 | // Обновить название. 163 | set_title(&headerbar, file.get_path()); 164 | if let Some(parent) = file.get_dir() { 165 | let subtitle: &str = &parent.to_string_lossy(); 166 | headerbar.set_subtitle(subtitle); 167 | } 168 | let mut current_file = current_file.write().unwrap(); 169 | *current_file = Some(file); 170 | save.set_sensitive(false); 171 | } 172 | Ok(SaveAction::Saved) => { 173 | if let Some(ref mut current_file) = *current_file.write().unwrap() { 174 | current_file.set_sum(&text.as_bytes()); 175 | save.set_sensitive(false); 176 | } 177 | } 178 | _ => (), 179 | } 180 | } 181 | } 182 | ``` -------------------------------------------------------------------------------- /src/pages/5/review_conclusion.md: -------------------------------------------------------------------------------- 1 | # Обзор и подведение итогов 2 | -------------------------------------------------------------------------------- /src/pages/5/setting_modules.md: -------------------------------------------------------------------------------- 1 | # Настройка модулей 2 | ![head_pic](https://mmstick.github.io/gtkrs-tutorials/images/ch04_mod_diagram.png) 3 | 4 | В данной главе мы начнем использовать модули, чтобы разделить код на отдельные 5 | части. Следуя приведенной выше диаграмме мы отделим GTK-специфичную часть кода 6 | от остального кода. 7 | 8 | - модуль **ui** будет содержать весь код, относящийся к построению и выполнению 9 | GTK-программы. 10 | - модуль **preview** генерирует HTML из предоставленного Markdown. 11 | - модуль **state** содержит типы, которые используются для работы с внешним 12 | состоянием. 13 | 14 | ## Создание модулей 15 | ![creating_modules](https://mmstick.github.io/gtkrs-tutorials/images/ch04_mod_structure.png) 16 | 17 | Модули **preview** и **state** относительно простые, они могут быть 18 | представлены двумя Rust-файлами: **preview.rs** и **state.rs**. Модуль **ui** 19 | будет гораздо более сложным, содержа в себе подмодули. У нас будет директория 20 | **ui** где будет точка входа в модуль в виде файла **mod.rs** внутри 21 | директории. 22 | 23 | ## Файл **main.rs** 24 | Как только мы настроим основные модули, мы свяжем всё воедино и соединим с 25 | главной точкой входа в программу - **main.rs**. Первоначальная версия будет 26 | выглядеть так: 27 | ```rust 28 | extern crate gdk; 29 | extern crate gtk; 30 | #[macro_use] 31 | extern crate horrorshow; 32 | extern crate pango; 33 | extern crate pulldown_cmark; 34 | extern crate sourceview; 35 | extern crate tiny_keccak; 36 | extern crate webkit2gtk; 37 | 38 | pub mod preview; 39 | pub mod state; 40 | pub mod ui; 41 | 42 | use ui::App; 43 | 44 | fn main() { 45 | // Инициализировать исходное состояние пользовательского интерфейса. 46 | App::new() 47 | // "Соединить" события с пользовательским интерфейсом. 48 | .connect_events() 49 | // Показать пользовательский интерфейс и запустить программу. 50 | .then_execute(); 51 | } 52 | ``` 53 | Вы можете заметить отличия от приведенного в прошлой главе кода. В этой главе 54 | мы будем использовать паттерн **Builder** для подготовки к работе и исполнению 55 | нашей программы. Данный паттерн здесь очень кстати, так как во время 56 | компиляции может предотвратить появление ошибок связанных с API. 57 | 58 | Метод `App::new()` создает новый **App**, который мы передадим в метод 59 | `connect_events()`. Данный метод преобразует **App** в **ConnectedApp**, 60 | который реализует `then_execute()`, показывающий пользовательский интерфейс и 61 | выполняющий главный цикл событий. -------------------------------------------------------------------------------- /src/pages/5/source_views.md: -------------------------------------------------------------------------------- 1 | # Выбор файла, просмотр кода и web-страниц 2 | ## GtkWebView 3 | 4 | ![pic_gtkwebview](https://mmstick.github.io/gtkrs-tutorials/images/web_view.png) 5 | 6 | Этот виджет, предоставляемый [webkit2gtk][], который содержит прокручиваемое 7 | окно и предоставляет встроенный web-движок для рендеринга HTML. Данный элемент 8 | нужен для показа отрендеренного HTML, полученного из текста Markdown в буфере. 9 | ```rust 10 | let context = WebContext::get_default().unwrap(); 11 | let webview = WebView::new_with_context(&context); 12 | ``` 13 | Мы поместим HTML, который получен посредством использования пакета horrowshow, 14 | в web-панель напрямую, с помощью метода `load_html()`. 15 | ```rust 16 | webview.load_html(&html, None); 17 | ``` 18 | ## GtkSourceView 19 | ![pic_gtkwebview]: https://mmstick.github.io/gtkrs-tutorials/images/source_view.png 20 | Это предоставляемый нам усовершенствованный **GtkTextView**. Однако не 21 | ожидайте слишком многого от него, потому что на текущий момент он достаточно 22 | примитивен. 23 | ```rust 24 | let source_buffer = Buffer::new(None); 25 | let source_view = View::new_with_buffer(&source_buffer); 26 | ``` 27 | Настройки по умолчанию не отличаются от соответствующих настроек **GtkTextView**, 28 | поэтому вам нужно будет самостоятельно настроить виджет. 29 | ```rust 30 | source_view.set_show_line_numbers(true); 31 | source_view.set_monospace(true); 32 | let font = FontDescription::from_string("monospace 11"); 33 | WidgetExt::override_font(&source_view, &font); 34 | ``` 35 | ## GtkFileChooserDialog 36 | ![pic_GtkFileChooserDialog](https://mmstick.github.io/gtkrs-tutorials/images/file_chooser_dialog.png) 37 | **GtkFileChooserDialogs** будет использоваться для программирования поведения 38 | кнопок для открытия/закрытия/сохранения файлов. Они будут открывать окно, где 39 | вы сможете выбрать файл. Важный момент: **GtkFileChooserDialogs** не 40 | использует возможности типажа **Drop**. Но беспокоиться не стоит, все нужное 41 | мы реализуем самостоятельно. 42 | ```rust 43 | // Создать новое диалоговое окно выбора файла для открытия. 44 | let open_dialog = FileChooserDialog::new( 45 | Some("Open"), 46 | Some(&Window::new(WindowType::Popup)), 47 | FileChooserAction::Open, 48 | ); 49 | 50 | // Добавить кнопки **Cancel**, **Save** в это диалоговое окно. 51 | open_dialog.add_button("Cancel", ResponseType::Cancel.into()); 52 | open_dialog.add_button("Open", ResponseType::Ok.into()); 53 | 54 | // Открыть созданное диалоговое окно и принять полученный результат. 55 | if open_dialog.run() == ResponseType::Ok.into() { 56 | if let Some(filename) = open_dialog.get_filename() { 57 | // Сделать что-то с полученным `PathBuf`. 58 | } 59 | } 60 | 61 | // Уничтожить диалоговое окно. Будьте внимательны: не возвращайтесь из 62 | // функции, не уничтожив прежде диалоговое окно. 63 | open_dialog.destroy(); 64 | ``` -------------------------------------------------------------------------------- /src/pages/5/ui_misc_rs.md: -------------------------------------------------------------------------------- 1 | # Модуль ui/misc.rs 2 | Мы реализуем несколько вспомогательных методов, которые при необходимости 3 | будут использоваться в проекте. Это две функции: одна для извлечения текста из 4 | **GtkSourceBuffer**, другая для установки названия у **GtkHeaderBar** с 5 | заданным **Path**. 6 | ```rust 7 | use gtk::*; 8 | use sourceview::*; 9 | use std::path::Path; 10 | 11 | /// Присвоить заголовку в заголовочной панели ссылку на строковое 12 | /// представление пути к файлу. 13 | pub fn set_title(headerbar: &HeaderBar, path: &Path) { 14 | if let Some(filename) = path.file_name() { 15 | let filename: &str = &filename.to_string_lossy(); 16 | headerbar.set_title(filename); 17 | } 18 | } 19 | 20 | /// Получить все внутреннее содержимое данного текстового буфера в виде 21 | /// строки. 22 | pub fn get_buffer(buffer: &Buffer) -> Option { 23 | let start = buffer.get_start_iter(); 24 | let end = buffer.get_end_iter(); 25 | buffer.get_text(&start, &end, true) 26 | } 27 | ``` -------------------------------------------------------------------------------- /src/pages/5/webviews.md: -------------------------------------------------------------------------------- 1 | # Обновление WebViews 2 | 3 | Первой фичей, с нашим пользовательским интерфейсом, будет динамическое обновление web предпросмотра. Начнём с создания нового метода **App::editor_changed()** для **App**, который принимает компонент **current_file** и ссылку на кнопку **save**, для отключения и включения кнопки через некоторое время, чтобы указать, были ли сделаны изменения, требующие сохранения. 4 | 5 | ```rust 6 | /// Обновляет WebView при изменении SourceBuffer. 7 | fn editor_changed( 8 | &self, 9 | current_file: Arc>>, 10 | save_button: &Button, 11 | ) { 12 | let preview = self.content.preview.clone(); 13 | let save_button = save_button.clone(); 14 | self.content.source.buff.connect_changed(move |editor| { 15 | if let Some(markdown) = get_buffer(&editor) { 16 | preview.load_html(&render(&markdown), None); 17 | if let Some(ref current_file) = *current_file.read().unwrap() { 18 | let has_same_sum = current_file.is_same_as(&markdown.as_bytes()); 19 | save_button.set_sensitive(!has_same_sum); 20 | } 21 | } 22 | }); 23 | } 24 | ``` 25 | 26 | ## connect_changed() 27 | 28 | В коде выше вы могли заметить, что мы вызываем метод **connect_changed** из исходного буфера, чтобы обновить просмотр и изменить кнопку сохранения, как только буфер будет изменён. 29 | 30 | ## Получение текста из Source Buffer 31 | 32 | Используем функцию **get_buffer()** из модуля **misc.rs**: 33 | 34 | ```rust 35 | if let Some(markdown) = get_buffer(&editor) { 36 | 37 | } 38 | ``` 39 | 40 | Мы можем получить текст в буфере редактора. 41 | 42 | ## Обновление Web просмотра 43 | 44 | ```rust 45 | preview.load_html(&render(&markdown), None); 46 | ``` 47 | 48 | Здесь мы используем функцию **render()** из модуля **preview** 49 | для преобразования `Markdown` текста в HTML строку и незамедлительно 50 | передаём этот HTML в наш `preview` с помощью метода **load_html()**. 51 | 52 | ## Изменение кнопки Save 53 | 54 | Этот раздел - последний фрагмент задачи, где мы получаем read-only 55 | блокировку для текущего файла и проверяем: генерируется ли текст в буфере с таким же хешем, как и хеш хранящийся на диске. Если хеш совпадает - кнопка сохранения будет неактивна. Если не совпадает - она будет активна. 56 | 57 | ```rust 58 | if let Some(ref current_file) = *current_file.read().unwrap() { 59 | let has_same_sum = current_file.is_same_as(&markdown.as_bytes()); 60 | save_button.set_sensitive(!has_same_sum); 61 | } 62 | ``` -------------------------------------------------------------------------------- /src/source_code/button_boxer/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /src/source_code/button_boxer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "button_boxer" 3 | version = "0.1.0" 4 | authors = ["Norman Ritchie "] 5 | 6 | [dependencies] 7 | 8 | [dependencies.gtk] 9 | version = "0.3.0" 10 | features = ["v3_22"] 11 | 12 | -------------------------------------------------------------------------------- /src/source_code/button_boxer/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | use gtk::*; 3 | use std::process; 4 | use std::sync::Arc; 5 | use std::sync::atomic::{AtomicUsize, Ordering}; 6 | 7 | // Заданные сообщения, которые будут использоваться в UI 8 | // при определённых условиях. 9 | const MESSAGES: [&str; 3] = [ 10 | "Ой! Ты ударил меня!", 11 | "...", 12 | "Спасибо!", 13 | ]; 14 | 15 | #[repr(u8)] 16 | // Типаж с типом `u8`, который используется как индекс в массиве `MESSAGES`. 17 | enum Message { 18 | Hit, 19 | Dead, 20 | Heal, 21 | } 22 | 23 | fn main() { 24 | // Инициализируем GTK перед продолжением. 25 | if gtk::init().is_err() { 26 | eprintln!("failed to initialize GTK Application"); 27 | process::exit(1); 28 | } 29 | 30 | /* Установим начальное состояние для нашего компонента - `health`. 31 | * Воспользуемся `Arc`, для того, чтобы мы могли 32 | * использовать несколько programmable замыканий. 33 | */ 34 | let health = Arc::new(HealthComponent::new(10)); 35 | 36 | // Инициализируем начальное состояние UI. 37 | let app = App::new(&health); 38 | 39 | { 40 | // Запрограммируем кнопку `Ударить` чтобы уменьшить здоровье. 41 | let health = health.clone(); 42 | let message = app.content.message.clone(); 43 | let info = app.content.health.clone(); 44 | app.header.hit.connect_clicked(move |_| { 45 | let new_health = health.subtract(1); 46 | let action = if new_health == 0 { 47 | Message::Dead 48 | } else { 49 | Message::Hit 50 | }; 51 | message.set_label(MESSAGES[action as usize]); 52 | info.set_label(new_health.to_string().as_str()); 53 | }); 54 | } 55 | 56 | { 57 | // Запрограммируем кнопку `Лечить`, чтобы вернуть очки здоровья. 58 | let health = health.clone(); 59 | let message = app.content.message.clone(); 60 | let info = app.content.health.clone(); 61 | app.header.heal.connect_clicked(move |_| { 62 | let new_health = health.heal(5); 63 | message.set_label(MESSAGES[Message::Heal as usize]); 64 | info.set_label(new_health.to_string().as_str()); 65 | }); 66 | } 67 | 68 | // Сделаем все виджеты видимыми в UI. 69 | app.window.show_all(); 70 | 71 | // Запуск основного цикла GTK. 72 | gtk::main(); 73 | } 74 | 75 | pub struct HealthComponent(AtomicUsize); 76 | 77 | impl HealthComponent { 78 | fn new(initial: usize) -> HealthComponent { 79 | HealthComponent(AtomicUsize::new(initial)) 80 | } 81 | 82 | fn get_health(&self) -> usize { 83 | self.0.load(Ordering::SeqCst) 84 | } 85 | 86 | fn subtract(&self, value: usize) -> usize { 87 | let current = self.0.load(Ordering::SeqCst); 88 | let new = if current < value { 0 } else { current - value }; 89 | self.0.store(new, Ordering::SeqCst); 90 | new 91 | } 92 | 93 | fn heal(&self, value: usize) -> usize { 94 | let original = self.0.fetch_add(value, Ordering::SeqCst); 95 | original + value 96 | } 97 | } 98 | 99 | pub struct App { 100 | pub window: Window, 101 | pub header: Header, 102 | pub content: Content, 103 | } 104 | 105 | pub struct Header { 106 | pub container: HeaderBar, 107 | pub hit: Button, 108 | pub heal: Button, 109 | } 110 | 111 | pub struct Content { 112 | pub container: Box, 113 | pub health: Label, 114 | pub message: Label, 115 | } 116 | 117 | impl Content { 118 | fn new(health: &HealthComponent) -> Content { 119 | // Создадим вертикальную упаковку, чтобы хранить там все дочерние элементы. 120 | let container = Box::new(Orientation::Vertical, 0); 121 | 122 | // Информация о здоровье будет храниться в горизонтальной упаковке вместе с вертикальной. 123 | let health_info = Box::new(Orientation::Horizontal, 0); 124 | let health_label = Label::new("Текущее значение здоровья:"); 125 | let health = Label::new(health.get_health().to_string().as_str()); 126 | 127 | // Установим горизонтальное выравнивание для наших объектов. 128 | health_info.set_halign(Align::Center); 129 | health_label.set_halign(Align::Start); 130 | health.set_halign(Align::Start); 131 | 132 | // Добивим информацию о здоровье в дочернюю коробку. 133 | health_info.pack_start(&health_label, false, false, 5); 134 | health_info.pack_start(&health, true, true, 5); 135 | 136 | /* 137 | * Создадим метку, которая будет изменяться приложением 138 | * при выполнении удара или лечения. 139 | */ 140 | let message = Label::new("Привет"); 141 | 142 | // Добавим все в нашу вертикальную коробку. 143 | container.pack_start(&health_info, true, false, 0); 144 | container.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0); 145 | container.pack_start(&message, true, false, 0); 146 | 147 | Content { 148 | container, 149 | health, 150 | message, 151 | } 152 | } 153 | } 154 | 155 | impl App { 156 | fn new(health: &HealthComponent) -> App { 157 | // Создадим новое окно с типом `Toplevel`. 158 | let window = Window::new(WindowType::Toplevel); 159 | // Создадим заголовок и связанное с ним содержимое. 160 | let header = Header::new(); 161 | // Расположим содержимое в окне. 162 | let content = Content::new(health); 163 | 164 | // Set the headerbar as the title bar widget. 165 | window.set_titlebar(&header.container); 166 | // Установим описание для окна. 167 | window.set_title("Боксирующие кнопки"); 168 | // Установим класс для оконного менеджера. 169 | window.set_wmclass("app-name", "Боксирующие кнопки"); 170 | // Установим иконку, отображаемую приложением. 171 | Window::set_default_icon_name("имя-иконки"); 172 | // Добавим коробку с содержимым в окно. 173 | window.add(&content.container); 174 | 175 | // Запрограммируем выход из программы при нажатии кнопки. 176 | window.connect_delete_event(move |_, _| { 177 | main_quit(); 178 | Inhibit(false) 179 | }); 180 | 181 | // Вернём состояние нашего приложения. 182 | App { 183 | window, 184 | header, 185 | content, 186 | } 187 | } 188 | } 189 | 190 | impl Header { 191 | fn new() -> Header { 192 | // Создадим главный заголовочный бар содержащий виджет. 193 | let container = HeaderBar::new(); 194 | 195 | // Установим текст для отображения в секции для названия. 196 | container.set_title("Боксирующие кнопки"); 197 | // Сделаем активными элементы управления окна в этой панели. 198 | container.set_show_close_button(true); 199 | 200 | // Создадим кнопки: `ударить` и `лечить`. 201 | let hit = Button::new_with_label("Ударить"); 202 | let heal = Button::new_with_label("Лечить"); 203 | 204 | // Добавим соответствующие классы стилей к этим кнопкам. 205 | hit.get_style_context() 206 | .map(|c| c.add_class("destructive-action")); 207 | heal.get_style_context() 208 | .map(|c| c.add_class("suggested-action")); 209 | 210 | // Теперь добавим их в панель заголовка. 211 | container.pack_start(&hit); 212 | container.pack_end(&heal); 213 | 214 | // Вернём the header and all of it's state 215 | Header { 216 | container, 217 | hit, 218 | heal, 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/source_code/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | version = "0.1.0" 4 | authors = ["Norman Ritchie "] 5 | 6 | [dependencies] 7 | 8 | [dependencies.gtk] 9 | version = "0.3.0" 10 | features = ["v3_22"] 11 | -------------------------------------------------------------------------------- /src/source_code/hello_world/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | 3 | use gtk::*; 4 | 5 | // Объявим структуру `Application`. 6 | pub struct Application { 7 | pub window: Window, 8 | pub header: Header, 9 | } 10 | 11 | // Объявим структуру `Header`. 12 | pub struct Header { 13 | pub container: HeaderBar, 14 | } 15 | 16 | // Блок реализации. 17 | impl Application { 18 | fn new() -> Application { 19 | // Создадим новое окно с типом `Toplevel`. 20 | let window = Window::new(WindowType::Toplevel); 21 | // Создадим header bar и и связанный с ним контент. 22 | let header = Header::new(); 23 | 24 | // Укажем название заголовка виджета. 25 | window.set_titlebar(&header.container); 26 | // Укажем название для окна приложения. 27 | window.set_title("Простая программа"); 28 | // Установим класс для оконного менеджера. 29 | window.set_wmclass("simple-gtk", "Простая программа"); 30 | // Установим иконку, отображаемую приложением. 31 | Window::set_default_icon_name("имя иконки"); 32 | 33 | // Программа закроется, если нажата кнопка выхода. 34 | window.connect_delete_event(move |_, _| { 35 | main_quit(); 36 | Inhibit(false) 37 | }); 38 | 39 | // Возвращаем основное состояние приложения. 40 | Application { window, header } 41 | } 42 | } 43 | 44 | impl Header { 45 | fn new() -> Header { 46 | // Создадим виджет контейнера для главной панели заголовка. 47 | let container = HeaderBar::new(); 48 | // Установим отображаемый тект в секции для названия. 49 | container.set_title("Простая программа"); 50 | // Делаем активными элементы управления окна в этой панели. 51 | container.set_show_close_button(true); 52 | 53 | // Возвращаем заголовок и его состояние. 54 | Header { container } 55 | } 56 | } 57 | 58 | fn main() { 59 | // Инициализация GTK. 60 | if gtk::init().is_err() { 61 | eprintln!("Не удалось инициализировать GTK приложение."); 62 | return; 63 | } 64 | 65 | // Инициализация начального состояния UI. 66 | let app = Application::new(); 67 | 68 | // Делаем видимыми все виджеты с UI. 69 | app.window.show_all(); 70 | 71 | // Запуск основного цикла GTK. 72 | gtk::main(); 73 | } 74 | -------------------------------------------------------------------------------- /src/source_code/html_article/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "html_article" 3 | version = "0.1.0" 4 | authors = ["Bulat Musin "] 5 | 6 | [dependencies] 7 | 8 | [dependencies.gtk] 9 | version = "0.3.0" 10 | features = ["v3_22"] 11 | 12 | -------------------------------------------------------------------------------- /src/source_code/html_article/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk; 2 | #[macro_use] 3 | extern crate horrorshow; 4 | 5 | use gtk::*; 6 | use std::process; 7 | 8 | fn main() { 9 | // Initialize GTK before proceeding. 10 | if gtk::init().is_err() { 11 | eprintln!("failed to initialize GTK Application"); 12 | process::exit(1); 13 | } 14 | 15 | // Initialize the UI's initial state 16 | let app = App::new(); 17 | 18 | { 19 | // Program the post button to take the inputs in the left pane, and update HTML code 20 | // within the right pane accordingly. Prepared to increment reference counters... 21 | let title = app.content.title.clone(); 22 | let tags = app.content.tags.clone(); 23 | let content = app.content.content.clone(); 24 | let right_pane = app.content.right_pane.clone(); 25 | app.header.post.connect_clicked(move |_| { 26 | let inputs = (title.get_text(), tags.get_text(), get_buffer(&content)); 27 | if let (Some(title), Some(tags), Some(content)) = inputs { 28 | right_pane.set_text(&generate_html(&title, &tags, &content)); 29 | } 30 | }); 31 | } 32 | 33 | // Make all the widgets within the UI visible. 34 | app.window.show_all(); 35 | 36 | // Start the GTK main event loop 37 | gtk::main(); 38 | } 39 | 40 | /// Obtain the entire text buffer's contents as a string. 41 | fn get_buffer(buffer: &TextBuffer) -> Option { 42 | let start = buffer.get_start_iter(); 43 | let end = buffer.get_end_iter(); 44 | buffer.get_text(&start, &end, true) 45 | } 46 | 47 | /// Generates the minified HTML that will be displayed in the right pane 48 | fn generate_html(title: &str, tags: &str, content: &str) -> String { 49 | format!{ 50 | "{}", 51 | html!{ 52 | article { 53 | header { 54 | h1 { : &title } 55 | div(class="tags") { 56 | @ for tag in tags.split(':') { 57 | div(class="tag") { : tag } 58 | } 59 | } 60 | } 61 | @ for line in content.lines().filter(|x| !x.is_empty()) { 62 | p { : line } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | pub struct App { 70 | pub window: Window, 71 | pub header: Header, 72 | pub content: Content, 73 | } 74 | 75 | pub struct Header { 76 | pub container: HeaderBar, 77 | pub post: Button, 78 | } 79 | 80 | pub struct Content { 81 | pub container: Paned, 82 | pub title: Entry, 83 | pub tags: Entry, 84 | pub content: TextBuffer, 85 | pub right_pane: TextBuffer, 86 | } 87 | 88 | impl App { 89 | fn new() -> App { 90 | // Create a new top level window. 91 | let window = Window::new(WindowType::Toplevel); 92 | // Create a the headerbar and it's associated content. 93 | let header = Header::new(); 94 | // Create the main content. 95 | let content = Content::new(); 96 | 97 | // Set the headerbar as the title bar widget. 98 | window.set_titlebar(&header.container); 99 | // Set the title of the window. 100 | window.set_title("HTML Articler"); 101 | // Set the window manager class. 102 | window.set_wmclass("html-articler", "HTML Articler"); 103 | // The icon the app will display. 104 | Window::set_default_icon_name("iconname"); 105 | // Set the default size of the window. 106 | window.set_default_size(800, 600); 107 | // Add the content to the window. 108 | window.add(&content.container); 109 | 110 | // Programs what to do when the exit button is used. 111 | window.connect_delete_event(move |_, _| { 112 | main_quit(); 113 | Inhibit(false) 114 | }); 115 | 116 | // Return our main application state 117 | App { window, header, content } 118 | } 119 | } 120 | 121 | impl Header { 122 | fn new() -> Header { 123 | // Creates the main header bar container widget. 124 | let container = HeaderBar::new(); 125 | 126 | // Sets the text to display in the title section of the header bar. 127 | container.set_title("HTML Articler"); 128 | // Enable the window controls within this headerbar. 129 | container.set_show_close_button(true); 130 | 131 | // Create a button that will post the HTML article. 132 | let post = Button::new_with_label("Post"); 133 | post.get_style_context().map(|x| x.add_class("suggested-action")); 134 | 135 | container.pack_end(&post); 136 | 137 | // Returns the header and all of it's state 138 | Header { container, post } 139 | } 140 | } 141 | 142 | impl Content { 143 | fn new() -> Content { 144 | // The main container will hold a left and right pane. The left pane is for user input, 145 | // whereas the right pane is for the generated output. 146 | let container = Paned::new(Orientation::Horizontal); 147 | let left_pane = Box::new(Orientation::Vertical, 5); 148 | let right_pane = TextBuffer::new(None); 149 | let right_pane_view = TextView::new_with_buffer(&right_pane); 150 | 151 | // The left pane will consist of a title entry, tags entry, and content text view. 152 | let title = Entry::new(); 153 | let tags = Entry::new(); 154 | let content = TextBuffer::new(None); 155 | let content_view = TextView::new_with_buffer(&content); 156 | 157 | // The label that we will display above the content box to describe it. 158 | let content_label = Label::new("Content"); 159 | content_label.set_halign(Align::Center); 160 | 161 | // Set placeholders within the entries to hint the user of the contents to enter. 162 | title.set_placeholder_text("Insert Title"); 163 | tags.set_placeholder_text("Insert Colon-Delimited Tags"); 164 | 165 | // Additionally set tooltips on the entries. Note that you may use either text or markup. 166 | title.set_tooltip_text("Insert the title of article here"); 167 | tags.set_tooltip_markup("tag_one:tag two: tag three"); 168 | 169 | // The right pane should disallow editing; and both editors should wrap by word. 170 | right_pane_view.set_editable(false); 171 | right_pane_view.set_wrap_mode(WrapMode::Word); 172 | content_view.set_wrap_mode(WrapMode::Word); 173 | 174 | // Wrap the text views within scrolled windows, so that they can scroll. 175 | let content_scroller = ScrolledWindow::new(None, None); 176 | let right_pane_scrolled = ScrolledWindow::new(None, None); 177 | content_scroller.add(&content_view); 178 | right_pane_scrolled.add(&right_pane_view); 179 | 180 | // Paddin' Widgets 181 | left_pane.set_border_width(5); 182 | right_pane_view.set_left_margin(5); 183 | right_pane_view.set_right_margin(5); 184 | right_pane_view.set_top_margin(5); 185 | right_pane_view.set_bottom_margin(5); 186 | content_view.set_left_margin(5); 187 | content_view.set_right_margin(5); 188 | content_view.set_top_margin(5); 189 | content_view.set_bottom_margin(5); 190 | 191 | // First add everything to the left pane box. 192 | left_pane.pack_start(&title, false, true, 0); 193 | left_pane.pack_start(&tags, false, true, 0); 194 | left_pane.pack_start(&content_label, false, false, 0); 195 | left_pane.pack_start(&content_scroller, true, true, 0); 196 | 197 | // Then add the left and right panes into the container 198 | container.pack1(&left_pane, true, true); 199 | container.pack2(&right_pane_scrolled, true, true); 200 | 201 | Content { container, title, tags, content, right_pane } 202 | } 203 | } --------------------------------------------------------------------------------