├── .gitmodules
├── Cargo.toml
├── LICENSE.md
├── README.md
├── assets
├── fonts
│ ├── AnonymiceNerd.ttf
│ ├── Anonymous Pro.ttf
│ ├── ExtraSymbols.ttf
│ ├── FiraCodeNerdFont-Regular.ttf
│ ├── LICENSE
│ ├── LastResort-Regular.ttf
│ ├── MissingGlyphs.ttf
│ ├── NotoColorEmoji.ttf
│ ├── NotoSans-Regular.ttf
│ └── NotoSansMono-Regular.ttf
├── helicoid.ico
├── helicoid_logo.svg
├── icon.ico
├── screenshot20230416.webp
├── screenshot20230507.webp
└── test.svg
├── helicoid-client
├── Cargo.toml
└── src
│ ├── bridge
│ └── mod.rs
│ ├── channel_utils.rs
│ ├── command_line.rs
│ ├── dimensions.rs
│ ├── editor
│ ├── cursor.rs
│ ├── editor.rs
│ ├── mod.rs
│ └── style.rs
│ ├── event_aggregator.rs
│ ├── frame.rs
│ ├── main.rs
│ ├── nv_editor
│ ├── cursor.rs
│ ├── draw_command_batcher.rs
│ ├── grid.rs
│ ├── mod.rs
│ ├── style.rs
│ └── window.rs
│ ├── redraw_scheduler.rs
│ ├── renderer
│ ├── animation_utils.rs
│ ├── block_renderer.rs
│ ├── cursor_renderer
│ │ ├── blink.rs
│ │ ├── cursor_vfx.rs
│ │ └── mod.rs
│ ├── fonts
│ │ ├── blob_builder.rs
│ │ ├── font_loader.rs
│ │ └── mod.rs
│ ├── grid_renderer.rs
│ ├── mod.rs
│ ├── profiler.rs
│ ├── rendered_window.rs
│ ├── text_box_renderer.rs
│ └── text_renderer.rs
│ └── window
│ ├── draw_background.rs
│ ├── keyboard_manager.rs
│ ├── mod.rs
│ ├── mouse_manager.rs
│ ├── renderer.rs
│ └── settings.rs
├── helicoid-gpurender
├── Cargo.toml
├── shaders
│ ├── text.fs.wgsl
│ ├── text.vs.wgsl
│ └── text_mono.fs.wgsl
└── src
│ ├── font
│ ├── fontcache.rs
│ ├── mod.rs
│ ├── swash_font.rs
│ ├── text_renderer.rs
│ ├── texture_atlases.rs
│ └── texture_map.rs
│ ├── lib.rs
│ └── renderer
│ ├── animation_utils.rs
│ ├── block_renderer.rs
│ ├── fontconverter.rs
│ ├── grid_renderer.rs
│ ├── mod.rs
│ ├── profiler.rs
│ ├── rendered_window.rs
│ ├── text_box_renderer.rs
│ └── text_renderer.rs
├── helicoid-helixserver
├── Cargo.toml
└── src
│ ├── center.rs
│ ├── compositor.rs
│ ├── constants.rs
│ ├── editor.rs
│ ├── editor_view.rs
│ ├── main.rs
│ ├── server.rs
│ ├── statusline.rs
│ └── tests
│ ├── center.rs
│ └── mod.rs
├── helicoid-protocol
├── Cargo.toml
└── src
│ ├── block_manager.rs
│ ├── bridge_logic.rs
│ ├── caching_shaper.rs
│ ├── font_options.rs
│ ├── gfx.rs
│ ├── input.rs
│ ├── lib.rs
│ ├── shadowblocks.rs
│ ├── swash_font.rs
│ ├── tcp_bridge_async.rs
│ ├── tcp_bridge_sync.rs
│ ├── text.rs
│ └── transferbuffer.rs
├── helicoid-testserver
├── Cargo.toml
└── src
│ ├── main.rs
│ └── testserver.rs
├── helicoid-wgpu
├── Cargo.toml
├── screenshot.png
├── shaders
│ ├── background.fs.wgsl
│ ├── background.vs.wgsl
│ ├── geometry.fs.wgsl
│ └── geometry.vs.wgsl
└── src
│ └── main.rs
└── skia_safe.patch
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "helix"]
2 | path = helix
3 | url = https://github.com/freqmod/helix.git
4 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | # "helicoid-client",
4 | "helicoid-protocol",
5 | "helicoid-helixserver",
6 | "helicoid-testserver",
7 | "helicoid-gpurender",
8 | "helicoid-wgpu",
9 | ]
10 |
11 | default-members = [
12 | "helicoid-wgpu"
13 | ]
14 | resolver = "2"
15 |
16 | [patch.crates-io]
17 | rkyv = { git = "https://github.com/rkyv/rkyv.git"}
18 | ordered-float = { git = "https://github.com/freqmod/rust-ordered-float.git"}
19 | wgpu = { git = "https://github.com/freqmod/wgpu.git", branch ="alt_blend"}
20 | naga = { git = "https://github.com/gfx-rs/naga.git", branch ="master"}
21 | #winit = { git = "https://github.com/freqmod/winit.git", branch="keys"}
22 |
23 | # Needed by helix repo crates (helix-core etc.)
24 | [workspace.dependencies]
25 | tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "7d4b0110a9e92161d247a051018ee42b0513e98d" }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Helicoid
2 | 
3 | 
4 |
5 | Work in progress attempt at making a remote client for the Helix code editor.
6 | The code editor code is at the moment not used in the project as the focus is to get a client and the client
7 | server architecture basics up and running first.
8 |
9 | Copyright: The goal is to license the code under MPL license to match the helix editor.
10 | However the source code is based from the Neovide (https://github.com/neovide/neovide)
11 | source code with is under MIT license.
12 |
13 | Note however that the dependency on Glutin & Winit has been upgraded to the newest versions (post v0.30 rewrite).
14 |
15 | The current state is that preliminary text shaping of single lines / paragraphs and window setup is working.
16 | So is the message passing between the server and client. Be ware there is currently no security
17 | for the network traffic and rkyv is running without validation (bytecheck) as that is not supported for complex enums.
18 |
19 | The next step is to get the shaped text that is transmitted from the server to the client drawn using the block renderer.
20 | This is almost complete.
21 |
22 |
23 |
24 | Architecture:
25 |
26 | Helicoid (a helix surface) aspires to be an editor frontend that enables the user to run the text editor on a remote system and
27 | access it as a local application. The goal is to be able to edit files that are stored on remote systems, or in
28 | virtual machines (or containers) fast and efficiently. It is also a hope that the frontend and editor can be combined
29 | in one process for local editing when that is appropriate, but that a separation of drawing and layout and editing
30 | still can be useful for testing and platform integration reasons.
31 |
32 | The helicoid architecture is inspired by the Neovim architecture. The backend editor (from now called server),
33 | and the front end renderer (from now called client) communicates using an ordered, reliable byte stream (typically a TCP
34 | socket). All user input (keyboard, mouse, resizing etc.) is sent from the client to the server. The server
35 | processes the input and lays out a renderable representation that is sent to the client. For interactivity,
36 | especially on high latency connections it is important to reduce round trips. Therefore only one round trip
37 | should occur per input, and new inputs should not be dependent on that the previous output have arrived.
38 |
39 | The display is based on a box model. All layout, including font shaping is done on the server to avoid round trips.
40 | This means that the font files needs to be accessable to the editor server. However the font shaping is performed
41 | using the swash rust library, so no further UI dependencies (like Skia, OpenGL etc.) are neccesary on the server.
42 |
43 | The text is shaped on the server on a per paragraph / shaping run basis. The shaping runs are organised in blocks
44 | with a certain location and extent. There are also blocks for polygon based uis, nested blocks, and planned to
45 | support references to SVG/bitmap images. All the blocks are serialized using rkyv (for speed) and transfered
46 | to the client to be drawed, see helicoid-protocol for the interface definitions and network logic.
47 | It is intended for the client to reuse text blocks in new locations when the user
48 | interface changes (e.g. to only send the affected paragraph when text changes). The server is expected to keep
49 | track of what the blocks it has sent to the client contains for reuse and to request unused blocks to be removed
50 | (garbage collected) at the client.
51 |
52 | By making the user interface rendering flexible (compared to the character grid that NeoVim uses),
53 | but still relatively simple it is a hope that more complex user interface concepts, like the ones used in
54 | Visual Studio Code and Jetbrains IDEs can be possible (like annotations with different font sizes). However
55 | by requiring a round trip for all user interaction interactivity may suffer in some cases, but should
56 | likely not be much worse than an SSH terminal.
57 |
58 |
59 | Future and notes:
60 |
61 | I have been experimenting with this over the christmas holiday, now i will be back in a full job, so it is probably
62 | limited how fast i can work on this. I hope still to be able to make something demoable rending the current text
63 | interface of helix (at least one file/view) in a window within a few months, but if anyone wants to pick this up
64 | that would still be very interesting to see how it turns out.
65 |
66 | The code is quite messy (some stuff commented out, not really any tests etc.) as it is kind of in a prototype state
67 | where the goal is to first get something simple up and running to see that it is feasable. If/when the prototype
68 | is completed to a state where it is somewhat usable cleanup and better test coverage will likely be introduced.
69 |
70 |
71 | Current plan:
72 | - Finish/Get the possibility to render a document stored in helix-view to work, scrolling up and down the document.
73 | - Look into making a key input system that can be (re) configured runtime based on some kind of serde (toml)
74 | description.
75 |
76 | How to use:
77 | Enter the helicoid folder in a shell (where this readme file is) in two terminals. Build and run the server first,
78 | then the client. Which should open up a window. Currently this has only been tested on (arch)linux using wayland.
79 |
80 | If you have problems with missing opengl functions while linking skia skia-safe should be patched with skia-safe.patch.
81 |
82 | ```
83 | Server (enter build and run):
84 | cd helicoid-testserver
85 | cargo build
86 | RUST_LOG=trace cargo run
87 | ; Press 'q' to exit (the message informing about this is printed using rust log)
88 |
89 | Client (build in the helicoid folder):
90 | cargo build
91 | RUST_LOG=trace cargo run
92 | ```
93 |
--------------------------------------------------------------------------------
/assets/fonts/AnonymiceNerd.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/AnonymiceNerd.ttf
--------------------------------------------------------------------------------
/assets/fonts/Anonymous Pro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/Anonymous Pro.ttf
--------------------------------------------------------------------------------
/assets/fonts/ExtraSymbols.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/ExtraSymbols.ttf
--------------------------------------------------------------------------------
/assets/fonts/FiraCodeNerdFont-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/FiraCodeNerdFont-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/LICENSE:
--------------------------------------------------------------------------------
1 | -----------------------------------------------------------
2 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
3 | -----------------------------------------------------------
4 |
5 | PREAMBLE
6 | The goals of the Open Font License (OFL) are to stimulate worldwide
7 | development of collaborative font projects, to support the font creation
8 | efforts of academic and linguistic communities, and to provide a free and
9 | open framework in which fonts may be shared and improved in partnership
10 | with others.
11 |
12 | The OFL allows the licensed fonts to be used, studied, modified and
13 | redistributed freely as long as they are not sold by themselves. The
14 | fonts, including any derivative works, can be bundled, embedded,
15 | redistributed and/or sold with any software provided that any reserved
16 | names are not used by derivative works. The fonts and derivatives,
17 | however, cannot be released under any other type of license. The
18 | requirement for fonts to remain under this license does not apply
19 | to any document created using the fonts or their derivatives.
20 |
21 | DEFINITIONS
22 | "Font Software" refers to the set of files released by the Copyright
23 | Holder(s) under this license and clearly marked as such. This may
24 | include source files, build scripts and documentation.
25 |
26 | "Reserved Font Name" refers to any names specified as such after the
27 | copyright statement(s).
28 |
29 | "Original Version" refers to the collection of Font Software components as
30 | distributed by the Copyright Holder(s).
31 |
32 | "Modified Version" refers to any derivative made by adding to, deleting,
33 | or substituting -- in part or in whole -- any of the components of the
34 | Original Version, by changing formats or by porting the Font Software to a
35 | new environment.
36 |
37 | "Author" refers to any designer, engineer, programmer, technical
38 | writer or other person who contributed to the Font Software.
39 |
40 | PERMISSION & CONDITIONS
41 | Permission is hereby granted, free of charge, to any person obtaining
42 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
43 | redistribute, and sell modified and unmodified copies of the Font
44 | Software, subject to the following conditions:
45 |
46 | 1) Neither the Font Software nor any of its individual components,
47 | in Original or Modified Versions, may be sold by itself.
48 |
49 | 2) Original or Modified Versions of the Font Software may be bundled,
50 | redistributed and/or sold with any software, provided that each copy
51 | contains the above copyright notice and this license. These can be
52 | included either as stand-alone text files, human-readable headers or
53 | in the appropriate machine-readable metadata fields within text or
54 | binary files as long as those fields can be easily viewed by the user.
55 |
56 | 3) No Modified Version of the Font Software may use the Reserved Font
57 | Name(s) unless explicit written permission is granted by the corresponding
58 | Copyright Holder. This restriction only applies to the primary font name as
59 | presented to the users.
60 |
61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
62 | Software shall not be used to promote, endorse or advertise any
63 | Modified Version, except to acknowledge the contribution(s) of the
64 | Copyright Holder(s) and the Author(s) or with their explicit written
65 | permission.
66 |
67 | 5) The Font Software, modified or unmodified, in part or in whole,
68 | must be distributed entirely under this license, and must not be
69 | distributed under any other license. The requirement for fonts to
70 | remain under this license does not apply to any document created
71 | using the Font Software.
72 |
73 | TERMINATION
74 | This license becomes null and void if any of the above conditions are
75 | not met.
76 |
77 | DISCLAIMER
78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
86 | OTHER DEALINGS IN THE FONT SOFTWARE.
87 |
--------------------------------------------------------------------------------
/assets/fonts/LastResort-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/LastResort-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/MissingGlyphs.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/MissingGlyphs.ttf
--------------------------------------------------------------------------------
/assets/fonts/NotoColorEmoji.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/NotoColorEmoji.ttf
--------------------------------------------------------------------------------
/assets/fonts/NotoSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/NotoSans-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/NotoSansMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/fonts/NotoSansMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/helicoid.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/helicoid.ico
--------------------------------------------------------------------------------
/assets/helicoid_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
145 |
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/icon.ico
--------------------------------------------------------------------------------
/assets/screenshot20230416.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/screenshot20230416.webp
--------------------------------------------------------------------------------
/assets/screenshot20230507.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freqmod/helicoid/a2ac59edc50dc540b2a737e45e3fdd6f81b88902/assets/screenshot20230507.webp
--------------------------------------------------------------------------------
/assets/test.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
145 |
--------------------------------------------------------------------------------
/helicoid-client/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "helicoid-client"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 |
9 | [dependencies]
10 | helicoid-protocol = {path="../helicoid-protocol", features = ["tokio"]}
11 | copypasta = "0.8.1"
12 | async-trait = "0.1.53"
13 | backtrace = "0.3.67"
14 | cfg-if = "1.0.0"
15 | clap = { version = "4.0.32", features = ["cargo", "derive", "env"] }
16 | csscolorparser = "0.6.2"
17 | derive-new = "0.5.9"
18 | dirs = "4.0.0"
19 | euclid = "0.22.7"
20 | flexi_logger = { version = "0.22.3", default-features = false }
21 | env_logger = {version = "0.10" }
22 | futures = "0.3.21"
23 | glutin = "0.30.3"
24 | glutin-winit = { version = "0.3.0"}
25 | raw-window-handle = { version ="0.5.0" }
26 | gl = "0.14.0"
27 | image = { version = "0.24.1", default-features = false, features = ["ico"] }
28 | itertools = "0.10.5"
29 | lazy_static = "1.4.0"
30 | log = "0.4.16"
31 | lru = "0.9"
32 | #nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"] }
33 | parking_lot = "0.12.0"
34 | pin-project = "1.0.10"
35 | rand = "0.8.5"
36 | rmpv = "1.0.0"
37 | serde = { version = "1.0.136", features = ["derive"] }
38 | serde_json = "1.0.79"
39 | shlex = "1.1.0"
40 | swash = "0.1.6"
41 | time = "0.3.9"
42 | tokio = { version = "1", features = ["full"] }
43 | tokio-util = { version = "0.7.1", features = ["compat"] }
44 | unicode-segmentation = "1.10.1"
45 | which = "4.2.5"
46 | winit = { version = "0.28"}
47 | xdg = "2.4.1"
48 | smallvec = {version = "1.10", features = ["serde", "const_generics"]}
49 | half = {version = "2.1", features=["serde", "bytemuck"]}
50 | rkyv = { version = "0.8", features = ["validation", "smallvec"] }
51 | ordered-float = { version = "3.0", features = ["bytemuck", "rkyv", "serde"]}
52 | anyhow = {version="1.0"}
53 | #skia-safe = {path = "/home/freqmod/Downloads/rust-skia/skia-safe/",features = ["gl"]}
54 | skia-safe = {version="0.58",features = ["gl"]}
55 | hashbrown = {version = "0.13.2"}
56 | #fnv = {version="1.0"}
57 | ahash = {version = "0.8.3"}
58 |
59 | resvg = {version="0.29"}
60 | usvg = {version="0.29"}
61 | tiny-skia = {version="0.8.3"}
62 |
63 | [dev-dependencies]
64 | mockall = "0.11.0"
65 |
66 |
67 | #[target.'cfg(linux)'.dependencies.skia-safe]
68 | #path = "/home/freqmod/Downloads/rust-skia/skia-safe/"
69 | #features = ["gl", "egl"]
70 | #version = "0.57.0"
71 |
72 | #[target.'cfg(not(linux))'.dependencies.skia-safe]
73 | #version = "0.57.0"
74 |
--------------------------------------------------------------------------------
/helicoid-client/src/bridge/mod.rs:
--------------------------------------------------------------------------------
1 | /* This is the start of an (currently imaginary) bridge to the helix editor.
2 | This bridge is having a quite different architecture than the (neo)vim bridge. */
3 |
4 | #[allow(dead_code)]
5 | #[derive(Clone, Debug)]
6 | pub enum WindowAnchor {
7 | NorthWest,
8 | NorthEast,
9 | SouthWest,
10 | SouthEast,
11 | }
12 |
13 | #[derive(Clone, Debug)]
14 | pub enum RedrawEvent {}
15 |
16 | /*
17 | #[derive(Clone, Debug)]
18 | pub enum GuiOption {
19 | ArabicShape(bool),
20 | AmbiWidth(String),
21 | Emoji(bool),
22 | GuiFont(String),
23 | GuiFontSet(String),
24 | GuiFontWide(String),
25 | LineSpace(u64),
26 | Pumblend(u64),
27 | ShowTabLine(u64),
28 | TermGuiColors(bool),
29 | //Unknown(String, ),
30 | }*/
31 |
--------------------------------------------------------------------------------
/helicoid-client/src/channel_utils.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/helicoid-client/src/command_line.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 |
3 |
4 | #[derive(Parser, Debug)]
5 | #[command(author, version, about, long_about = None)]
6 | pub struct HeliconeCommandLineArguments {
7 | /// Address of editor server, for connecting to an existing server
8 | #[arg(short, long, default_value = "127.0.0.1:15566")]
9 | pub server_address: Option,
10 | /* /// Number of times to greet
11 | #[arg(short, long, default_value_t = 1)]
12 | count: u8,*/
13 | }
14 |
--------------------------------------------------------------------------------
/helicoid-client/src/dimensions.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::Display,
3 | ops::{Div, Mul},
4 | str::FromStr,
5 | };
6 |
7 | use serde::{Deserialize, Serialize};
8 | use winit::dpi::PhysicalSize;
9 |
10 | //use crate::settings;
11 | pub const DEFAULT_WINDOW_GEOMETRY: Dimensions = Dimensions {
12 | width: 100,
13 | height: 50,
14 | };
15 |
16 | pub fn parse_window_geometry(input: &str) -> Result {
17 | let invalid_parse_err = format!(
18 | "Invalid geometry: {}\nValid format: x",
19 | input
20 | );
21 |
22 | input
23 | .split('x')
24 | .map(|dimension| {
25 | dimension
26 | .parse::()
27 | .map_err(|_| invalid_parse_err.as_str())
28 | .and_then(|dimension| {
29 | if dimension > 0 {
30 | Ok(dimension)
31 | } else {
32 | Err("Invalid geometry: Window dimensions should be greater than 0.")
33 | }
34 | })
35 | })
36 | .collect::, &str>>()
37 | .and_then(|dimensions| {
38 | if let [width, height] = dimensions[..] {
39 | Ok(Dimensions { width, height })
40 | } else {
41 | Err(invalid_parse_err.as_str())
42 | }
43 | })
44 | .map_err(|msg| msg.to_owned())
45 | }
46 |
47 | // Maybe this should be independent from serialization?
48 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
49 | pub struct Dimensions {
50 | pub width: u64,
51 | pub height: u64,
52 | }
53 |
54 | impl Default for Dimensions {
55 | fn default() -> Self {
56 | DEFAULT_WINDOW_GEOMETRY
57 | }
58 | }
59 |
60 | impl FromStr for Dimensions {
61 | type Err = String;
62 | fn from_str(s: &str) -> Result {
63 | parse_window_geometry(s)
64 | }
65 | }
66 |
67 | impl Display for Dimensions {
68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 | write!(f, "{}x{}", self.width, self.height)
70 | }
71 | }
72 |
73 | macro_rules! impl_from_tuple_to_dimensions {
74 | ($type:ty) => {
75 | impl From<($type, $type)> for Dimensions {
76 | fn from((width, height): ($type, $type)) -> Self {
77 | Dimensions {
78 | width: width as u64,
79 | height: height as u64,
80 | }
81 | }
82 | }
83 | };
84 | }
85 |
86 | impl_from_tuple_to_dimensions!(u64);
87 | impl_from_tuple_to_dimensions!(f32);
88 |
89 | macro_rules! impl_from_dimensions_to_tuple {
90 | ($type:ty) => {
91 | impl From for ($type, $type) {
92 | fn from(dimensions: Dimensions) -> Self {
93 | (dimensions.width as $type, dimensions.height as $type)
94 | }
95 | }
96 | };
97 | }
98 |
99 | impl_from_dimensions_to_tuple!(u64);
100 | impl_from_dimensions_to_tuple!(u32);
101 | impl_from_dimensions_to_tuple!(i32);
102 |
103 | impl From> for Dimensions {
104 | fn from(PhysicalSize { width, height }: PhysicalSize) -> Self {
105 | Dimensions {
106 | width: width as u64,
107 | height: height as u64,
108 | }
109 | }
110 | }
111 |
112 | impl From for PhysicalSize {
113 | fn from(Dimensions { width, height }: Dimensions) -> Self {
114 | PhysicalSize {
115 | width: width as u32,
116 | height: height as u32,
117 | }
118 | }
119 | }
120 |
121 | impl Mul for Dimensions {
122 | type Output = Self;
123 |
124 | fn mul(self, other: Self) -> Self {
125 | Dimensions::from((self.width * other.width, self.height * other.height))
126 | }
127 | }
128 |
129 | impl Div for Dimensions {
130 | type Output = Self;
131 |
132 | fn div(self, other: Self) -> Self {
133 | Dimensions::from((self.width / other.width, self.height / other.height))
134 | }
135 | }
136 |
137 | impl Mul for (u64, u64) {
138 | type Output = Self;
139 |
140 | fn mul(self, other: Dimensions) -> Self {
141 | let (x, y) = self;
142 | (x * other.width, y * other.height)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/helicoid-client/src/editor/cursor.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, sync::Arc};
2 |
3 | use skia_safe::Color4f;
4 |
5 | use crate::editor::style::{Colors, Style};
6 |
7 | #[derive(Debug, Clone, PartialEq)]
8 | pub enum CursorShape {
9 | Block,
10 | Horizontal,
11 | Vertical,
12 | }
13 |
14 | impl CursorShape {
15 | pub fn from_type_name(name: &str) -> Option {
16 | match name {
17 | "block" => Some(CursorShape::Block),
18 | "horizontal" => Some(CursorShape::Horizontal),
19 | "vertical" => Some(CursorShape::Vertical),
20 | _ => None,
21 | }
22 | }
23 | }
24 |
25 | #[derive(Default, Debug, Clone, PartialEq)]
26 | pub struct CursorMode {
27 | pub shape: Option,
28 | pub style_id: Option,
29 | pub cell_percentage: Option,
30 | pub blinkwait: Option,
31 | pub blinkon: Option,
32 | pub blinkoff: Option,
33 | }
34 |
35 | #[derive(Clone, Debug, PartialEq)]
36 | pub struct Cursor {
37 | pub grid_position: (u64, u64),
38 | pub parent_window_id: u64,
39 | pub shape: CursorShape,
40 | pub cell_percentage: Option,
41 | pub blinkwait: Option,
42 | pub blinkon: Option,
43 | pub blinkoff: Option,
44 | pub style: Option>,
45 | pub enabled: bool,
46 | pub double_width: bool,
47 | //pub grid_cell: GridCell,
48 | }
49 |
50 | impl Cursor {
51 | pub fn new() -> Cursor {
52 | Cursor {
53 | grid_position: (0, 0),
54 | parent_window_id: 0,
55 | shape: CursorShape::Block,
56 | style: None,
57 | cell_percentage: None,
58 | blinkwait: None,
59 | blinkon: None,
60 | blinkoff: None,
61 | enabled: true,
62 | double_width: false,
63 | }
64 | }
65 |
66 | pub fn foreground(&self, default_colors: &Colors) -> Color4f {
67 | self.style
68 | .as_ref()
69 | .and_then(|s| s.colors.foreground)
70 | .unwrap_or_else(|| default_colors.background.unwrap())
71 | }
72 |
73 | pub fn background(&self, default_colors: &Colors) -> Color4f {
74 | self.style
75 | .as_ref()
76 | .and_then(|s| s.colors.background)
77 | .unwrap_or_else(|| default_colors.foreground.unwrap())
78 | }
79 |
80 | pub fn alpha(&self) -> u8 {
81 | return self
82 | .style
83 | .as_ref()
84 | .map(|s| (255_f32 * ((100 - s.blend) as f32 / 100.0_f32)) as u8)
85 | .unwrap_or(255);
86 | }
87 |
88 | pub fn change_mode(&mut self, cursor_mode: &CursorMode, styles: &HashMap>) {
89 | let CursorMode {
90 | shape,
91 | style_id,
92 | cell_percentage,
93 | blinkwait,
94 | blinkon,
95 | blinkoff,
96 | } = cursor_mode;
97 |
98 | if let Some(shape) = shape {
99 | self.shape = shape.clone();
100 | }
101 |
102 | if let Some(style_id) = style_id {
103 | self.style = styles.get(style_id).cloned();
104 | }
105 |
106 | self.cell_percentage = *cell_percentage;
107 | self.blinkwait = *blinkwait;
108 | self.blinkon = *blinkon;
109 | self.blinkoff = *blinkoff;
110 | }
111 | }
112 |
113 | #[cfg(test)]
114 | mod tests {
115 | use super::*;
116 |
117 | const COLORS: Colors = Colors {
118 | foreground: Some(Color4f::new(0.1, 0.1, 0.1, 0.1)),
119 | background: Some(Color4f::new(0.2, 0.1, 0.1, 0.1)),
120 | special: Some(Color4f::new(0.3, 0.1, 0.1, 0.1)),
121 | };
122 |
123 | const DEFAULT_COLORS: Colors = Colors {
124 | foreground: Some(Color4f::new(0.1, 0.2, 0.1, 0.1)),
125 | background: Some(Color4f::new(0.2, 0.2, 0.1, 0.1)),
126 | special: Some(Color4f::new(0.3, 0.2, 0.1, 0.1)),
127 | };
128 |
129 | const NONE_COLORS: Colors = Colors {
130 | foreground: None,
131 | background: None,
132 | special: None,
133 | };
134 |
135 | #[test]
136 | fn test_from_type_name() {
137 | assert_eq!(
138 | CursorShape::from_type_name("block"),
139 | Some(CursorShape::Block)
140 | );
141 | assert_eq!(
142 | CursorShape::from_type_name("horizontal"),
143 | Some(CursorShape::Horizontal)
144 | );
145 | assert_eq!(
146 | CursorShape::from_type_name("vertical"),
147 | Some(CursorShape::Vertical)
148 | );
149 | }
150 |
151 | #[test]
152 | fn test_foreground() {
153 | let mut cursor = Cursor::new();
154 | let style = Some(Arc::new(Style::new(COLORS)));
155 |
156 | assert_eq!(
157 | cursor.foreground(&DEFAULT_COLORS),
158 | DEFAULT_COLORS.background.unwrap()
159 | );
160 | cursor.style = style;
161 | assert_eq!(
162 | cursor.foreground(&DEFAULT_COLORS),
163 | COLORS.foreground.unwrap()
164 | );
165 |
166 | cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
167 | assert_eq!(
168 | cursor.foreground(&DEFAULT_COLORS),
169 | DEFAULT_COLORS.background.unwrap()
170 | );
171 | }
172 |
173 | #[test]
174 | fn test_background() {
175 | let mut cursor = Cursor::new();
176 | let style = Some(Arc::new(Style::new(COLORS)));
177 |
178 | assert_eq!(
179 | cursor.background(&DEFAULT_COLORS),
180 | DEFAULT_COLORS.foreground.unwrap()
181 | );
182 | cursor.style = style;
183 | assert_eq!(
184 | cursor.background(&DEFAULT_COLORS),
185 | COLORS.background.unwrap()
186 | );
187 |
188 | cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
189 | assert_eq!(
190 | cursor.background(&DEFAULT_COLORS),
191 | DEFAULT_COLORS.foreground.unwrap()
192 | );
193 | }
194 |
195 | #[test]
196 | fn test_change_mode() {
197 | let cursor_mode = CursorMode {
198 | shape: Some(CursorShape::Horizontal),
199 | style_id: Some(1),
200 | cell_percentage: Some(100.0),
201 | blinkwait: Some(1),
202 | blinkon: Some(1),
203 | blinkoff: Some(1),
204 | };
205 | let mut styles = HashMap::new();
206 | styles.insert(1, Arc::new(Style::new(COLORS)));
207 |
208 | let mut cursor = Cursor::new();
209 |
210 | cursor.change_mode(&cursor_mode, &styles);
211 | assert_eq!(cursor.shape, CursorShape::Horizontal);
212 | assert_eq!(cursor.style, styles.get(&1).cloned());
213 | assert_eq!(cursor.cell_percentage, Some(100.0));
214 | assert_eq!(cursor.blinkwait, Some(1));
215 | assert_eq!(cursor.blinkon, Some(1));
216 | assert_eq!(cursor.blinkoff, Some(1));
217 |
218 | let cursor_mode_with_none = CursorMode {
219 | shape: None,
220 | style_id: None,
221 | cell_percentage: None,
222 | blinkwait: None,
223 | blinkon: None,
224 | blinkoff: None,
225 | };
226 | cursor.change_mode(&cursor_mode_with_none, &styles);
227 | assert_eq!(cursor.shape, CursorShape::Horizontal);
228 | assert_eq!(cursor.style, styles.get(&1).cloned());
229 | assert_eq!(cursor.cell_percentage, None);
230 | assert_eq!(cursor.blinkwait, None);
231 | assert_eq!(cursor.blinkon, None);
232 | assert_eq!(cursor.blinkoff, None);
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/helicoid-client/src/editor/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::bridge::WindowAnchor;
2 |
3 | //mod cursor;
4 | pub(crate) mod editor;
5 | //mod style;
6 |
7 | //pub use cursor::{Cursor, CursorMode, CursorShape};
8 |
9 | #[derive(Clone, Debug)]
10 | pub struct AnchorInfo {
11 | pub anchor_grid_id: u64,
12 | pub anchor_type: WindowAnchor,
13 | pub anchor_left: f64,
14 | pub anchor_top: f64,
15 | pub sort_order: u64,
16 | }
17 |
18 | impl WindowAnchor {
19 | fn _modified_top_left(
20 | &self,
21 | grid_left: f64,
22 | grid_top: f64,
23 | width: u64,
24 | height: u64,
25 | ) -> (f64, f64) {
26 | match self {
27 | WindowAnchor::NorthWest => (grid_left, grid_top),
28 | WindowAnchor::NorthEast => (grid_left - width as f64, grid_top),
29 | WindowAnchor::SouthWest => (grid_left, grid_top - height as f64),
30 | WindowAnchor::SouthEast => (grid_left - width as f64, grid_top - height as f64),
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/helicoid-client/src/editor/style.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/helicoid-client/src/event_aggregator.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | any::{type_name, Any, TypeId},
3 | collections::{hash_map::Entry, HashMap},
4 | fmt::Debug,
5 | };
6 |
7 | use parking_lot::{Mutex, RwLock};
8 | use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
9 |
10 | use crate::channel_utils::*;
11 |
12 | lazy_static! {
13 | pub static ref EVENT_AGGREGATOR: EventAggregator = EventAggregator::default();
14 | }
15 |
16 | thread_local! {
17 | static THREAD_SENDERS: RwLock>> = RwLock::new(HashMap::new());
18 | }
19 |
20 | pub struct EventAggregator {
21 | parent_senders: RwLock>>>,
22 | unclaimed_receivers: RwLock>>,
23 | }
24 |
25 | impl Default for EventAggregator {
26 | fn default() -> Self {
27 | EventAggregator {
28 | parent_senders: RwLock::new(HashMap::new()),
29 | unclaimed_receivers: RwLock::new(HashMap::new()),
30 | }
31 | }
32 | }
33 |
34 | impl EventAggregator {
35 | fn get_sender(&self) -> LoggingTx {
36 | match self.parent_senders.write().entry(TypeId::of::()) {
37 | Entry::Occupied(entry) => {
38 | let sender = entry.get().lock();
39 | sender.downcast_ref::>().unwrap().clone()
40 | }
41 | Entry::Vacant(entry) => {
42 | let (sender, receiver) = unbounded_channel();
43 | let logging_tx = LoggingTx::attach(sender, type_name::().to_owned());
44 | entry.insert(Mutex::new(Box::new(logging_tx.clone())));
45 | self.unclaimed_receivers
46 | .write()
47 | .insert(TypeId::of::(), Box::new(receiver));
48 | logging_tx
49 | }
50 | }
51 | }
52 |
53 | pub fn send(&self, event: T) {
54 | let sender = self.get_sender::();
55 | sender.send(event).unwrap();
56 | }
57 |
58 | pub fn register_event(&self) -> UnboundedReceiver {
59 | let type_id = TypeId::of::();
60 |
61 | if let Some(receiver) = self.unclaimed_receivers.write().remove(&type_id) {
62 | *receiver.downcast::>().unwrap()
63 | } else {
64 | let (sender, receiver) = unbounded_channel();
65 | let logging_sender = LoggingTx::attach(sender, type_name::().to_owned());
66 |
67 | match self.parent_senders.write().entry(type_id) {
68 | Entry::Occupied(_) => panic!("EventAggregator: type already registered"),
69 | Entry::Vacant(entry) => {
70 | entry.insert(Mutex::new(Box::new(logging_sender)));
71 | }
72 | }
73 |
74 | receiver
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/helicoid-client/src/frame.rs:
--------------------------------------------------------------------------------
1 | use core::fmt;
2 |
3 | use clap::{builder::PossibleValue, ValueEnum};
4 |
5 | // Options for the frame decorations
6 | #[derive(Debug, Clone, Copy, PartialEq)]
7 | pub enum Frame {
8 | Full,
9 | #[cfg(target_os = "macos")]
10 | Transparent,
11 | #[cfg(target_os = "macos")]
12 | Buttonless,
13 | None,
14 | }
15 |
16 | impl From<&'_ Frame> for &'static str {
17 | fn from(frame: &'_ Frame) -> Self {
18 | match frame {
19 | Frame::Full => "full",
20 |
21 | #[cfg(target_os = "macos")]
22 | Frame::Transparent => "transparent",
23 | #[cfg(target_os = "macos")]
24 | Frame::Buttonless => "buttonless",
25 |
26 | Frame::None => "none",
27 | }
28 | }
29 | }
30 |
31 | impl Default for Frame {
32 | fn default() -> Frame {
33 | Frame::Full
34 | }
35 | }
36 |
37 | impl ValueEnum for Frame {
38 | fn value_variants<'a>() -> &'a [Self] {
39 | #[cfg(target_os = "macos")]
40 | return &[Self::Full, Self::Transparent, Self::Buttonless, Self::None];
41 | #[cfg(not(target_os = "macos"))]
42 | return &[Self::Full, Self::None];
43 | }
44 |
45 | fn to_possible_value(&self) -> Option {
46 | Some(PossibleValue::new(<&str>::from(self)))
47 | }
48 | }
49 |
50 | impl fmt::Display for Frame {
51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 | write!(f, "{}", <&str>::from(self))
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/helicoid-client/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use command_line::HeliconeCommandLineArguments;
3 | use window::create_window;
4 |
5 | mod bridge;
6 | //mod channel_utils;
7 | mod command_line;
8 | mod dimensions;
9 | mod editor;
10 | //mod event_aggregator;
11 | mod frame;
12 | mod redraw_scheduler;
13 | mod renderer;
14 | mod window;
15 |
16 | #[macro_use]
17 | extern crate lazy_static;
18 | /// Simple program to greet a person
19 |
20 | //#[tokio::main]
21 | fn main() {
22 | env_logger::init();
23 | let args = HeliconeCommandLineArguments::parse();
24 |
25 | let runtime = tokio::runtime::Builder::new_multi_thread()
26 | .enable_time()
27 | .thread_stack_size(16 * 1024 * 1024) // Especially in debug mode the font shaping stuff may need some more stack
28 | .enable_io()
29 | .build()
30 | .unwrap();
31 | let _guard = runtime.enter();
32 | /* We still want to run the GUI on the main thread, as this has benefits on some OSes */
33 | create_window(&args);
34 | }
35 |
--------------------------------------------------------------------------------
/helicoid-client/src/nv_editor/cursor.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, sync::Arc};
2 |
3 | use skia_safe::Color4f;
4 |
5 | use crate::editor::style::{Colors, Style};
6 |
7 | use super::grid::GridCell;
8 |
9 | #[derive(Debug, Clone, PartialEq)]
10 | pub enum CursorShape {
11 | Block,
12 | Horizontal,
13 | Vertical,
14 | }
15 |
16 | impl CursorShape {
17 | pub fn from_type_name(name: &str) -> Option {
18 | match name {
19 | "block" => Some(CursorShape::Block),
20 | "horizontal" => Some(CursorShape::Horizontal),
21 | "vertical" => Some(CursorShape::Vertical),
22 | _ => None,
23 | }
24 | }
25 | }
26 |
27 | #[derive(Default, Debug, Clone, PartialEq)]
28 | pub struct CursorMode {
29 | pub shape: Option,
30 | pub style_id: Option,
31 | pub cell_percentage: Option,
32 | pub blinkwait: Option,
33 | pub blinkon: Option,
34 | pub blinkoff: Option,
35 | }
36 |
37 | #[derive(Clone, Debug, PartialEq)]
38 | pub struct Cursor {
39 | pub grid_position: (u64, u64),
40 | pub parent_window_id: u64,
41 | pub shape: CursorShape,
42 | pub cell_percentage: Option,
43 | pub blinkwait: Option,
44 | pub blinkon: Option,
45 | pub blinkoff: Option,
46 | pub style: Option>,
47 | pub enabled: bool,
48 | pub double_width: bool,
49 | pub grid_cell: GridCell,
50 | }
51 |
52 | impl Cursor {
53 | pub fn new() -> Cursor {
54 | Cursor {
55 | grid_position: (0, 0),
56 | parent_window_id: 0,
57 | shape: CursorShape::Block,
58 | style: None,
59 | cell_percentage: None,
60 | blinkwait: None,
61 | blinkon: None,
62 | blinkoff: None,
63 | enabled: true,
64 | double_width: false,
65 | grid_cell: (" ".to_string(), None),
66 | }
67 | }
68 |
69 | pub fn foreground(&self, default_colors: &Colors) -> Color4f {
70 | self.style
71 | .as_ref()
72 | .and_then(|s| s.colors.foreground)
73 | .unwrap_or_else(|| default_colors.background.unwrap())
74 | }
75 |
76 | pub fn background(&self, default_colors: &Colors) -> Color4f {
77 | self.style
78 | .as_ref()
79 | .and_then(|s| s.colors.background)
80 | .unwrap_or_else(|| default_colors.foreground.unwrap())
81 | }
82 |
83 | pub fn alpha(&self) -> u8 {
84 | return self
85 | .style
86 | .as_ref()
87 | .map(|s| (255_f32 * ((100 - s.blend) as f32 / 100.0_f32)) as u8)
88 | .unwrap_or(255);
89 | }
90 |
91 | pub fn change_mode(&mut self, cursor_mode: &CursorMode, styles: &HashMap>) {
92 | let CursorMode {
93 | shape,
94 | style_id,
95 | cell_percentage,
96 | blinkwait,
97 | blinkon,
98 | blinkoff,
99 | } = cursor_mode;
100 |
101 | if let Some(shape) = shape {
102 | self.shape = shape.clone();
103 | }
104 |
105 | if let Some(style_id) = style_id {
106 | self.style = styles.get(style_id).cloned();
107 | }
108 |
109 | self.cell_percentage = *cell_percentage;
110 | self.blinkwait = *blinkwait;
111 | self.blinkon = *blinkon;
112 | self.blinkoff = *blinkoff;
113 | }
114 | }
115 |
116 | #[cfg(test)]
117 | mod tests {
118 | use super::*;
119 |
120 | const COLORS: Colors = Colors {
121 | foreground: Some(Color4f::new(0.1, 0.1, 0.1, 0.1)),
122 | background: Some(Color4f::new(0.2, 0.1, 0.1, 0.1)),
123 | special: Some(Color4f::new(0.3, 0.1, 0.1, 0.1)),
124 | };
125 |
126 | const DEFAULT_COLORS: Colors = Colors {
127 | foreground: Some(Color4f::new(0.1, 0.2, 0.1, 0.1)),
128 | background: Some(Color4f::new(0.2, 0.2, 0.1, 0.1)),
129 | special: Some(Color4f::new(0.3, 0.2, 0.1, 0.1)),
130 | };
131 |
132 | const NONE_COLORS: Colors = Colors {
133 | foreground: None,
134 | background: None,
135 | special: None,
136 | };
137 |
138 | #[test]
139 | fn test_from_type_name() {
140 | assert_eq!(
141 | CursorShape::from_type_name("block"),
142 | Some(CursorShape::Block)
143 | );
144 | assert_eq!(
145 | CursorShape::from_type_name("horizontal"),
146 | Some(CursorShape::Horizontal)
147 | );
148 | assert_eq!(
149 | CursorShape::from_type_name("vertical"),
150 | Some(CursorShape::Vertical)
151 | );
152 | }
153 |
154 | #[test]
155 | fn test_foreground() {
156 | let mut cursor = Cursor::new();
157 | let style = Some(Arc::new(Style::new(COLORS)));
158 |
159 | assert_eq!(
160 | cursor.foreground(&DEFAULT_COLORS),
161 | DEFAULT_COLORS.background.unwrap()
162 | );
163 | cursor.style = style;
164 | assert_eq!(
165 | cursor.foreground(&DEFAULT_COLORS),
166 | COLORS.foreground.unwrap()
167 | );
168 |
169 | cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
170 | assert_eq!(
171 | cursor.foreground(&DEFAULT_COLORS),
172 | DEFAULT_COLORS.background.unwrap()
173 | );
174 | }
175 |
176 | #[test]
177 | fn test_background() {
178 | let mut cursor = Cursor::new();
179 | let style = Some(Arc::new(Style::new(COLORS)));
180 |
181 | assert_eq!(
182 | cursor.background(&DEFAULT_COLORS),
183 | DEFAULT_COLORS.foreground.unwrap()
184 | );
185 | cursor.style = style;
186 | assert_eq!(
187 | cursor.background(&DEFAULT_COLORS),
188 | COLORS.background.unwrap()
189 | );
190 |
191 | cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
192 | assert_eq!(
193 | cursor.background(&DEFAULT_COLORS),
194 | DEFAULT_COLORS.foreground.unwrap()
195 | );
196 | }
197 |
198 | #[test]
199 | fn test_change_mode() {
200 | let cursor_mode = CursorMode {
201 | shape: Some(CursorShape::Horizontal),
202 | style_id: Some(1),
203 | cell_percentage: Some(100.0),
204 | blinkwait: Some(1),
205 | blinkon: Some(1),
206 | blinkoff: Some(1),
207 | };
208 | let mut styles = HashMap::new();
209 | styles.insert(1, Arc::new(Style::new(COLORS)));
210 |
211 | let mut cursor = Cursor::new();
212 |
213 | cursor.change_mode(&cursor_mode, &styles);
214 | assert_eq!(cursor.shape, CursorShape::Horizontal);
215 | assert_eq!(cursor.style, styles.get(&1).cloned());
216 | assert_eq!(cursor.cell_percentage, Some(100.0));
217 | assert_eq!(cursor.blinkwait, Some(1));
218 | assert_eq!(cursor.blinkon, Some(1));
219 | assert_eq!(cursor.blinkoff, Some(1));
220 |
221 | let cursor_mode_with_none = CursorMode {
222 | shape: None,
223 | style_id: None,
224 | cell_percentage: None,
225 | blinkwait: None,
226 | blinkon: None,
227 | blinkoff: None,
228 | };
229 | cursor.change_mode(&cursor_mode_with_none, &styles);
230 | assert_eq!(cursor.shape, CursorShape::Horizontal);
231 | assert_eq!(cursor.style, styles.get(&1).cloned());
232 | assert_eq!(cursor.cell_percentage, None);
233 | assert_eq!(cursor.blinkwait, None);
234 | assert_eq!(cursor.blinkon, None);
235 | assert_eq!(cursor.blinkoff, None);
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/helicoid-client/src/nv_editor/draw_command_batcher.rs:
--------------------------------------------------------------------------------
1 | use std::sync::mpsc::{channel, Receiver, SendError, Sender};
2 |
3 | use crate::{editor::DrawCommand, event_aggregator::EVENT_AGGREGATOR};
4 |
5 | pub struct DrawCommandBatcher {
6 | window_draw_command_sender: Sender,
7 | window_draw_command_receiver: Receiver,
8 | }
9 |
10 | impl DrawCommandBatcher {
11 | pub fn new() -> DrawCommandBatcher {
12 | let (sender, receiver) = channel();
13 |
14 | DrawCommandBatcher {
15 | window_draw_command_sender: sender,
16 | window_draw_command_receiver: receiver,
17 | }
18 | }
19 |
20 | pub fn queue(&self, draw_command: DrawCommand) -> Result<(), SendError> {
21 | self.window_draw_command_sender.send(draw_command)
22 | }
23 |
24 | pub fn send_batch(&self) {
25 | let batch: Vec = self.window_draw_command_receiver.try_iter().collect();
26 | EVENT_AGGREGATOR.send(batch);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/helicoid-client/src/nv_editor/grid.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use crate::editor::style::Style;
4 |
5 | pub type GridCell = (String, Option>);
6 |
7 | #[macro_export]
8 | macro_rules! default_cell {
9 | () => {
10 | (" ".to_owned(), None)
11 | };
12 | }
13 |
14 | pub struct CharacterGrid {
15 | pub width: u64,
16 | pub height: u64,
17 |
18 | characters: Vec,
19 | }
20 |
21 | impl CharacterGrid {
22 | pub fn new(size: (u64, u64)) -> CharacterGrid {
23 | let (width, height) = size;
24 | let cell_count = (width * height) as usize;
25 | CharacterGrid {
26 | characters: vec![default_cell!(); cell_count],
27 | width,
28 | height,
29 | }
30 | }
31 |
32 | pub fn resize(&mut self, (width, height): (u64, u64)) {
33 | let new_cell_count = (width * height) as usize;
34 | let mut new_characters = vec![default_cell!(); new_cell_count];
35 |
36 | for x in 0..self.width.min(width) {
37 | for y in 0..self.height.min(height) {
38 | if let Some(existing_cell) = self.get_cell(x, y) {
39 | new_characters[(x + y * width) as usize] = existing_cell.clone();
40 | }
41 | }
42 | }
43 |
44 | self.width = width;
45 | self.height = height;
46 | self.characters = new_characters;
47 | }
48 |
49 | pub fn clear(&mut self) {
50 | self.set_all_characters(default_cell!());
51 | }
52 |
53 | fn cell_index(&self, x: u64, y: u64) -> Option {
54 | if x >= self.width || y >= self.height {
55 | None
56 | } else {
57 | Some((x + y * self.width) as usize)
58 | }
59 | }
60 |
61 | pub fn get_cell(&self, x: u64, y: u64) -> Option<&GridCell> {
62 | self.cell_index(x, y).map(|idx| &self.characters[idx])
63 | }
64 |
65 | pub fn get_cell_mut(&mut self, x: u64, y: u64) -> Option<&mut GridCell> {
66 | self.cell_index(x, y)
67 | .map(move |idx| &mut self.characters[idx])
68 | }
69 |
70 | pub fn set_all_characters(&mut self, value: GridCell) {
71 | self.characters.clear();
72 | self.characters
73 | .resize_with((self.width * self.height) as usize, || value.clone());
74 | }
75 |
76 | pub fn row(&self, row_index: u64) -> Option<&[GridCell]> {
77 | if row_index < self.height {
78 | Some(
79 | &self.characters
80 | [(row_index * self.width) as usize..((row_index + 1) * self.width) as usize],
81 | )
82 | } else {
83 | None
84 | }
85 | }
86 | }
87 |
88 | #[cfg(test)]
89 | mod tests {
90 | use super::*;
91 | use crate::editor::style::Colors;
92 | use rand::*;
93 |
94 | #[derive(Debug)]
95 | struct Context {
96 | none_colors: Colors,
97 | size: (u64, u64),
98 | x: u64,
99 | y: u64,
100 | area: usize,
101 | index: usize,
102 | }
103 |
104 | impl Context {
105 | fn new() -> Self {
106 | let size = (
107 | (thread_rng().gen::() % 500) + 1,
108 | (thread_rng().gen::() % 500) + 1,
109 | );
110 | let (x, y) = (
111 | thread_rng().gen::() % size.0,
112 | thread_rng().gen::() % size.1,
113 | );
114 | Self {
115 | none_colors: Colors {
116 | foreground: None,
117 | background: None,
118 | special: None,
119 | },
120 | size,
121 | x,
122 | y,
123 | area: (size.0 * size.1) as usize,
124 | index: (x + y * size.0) as usize,
125 | }
126 | }
127 | }
128 |
129 | #[test]
130 | fn new_constructs_grid() {
131 | let context = Context::new();
132 |
133 | // RUN FUNCTION
134 | let character_grid = CharacterGrid::new(context.size);
135 | assert_eq!(character_grid.width, context.size.0);
136 | assert_eq!(character_grid.height, context.size.1);
137 | assert_eq!(
138 | character_grid.characters,
139 | vec![default_cell!(); context.area]
140 | );
141 | }
142 |
143 | #[test]
144 | fn get_cell_returns_expected_cell() {
145 | let context = Context::new();
146 | let mut character_grid = CharacterGrid::new(context.size);
147 |
148 | character_grid.characters[context.index] = (
149 | "foo".to_string(),
150 | Some(Arc::new(Style::new(context.none_colors.clone()))),
151 | );
152 | let result = (
153 | "foo".to_string(),
154 | Some(Arc::new(Style::new(context.none_colors.clone()))),
155 | );
156 |
157 | // RUN FUNCTION
158 | assert_eq!(
159 | character_grid.get_cell(context.x, context.y).unwrap(),
160 | &result
161 | );
162 | }
163 |
164 | #[test]
165 | fn get_cell_mut_modifiers_grid_properly() {
166 | let context = Context::new();
167 | let mut character_grid = CharacterGrid::new(context.size);
168 |
169 | character_grid.characters[context.index] = (
170 | "foo".to_string(),
171 | Some(Arc::new(Style::new(context.none_colors.clone()))),
172 | );
173 | let result = (
174 | "bar".to_string(),
175 | Some(Arc::new(Style::new(context.none_colors.clone()))),
176 | );
177 |
178 | // RUN FUNCTION
179 | let cell = character_grid.get_cell_mut(context.x, context.y).unwrap();
180 | *cell = (
181 | "bar".to_string(),
182 | Some(Arc::new(Style::new(context.none_colors.clone()))),
183 | );
184 |
185 | assert_eq!(
186 | character_grid.get_cell_mut(context.x, context.y).unwrap(),
187 | &result
188 | );
189 | }
190 |
191 | #[test]
192 | fn set_all_characters_sets_all_cells_to_given_character() {
193 | let context = Context::new();
194 | let grid_cell = (
195 | "foo".to_string(),
196 | Some(Arc::new(Style::new(context.none_colors))),
197 | );
198 | let mut character_grid = CharacterGrid::new(context.size);
199 |
200 | // RUN FUNCTION
201 | character_grid.set_all_characters(grid_cell.clone());
202 | assert_eq!(character_grid.characters, vec![grid_cell; context.area]);
203 | }
204 |
205 | #[test]
206 | fn clear_empties_buffer() {
207 | let context = Context::new();
208 | let mut character_grid = CharacterGrid::new(context.size);
209 |
210 | let grid_cell = (
211 | "foo".to_string(),
212 | Some(Arc::new(Style::new(context.none_colors))),
213 | );
214 | character_grid.characters = vec![grid_cell; context.area];
215 |
216 | // RUN FUNCTION
217 | character_grid.clear();
218 |
219 | assert_eq!(character_grid.width, context.size.0);
220 | assert_eq!(character_grid.height, context.size.1);
221 | assert_eq!(
222 | character_grid.characters,
223 | vec![default_cell!(); context.area]
224 | );
225 | }
226 |
227 | #[test]
228 | fn resize_clears_and_resizes_grid() {
229 | let context = Context::new();
230 | let mut character_grid = CharacterGrid::new(context.size);
231 | let (width, height) = (
232 | (thread_rng().gen::() % 500) + 1,
233 | (thread_rng().gen::() % 500) + 1,
234 | );
235 |
236 | let grid_cell = (
237 | "foo".to_string(),
238 | Some(Arc::new(Style::new(context.none_colors))),
239 | );
240 | character_grid.characters = vec![grid_cell.clone(); context.area];
241 |
242 | // RUN FUNCTION
243 | character_grid.resize((width, height));
244 |
245 | assert_eq!(character_grid.width, width);
246 | assert_eq!(character_grid.height, height);
247 |
248 | let (original_width, original_height) = context.size;
249 | for x in 0..original_width.min(width) {
250 | for y in 0..original_height.min(height) {
251 | assert_eq!(character_grid.get_cell(x, y).unwrap(), &grid_cell);
252 | }
253 | }
254 |
255 | for x in original_width..width {
256 | for y in original_height..height {
257 | assert_eq!(character_grid.get_cell(x, y).unwrap(), &default_cell!());
258 | }
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/helicoid-client/src/nv_editor/style.rs:
--------------------------------------------------------------------------------
1 | use skia_safe::Color4f;
2 |
3 | #[derive(new, PartialEq, Debug, Clone)]
4 | pub struct Colors {
5 | pub foreground: Option,
6 | pub background: Option,
7 | pub special: Option,
8 | }
9 |
10 | #[derive(PartialEq, Debug, Clone, Copy)]
11 | pub enum UnderlineStyle {
12 | Underline,
13 | UnderDouble,
14 | UnderDash,
15 | UnderDot,
16 | UnderCurl,
17 | }
18 |
19 | #[derive(new, Debug, Clone, PartialEq)]
20 | pub struct Style {
21 | pub colors: Colors,
22 | #[new(default)]
23 | pub reverse: bool,
24 | #[new(default)]
25 | pub italic: bool,
26 | #[new(default)]
27 | pub bold: bool,
28 | #[new(default)]
29 | pub strikethrough: bool,
30 | #[new(default)]
31 | pub blend: u8,
32 | #[new(default)]
33 | pub underline: Option,
34 | }
35 |
36 | impl Style {
37 | pub fn foreground(&self, default_colors: &Colors) -> Color4f {
38 | if self.reverse {
39 | self.colors
40 | .background
41 | .unwrap_or_else(|| default_colors.background.unwrap())
42 | } else {
43 | self.colors
44 | .foreground
45 | .unwrap_or_else(|| default_colors.foreground.unwrap())
46 | }
47 | }
48 |
49 | pub fn background(&self, default_colors: &Colors) -> Color4f {
50 | if self.reverse {
51 | self.colors
52 | .foreground
53 | .unwrap_or_else(|| default_colors.foreground.unwrap())
54 | } else {
55 | self.colors
56 | .background
57 | .unwrap_or_else(|| default_colors.background.unwrap())
58 | }
59 | }
60 |
61 | pub fn special(&self, default_colors: &Colors) -> Color4f {
62 | self.colors
63 | .special
64 | .unwrap_or_else(|| self.foreground(default_colors))
65 | }
66 | }
67 |
68 | #[cfg(test)]
69 | mod tests {
70 | use super::*;
71 |
72 | const COLORS: Colors = Colors {
73 | foreground: Some(Color4f::new(0.1, 0.1, 0.1, 0.1)),
74 | background: Some(Color4f::new(0.2, 0.1, 0.1, 0.1)),
75 | special: Some(Color4f::new(0.3, 0.1, 0.1, 0.1)),
76 | };
77 |
78 | const DEFAULT_COLORS: Colors = Colors {
79 | foreground: Some(Color4f::new(0.1, 0.2, 0.1, 0.1)),
80 | background: Some(Color4f::new(0.2, 0.2, 0.1, 0.1)),
81 | special: Some(Color4f::new(0.3, 0.2, 0.1, 0.1)),
82 | };
83 |
84 | #[test]
85 | fn test_foreground() {
86 | let mut style = Style::new(COLORS);
87 |
88 | assert_eq!(
89 | style.foreground(&DEFAULT_COLORS),
90 | COLORS.foreground.unwrap()
91 | );
92 | style.colors.foreground = None;
93 | assert_eq!(
94 | style.foreground(&DEFAULT_COLORS),
95 | DEFAULT_COLORS.foreground.unwrap()
96 | );
97 | }
98 |
99 | #[test]
100 | fn test_foreground_reverse() {
101 | let mut style = Style::new(COLORS);
102 | style.reverse = true;
103 |
104 | assert_eq!(
105 | style.foreground(&DEFAULT_COLORS),
106 | COLORS.background.unwrap()
107 | );
108 | style.colors.background = None;
109 | assert_eq!(
110 | style.foreground(&DEFAULT_COLORS),
111 | DEFAULT_COLORS.background.unwrap()
112 | );
113 | }
114 |
115 | #[test]
116 | fn test_background() {
117 | let mut style = Style::new(COLORS);
118 |
119 | assert_eq!(
120 | style.background(&DEFAULT_COLORS),
121 | COLORS.background.unwrap()
122 | );
123 | style.colors.background = None;
124 | assert_eq!(
125 | style.background(&DEFAULT_COLORS),
126 | DEFAULT_COLORS.background.unwrap()
127 | );
128 | }
129 |
130 | #[test]
131 | fn test_background_reverse() {
132 | let mut style = Style::new(COLORS);
133 | style.reverse = true;
134 |
135 | assert_eq!(
136 | style.background(&DEFAULT_COLORS),
137 | COLORS.foreground.unwrap()
138 | );
139 | style.colors.foreground = None;
140 | assert_eq!(
141 | style.background(&DEFAULT_COLORS),
142 | DEFAULT_COLORS.foreground.unwrap()
143 | );
144 | }
145 |
146 | #[test]
147 | fn test_special() {
148 | let mut style = Style::new(COLORS);
149 |
150 | assert_eq!(style.special(&DEFAULT_COLORS), COLORS.special.unwrap());
151 | style.colors.special = None;
152 | assert_eq!(
153 | style.special(&DEFAULT_COLORS),
154 | style.foreground(&DEFAULT_COLORS),
155 | );
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/helicoid-client/src/redraw_scheduler.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | sync::{
3 | atomic::{AtomicBool, Ordering},
4 | Mutex,
5 | },
6 | time::Instant,
7 | };
8 |
9 | use log::trace;
10 |
11 | lazy_static! {
12 | pub static ref REDRAW_SCHEDULER: RedrawScheduler = RedrawScheduler::new();
13 | }
14 |
15 | pub struct RedrawScheduler {
16 | scheduled_frame: Mutex