├── .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 | ![Logo](./assets/helicoid_logo.svg) 3 | ![Screenshot](./assets/screenshot20230507.webp) 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 | 17 | 37 | 39 | 47 | 51 | 52 | 60 | 64 | 65 | 74 | 77 | 81 | 85 | 89 | 90 | 98 | 102 | 103 | 104 | 108 | 116 | 121 | 126 | 132 | 138 | 143 | 144 | 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 | 17 | 37 | 39 | 47 | 51 | 52 | 60 | 64 | 65 | 74 | 77 | 81 | 85 | 89 | 90 | 98 | 102 | 103 | 104 | 108 | 116 | 121 | 126 | 132 | 138 | 143 | 144 | 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>, 17 | frame_queued: AtomicBool, 18 | } 19 | 20 | impl RedrawScheduler { 21 | pub fn new() -> RedrawScheduler { 22 | RedrawScheduler { 23 | scheduled_frame: Mutex::new(None), 24 | frame_queued: AtomicBool::new(true), 25 | } 26 | } 27 | 28 | pub fn _schedule(&self, new_scheduled: Instant) { 29 | trace!("Redraw scheduled for {:?}", new_scheduled); 30 | let mut scheduled_frame = self.scheduled_frame.lock().unwrap(); 31 | 32 | if let Some(previous_scheduled) = *scheduled_frame { 33 | if new_scheduled < previous_scheduled { 34 | *scheduled_frame = Some(new_scheduled); 35 | } 36 | } else { 37 | *scheduled_frame = Some(new_scheduled); 38 | } 39 | } 40 | 41 | pub fn queue_next_frame(&self) { 42 | trace!("Next frame queued"); 43 | self.frame_queued.store(true, Ordering::Relaxed); 44 | } 45 | 46 | pub fn should_draw(&self) -> bool { 47 | if self.frame_queued.load(Ordering::Relaxed) { 48 | self.frame_queued.store(false, Ordering::Relaxed); 49 | true 50 | } else { 51 | let mut next_scheduled_frame = self.scheduled_frame.lock().unwrap(); 52 | 53 | if let Some(scheduled_frame) = *next_scheduled_frame { 54 | if scheduled_frame < Instant::now() { 55 | *next_scheduled_frame = None; 56 | true 57 | } else { 58 | false 59 | } 60 | } else { 61 | false 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /helicoid-client/src/renderer/animation_utils.rs: -------------------------------------------------------------------------------- 1 | use skia_safe::Point; 2 | 3 | #[allow(dead_code)] 4 | pub fn ease_linear(t: f32) -> f32 { 5 | t 6 | } 7 | 8 | #[allow(dead_code)] 9 | pub fn ease_in_quad(t: f32) -> f32 { 10 | t * t 11 | } 12 | 13 | #[allow(dead_code)] 14 | pub fn ease_out_quad(t: f32) -> f32 { 15 | -t * (t - 2.0) 16 | } 17 | 18 | #[allow(dead_code)] 19 | pub fn ease_in_out_quad(t: f32) -> f32 { 20 | if t < 0.5 { 21 | 2.0 * t * t 22 | } else { 23 | let n = t * 2.0 - 1.0; 24 | -0.5 * (n * (n - 2.0) - 1.0) 25 | } 26 | } 27 | 28 | #[allow(dead_code)] 29 | pub fn ease_in_cubic(t: f32) -> f32 { 30 | t * t * t 31 | } 32 | 33 | #[allow(dead_code)] 34 | pub fn ease_out_cubic(t: f32) -> f32 { 35 | let n = t - 1.0; 36 | n * n * n + 1.0 37 | } 38 | 39 | #[allow(dead_code)] 40 | pub fn ease_in_out_cubic(t: f32) -> f32 { 41 | let n = 2.0 * t; 42 | if n < 1.0 { 43 | 0.5 * n * n * n 44 | } else { 45 | let n = n - 2.0; 46 | 0.5 * (n * n * n + 2.0) 47 | } 48 | } 49 | 50 | #[allow(dead_code)] 51 | pub fn ease_in_expo(t: f32) -> f32 { 52 | if t == 0.0 { 53 | 0.0 54 | } else { 55 | 2.0f32.powf(10.0 * (t - 1.0)) 56 | } 57 | } 58 | 59 | #[allow(dead_code)] 60 | pub fn ease_out_expo(t: f32) -> f32 { 61 | if (t - 1.0).abs() < std::f32::EPSILON { 62 | 1.0 63 | } else { 64 | 1.0 - 2.0f32.powf(-10.0 * t) 65 | } 66 | } 67 | 68 | #[allow(dead_code)] 69 | pub fn lerp(start: f32, end: f32, t: f32) -> f32 { 70 | start + (end - start) * t 71 | } 72 | 73 | #[allow(dead_code)] 74 | pub fn ease(ease_func: fn(f32) -> f32, start: f32, end: f32, t: f32) -> f32 { 75 | lerp(start, end, ease_func(t)) 76 | } 77 | 78 | #[allow(dead_code)] 79 | pub fn ease_point(ease_func: fn(f32) -> f32, start: Point, end: Point, t: f32) -> Point { 80 | Point { 81 | x: ease(ease_func, start.x, end.x, t), 82 | y: ease(ease_func, start.y, end.y, t), 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod test { 88 | use super::*; 89 | 90 | #[test] 91 | fn test_lerp() { 92 | assert_eq!(lerp(1.0, 0.0, 1.0), 0.0); 93 | } 94 | 95 | #[test] 96 | fn test_ease_linear() { 97 | assert_eq!(ease(ease_linear, 1.0, 0.0, 1.0), 0.0); 98 | } 99 | 100 | #[test] 101 | fn test_ease_in_quad() { 102 | assert_eq!(ease(ease_in_quad, 1.00, 0.0, 1.0), 0.0); 103 | } 104 | 105 | #[test] 106 | fn test_ease_out_quad() { 107 | assert_eq!(ease(ease_out_quad, 1.0, 0.0, 1.0), 0.0); 108 | } 109 | 110 | #[test] 111 | fn test_ease_in_expo() { 112 | assert_eq!(ease(ease_in_expo, 1.0, 0.0, 1.0), 0.0); 113 | assert_eq!(ease(ease_in_expo, 1.0, 0.0, 0.0), 1.0); 114 | } 115 | 116 | #[test] 117 | fn test_ease_out_expo() { 118 | assert_eq!(ease(ease_out_expo, 1.0, 0.0, 1.0), 0.0); 119 | assert_eq!(ease(ease_out_expo, 1.0, 0.0, 1.1), 0.00048828125); 120 | } 121 | 122 | #[test] 123 | fn test_ease_in_out_quad() { 124 | assert_eq!(ease(ease_in_out_quad, 1.0, 0.0, 1.0), 0.0); 125 | assert_eq!(ease(ease_in_out_quad, 1.00, 0.0, 0.4), 0.67999995); 126 | } 127 | 128 | #[test] 129 | fn test_ease_in_cubic() { 130 | assert_eq!(ease(ease_in_cubic, 1.0, 0.0, 1.0), 0.0); 131 | } 132 | 133 | #[test] 134 | fn test_ease_out_cubic() { 135 | assert_eq!(ease(ease_out_cubic, 1.0, 0.0, 1.0), 0.0); 136 | } 137 | 138 | #[test] 139 | fn test_ease_in_out_cubic() { 140 | assert_eq!(ease(ease_in_out_cubic, 1.0, 0.0, 1.0), 0.0); 141 | assert_eq!(ease(ease_in_out_cubic, 1.0, 0.0, 0.25), 0.9375); 142 | } 143 | 144 | #[test] 145 | fn test_ease_point_linear() { 146 | let start = Point { x: 0.0, y: 0.0 }; 147 | let end = Point { x: 1.0, y: 1.0 }; 148 | assert_eq!(ease_point(ease_linear, start, end, 1.0), end); 149 | } 150 | 151 | #[test] 152 | fn test_ease_point_in_quad() { 153 | let start = Point { x: 0.0, y: 0.0 }; 154 | let end = Point { x: 1.0, y: 1.0 }; 155 | assert_eq!(ease_point(ease_in_quad, start, end, 1.0), end); 156 | } 157 | 158 | #[test] 159 | fn test_ease_point_out_quad() { 160 | let start = Point { x: 0.0, y: 0.0 }; 161 | let end = Point { x: 1.0, y: 1.0 }; 162 | assert_eq!(ease_point(ease_out_quad, start, end, 1.0), end); 163 | } 164 | 165 | #[test] 166 | fn test_ease_point_in_out_quad() { 167 | let start = Point { x: 0.0, y: 0.0 }; 168 | let end = Point { x: 1.0, y: 1.0 }; 169 | let expected = Point { 170 | x: 0.68000007, 171 | y: 0.68000007, 172 | }; 173 | assert_eq!(ease_point(ease_in_out_quad, start, end, 1.0), end); 174 | assert_eq!(ease_point(ease_in_out_quad, start, end, 1.4), expected); 175 | } 176 | 177 | #[test] 178 | fn test_ease_point_in_cubic() { 179 | let start = Point { x: 0.0, y: 0.0 }; 180 | let end = Point { x: 1.0, y: 1.0 }; 181 | assert_eq!(ease_point(ease_in_cubic, start, end, 1.0), end); 182 | } 183 | 184 | #[test] 185 | fn test_ease_point_out_cubic() { 186 | let start = Point { x: 0.0, y: 0.0 }; 187 | let end = Point { x: 1.0, y: 1.0 }; 188 | assert_eq!(ease_point(ease_out_cubic, start, end, 1.0), end); 189 | } 190 | 191 | #[test] 192 | fn test_ease_point_in_out_cubic() { 193 | let start = Point { x: 0.0, y: 0.0 }; 194 | let end = Point { x: 1.0, y: 1.0 }; 195 | let expected = Point { 196 | x: 0.0625, 197 | y: 0.0625, 198 | }; 199 | assert_eq!(ease_point(ease_in_out_cubic, start, end, 1.0), end); 200 | assert_eq!(ease_point(ease_in_out_cubic, start, end, 0.25), expected); 201 | } 202 | 203 | #[test] 204 | fn test_ease_point_in_expo() { 205 | let start = Point { x: 0.0, y: 0.0 }; 206 | let end = Point { x: 1.0, y: 1.0 }; 207 | assert_eq!(ease_point(ease_in_expo, start, end, 1.0), end); 208 | assert_eq!(ease_point(ease_in_expo, start, end, 0.0), start); 209 | } 210 | 211 | #[test] 212 | fn test_ease_point_out_expo() { 213 | let start = Point { x: 0.0, y: 0.0 }; 214 | let end = Point { x: 1.0, y: 1.0 }; 215 | let expected = Point { 216 | x: 0.9995117, 217 | y: 0.9995117, 218 | }; 219 | assert_eq!(ease_point(ease_out_expo, start, end, 1.0), end); 220 | assert_eq!(ease_point(ease_out_expo, start, end, 1.1), expected); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /helicoid-client/src/renderer/cursor_renderer/blink.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use crate::{editor::Cursor, redraw_scheduler::REDRAW_SCHEDULER}; 4 | 5 | #[derive(Debug)] 6 | pub enum BlinkState { 7 | Waiting, 8 | On, 9 | Off, 10 | } 11 | 12 | pub struct BlinkStatus { 13 | state: BlinkState, 14 | last_transition: Instant, 15 | previous_cursor: Option, 16 | } 17 | 18 | impl BlinkStatus { 19 | pub fn new() -> BlinkStatus { 20 | BlinkStatus { 21 | state: BlinkState::Waiting, 22 | last_transition: Instant::now(), 23 | previous_cursor: None, 24 | } 25 | } 26 | 27 | pub fn update_status(&mut self, new_cursor: &Cursor) -> bool { 28 | if self.previous_cursor.is_none() || new_cursor != self.previous_cursor.as_ref().unwrap() { 29 | self.previous_cursor = Some(new_cursor.clone()); 30 | self.last_transition = Instant::now(); 31 | if new_cursor.blinkwait.is_some() && new_cursor.blinkwait != Some(0) { 32 | self.state = BlinkState::Waiting; 33 | } else { 34 | self.state = BlinkState::On; 35 | } 36 | } 37 | 38 | if new_cursor.blinkwait == Some(0) 39 | || new_cursor.blinkoff == Some(0) 40 | || new_cursor.blinkon == Some(0) 41 | { 42 | return true; 43 | } 44 | 45 | let delay = match self.state { 46 | BlinkState::Waiting => new_cursor.blinkwait, 47 | BlinkState::Off => new_cursor.blinkoff, 48 | BlinkState::On => new_cursor.blinkon, 49 | } 50 | .filter(|millis| *millis > 0) 51 | .map(Duration::from_millis); 52 | 53 | if delay 54 | .map(|delay| self.last_transition + delay < Instant::now()) 55 | .unwrap_or(false) 56 | { 57 | self.state = match self.state { 58 | BlinkState::Waiting => BlinkState::On, 59 | BlinkState::On => BlinkState::Off, 60 | BlinkState::Off => BlinkState::On, 61 | }; 62 | self.last_transition = Instant::now(); 63 | } 64 | 65 | let scheduled_frame = (match self.state { 66 | BlinkState::Waiting => new_cursor.blinkwait, 67 | BlinkState::Off => new_cursor.blinkoff, 68 | BlinkState::On => new_cursor.blinkon, 69 | }) 70 | .map(|delay| self.last_transition + Duration::from_millis(delay)); 71 | 72 | if let Some(scheduled_frame) = scheduled_frame { 73 | REDRAW_SCHEDULER.schedule(scheduled_frame); 74 | } 75 | 76 | match self.state { 77 | BlinkState::Off => false, 78 | BlinkState::On | BlinkState::Waiting => true, 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /helicoid-client/src/renderer/fonts/font_loader.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /helicoid-client/src/renderer/fonts/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod blob_builder; 2 | -------------------------------------------------------------------------------- /helicoid-client/src/renderer/grid_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use glutin::dpi::PhysicalSize; 4 | use log::trace; 5 | use skia_safe::{ 6 | colors, dash_path_effect, BlendMode, Canvas, Color, Paint, Path, Point, Rect, HSV, 7 | }; 8 | 9 | use crate::{ 10 | //dimensions::Dimensions, 11 | editor::{Colors, Style, UnderlineStyle}, 12 | renderer::{CachingShaper, RendererSettings}, 13 | //settings::*, 14 | window::WindowSettings, 15 | }; 16 | /* 17 | pub struct GridRenderer { 18 | pub shaper: CachingShaper, 19 | pub paint: Paint, 20 | pub default_style: Arc