* OpenGL Backend (#486)

* opengl renderer sorta working

* add x11 build argument

* remove x11

* format

* remove alternate windowing systems

* remove non opengl windowing systems

* fix thread spin

* add graphics interface debugging expects

* update interface build and add conditional compilation

* install nightly in actions

* add format component to nightly install

* remove vulkan from action

* working without shaping

* Add rustybuzz

* applied suggestions from calvinkosmatka

* remove husky

* update skia-safe

* better subpixel font rendering

* commit lock file

* fix merge error

* Bump skia-safe to 0.39.1 (#584)

Compiling for apple silicon/m1 only works on 0.39.1

* remove sdl2 file and upgrade skia/swap to egl

* Update README.md to reflect changes in the main branch. Fix sequence break in Mac installation guide. (#615)

Co-authored-by: Lord Valen <lord_valen@protonmail.com>

* OpenGL Backend (#486)

* opengl renderer sorta working

* add x11 build argument

* remove x11

* format

* remove alternate windowing systems

* remove non opengl windowing systems

* fix thread spin

* add graphics interface debugging expects

* update interface build and add conditional compilation

* install nightly in actions

* add format component to nightly install

* remove vulkan from action

* working without shaping

* Add rustybuzz

* applied suggestions from calvinkosmatka

* remove husky

* update skia-safe

* better subpixel font rendering

* commit lock file

* fix merge error

* Bump skia-safe to 0.39.1 (#584)

Compiling for apple silicon/m1 only works on 0.39.1

* remove sdl2 file and upgrade skia/swap to egl

* Update README.md to reflect changes in the main branch. Fix sequence break in Mac installation guide. (#615)

Co-authored-by: Lord Valen <lord_valen@protonmail.com>

* Version check (#631)

* add more robust version check

* clippy

* fix some copy pasta

* revert font changes

* add lock file back

* pull in animation length change from main and adjust default cursor settings

* Snap for Ubuntu (#576)

* add more robust version check

* clippy

* fix some copy pasta

* revert font changes

* snap builds correctly

* add snapcraft workflow

* clean up workflow

* clean up workflow

* clean up workflow

* check workflow works

* build snap

* use lxd

* add snap push

* add step id

* use official snapcraft actions

* add snap badge

* move snap badge

* swap from rustybuzz to swash. WAY faster performance

* format files

* fix command line jump filtering

* attempt fix of github action

* add comma

* add caching to the build dependencies

* fix foating window position

* Fix 577 (#668)

* fix formatting and clippy errors

* upstream formatting fix

* Added trackpad support to scrolling/smoothscrolling in OpenGL branch (#681)

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Fixed regular scroll

* Update mod.rs

* Update mod.rs

* Reverted previous change, dividing first instead

* Update mod.rs

* Update mod.rs

* I completely forgot about how unsigned integers in rust work

* Remove the casting to integer

* Forgot function signature requires integer

* divide before cast

* remove semicolon

* Changed function signature to use float instead of integer, using float for PixelDelta

* Forgot parenthesis

* Change 0 to math float

* Do the same for the function itself

* Scrolling still too fast

* Still way to fast

* Still too fast

* Still too fast

* somehow still too fast

* Still too fast

* Update mod.rs

* testing

* still fast

* doesn't fit in f32 anymore

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Update mod.rs

* Testing

* Testing something new

* Update mod.rs

* Update mod.rs

* Deleted unnecessary code

* Implement CLI parsing with clap (#680)

* implemented command line parsing with clap

* removed println!

* fixed file opening

* added files parameter to avoid -- [FILES]

* use new cmdlinesettings in various places

* lets stick to the old cli-api

* moved to builder syntax to allow for hypens in args

* Fixed merge conflict that was overlooked before

* Basic Environment variables

* Added alias for MultiGrid environment var

* Fix for #566 (#687)

* add logging to async channels

* use better logging in channels

* made float change backwards compatible

* fix clippy work

* ignore new clippy lint

* fix mac clippy lint

* adjust actions

* use nightly format

* install utilities in mac and linux builds

* Fix some shift+key mappings (#695)

* fix publish test results step

* Basic Frameless window support (#694)

* Frameless window (not resizeable)

* Update

* Delete neovide.ico

idk how did this get here

* add readme tweak to make sure people know what neovim is

* add scoop instructions for windows

* implement manual font fallback with swash

* M1 Runner (#711)

* add self-hosted m1

* remove vulkan

* cahnge toolchain

* fmt

* clippy

* remove llvm

* upload m1

* checkout upstream build.yaml

* remove llvm step

* remove git caching for self hosted

* Add environment variables for window options (#708)

* Add Env for window options

* Remove PascalCase

* Font Fallback (#701)

* better font fallback

* actually fix font fallback

* swap to u64s for most font size solutions

* increase line height slightly and attempt stencil buffer fix

* fix divider issue

* clippy and formatting fixes

Co-authored-by: Keith Simmons <keithsim@microsoft.com>

* clippy fixes

* Fix default font (#719)

* size using Z

* default font ordering

* clippy fixes

* interactive wsl path

* fix formatting

Co-authored-by: Keith Simmons <keithsim@microsoft.com>
Co-authored-by: shaunsingh <71196912+shaunsingh@users.noreply.github.com>
Co-authored-by: Lord-Valen <46138807+Lord-Valen@users.noreply.github.com>
Co-authored-by: Lord Valen <lord_valen@protonmail.com>
Co-authored-by: j4qfrost <j4qfrost@gmail.com>
Co-authored-by: PyGamer0 <64531844+PyGamer0@users.noreply.github.com>
Co-authored-by: Obyoxar <43534802+ErikMayrhofer@users.noreply.github.com>
Co-authored-by: Benson Li <bensonbinbinli@gmail.com>
Co-authored-by: meluskyc <meluskyc@gmail.com>
macos-click-through
Keith Simmons 3 years ago committed by GitHub
parent c6a68915a2
commit c7694569bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
# These are supported funding model platforms # These are supported funding model platforms
github: Kethku github: Kethku
ko_fi: keithsimmons ko_fi: keithsimmons

@ -10,7 +10,21 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Check formatting - name: Install Nightly Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
components: rustfmt, clippy
override: true
- name: Install Utilities
run: |
cargo install cargo2junit
- uses: Swatinem/rust-cache@v1
- name: Check Formatting
run: | run: |
cargo fmt --all -- --check cargo fmt --all -- --check
@ -27,7 +41,13 @@ jobs:
NEOVIM_BIN: "C:/tools/neovim/Neovim/bin/nvim.exe" NEOVIM_BIN: "C:/tools/neovim/Neovim/bin/nvim.exe"
RUST_BACKTRACE: full RUST_BACKTRACE: full
run: | run: |
cargo test cargo test -- -- -Z unstable-options --format json | cargo2junit > results.xml
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action/composite@v1
if: always()
with:
files: results.xml
- name: Build Release - name: Build Release
run: | run: |
@ -45,17 +65,26 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Vulkan SDK - name: Install Nightly Toolchain
run: brew install apenngrace/vulkan/vulkan-sdk uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
components: rustfmt, clippy
override: true
- name: Check formatting - name: Install Utilities
run: |
cargo install cargo2junit
- uses: Swatinem/rust-cache@v1
- name: Check Formatting
run: | run: |
rustup component add rustfmt --toolchain stable-x86_64-apple-darwin
cargo fmt --all -- --check cargo fmt --all -- --check
- name: Lint with Clippy - name: Lint with Clippy
run: | run: |
rustup component add clippy --toolchain stable-x86_64-apple-darwin
cargo clippy --all -- -D warnings cargo clippy --all -- -D warnings
- name: Uninstall Conflicting LLVM - name: Uninstall Conflicting LLVM
@ -67,8 +96,16 @@ jobs:
brew install neovim brew install neovim
- name: Test - name: Test
env:
RUST_BACKTRACE: full
run: | run: |
RUST_BACKTRACE=full cargo test cargo test -- -- -Z unstable-options --format json | cargo2junit > results.xml
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action/composite@v1
if: always()
with:
files: results.xml
- name: Build Release - name: Build Release
run: | run: |
@ -76,18 +113,6 @@ jobs:
cargo install cargo-bundle cargo install cargo-bundle
cargo bundle --release cargo bundle --release
- name: Update Bundle
run: |
cd target/release/bundle/osx/Neovide.app/Contents
mkdir Frameworks
cp /usr/local/lib/libvulkan.dylib ./Frameworks/
cp /usr/local/lib/libMoltenVK.dylib ./Frameworks/
mkdir -p Resources/vulkan
cp -r /usr/local/share/vulkan/icd.d ./Resources/vulkan/
jq '.ICD.library_path = "../../../Frameworks/libMoltenVK.dylib"' ./Resources/vulkan/icd.d/MoltenVK_icd.json > MoltenVK_icd.json
mv MoltenVK_icd.json ./Resources/vulkan/icd.d/
install_name_tool -add_rpath "@executable_path/../Frameworks" ./MacOS/neovide
- name: Create .dmg file - name: Create .dmg file
run: | run: |
hdiutil create Neovide-uncompressed.dmg -volname "Neovide" -srcfolder target/release/bundle/osx hdiutil create Neovide-uncompressed.dmg -volname "Neovide" -srcfolder target/release/bundle/osx
@ -105,18 +130,25 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Vulkan SDK - name: Install Nightly Toolchain
run: | uses: actions-rs/toolchain@v1
curl -sL "http://packages.lunarg.com/lunarg-signing-key-pub.asc" | sudo apt-key add - with:
sudo curl -sLo "/etc/apt/sources.list.d/lunarg-vulkan-1.2.131-bionic.list" "http://packages.lunarg.com/vulkan/1.2.131/lunarg-vulkan-1.2.131-bionic.list" profile: minimal
sudo apt-get update -y --ignore-missing toolchain: nightly
sudo apt-get install -y vulkan-sdk components: rustfmt, clippy
override: true
- name: Install Utilities
run: |
cargo install cargo2junit
- uses: Swatinem/rust-cache@v1
- name: Install dependencies - name: Install Dependencies
run: | run: |
sudo apt-get install -y curl gnupg ca-certificates git gcc-multilib g++-multilib cmake libssl-dev pkg-config libfreetype6-dev libasound2-dev libexpat1-dev libxcb-composite0-dev libbz2-dev freeglut3-dev libxi-dev libsdl2-dev sudo apt-get install -y curl gnupg ca-certificates git gcc-multilib g++-multilib cmake libssl-dev pkg-config libfreetype6-dev libasound2-dev libexpat1-dev libxcb-composite0-dev libbz2-dev freeglut3-dev libxi-dev
- name: Check formatting - name: Check Formatting
run: | run: |
cargo fmt --all -- --check cargo fmt --all -- --check
@ -129,8 +161,16 @@ jobs:
sudo apt-get install -y neovim sudo apt-get install -y neovim
- name: Test - name: Test
env:
RUST_BACKTRACE: full
run: | run: |
RUST_BACKTRACE=full cargo test cargo test -- -- -Z unstable-options --format json | cargo2junit > results.xml
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:
files: results.xml
- name: Build Release - name: Build Release
run: | run: |
@ -140,3 +180,62 @@ jobs:
with: with:
name: neovide-linux name: neovide-linux
path: ./target/release/neovide path: ./target/release/neovide
build-m1:
runs-on: self-hosted
steps:
- uses: actions/checkout@v2
- name: Install Nightly Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
components: rustfmt, clippy
override: true
- name: Install Utilities
run: |
cargo install cargo2junit
- name: Check Formatting
run: |
cargo fmt --all -- --check
- name: Lint with Clippy
run: |
cargo clippy --all -- -D warnings
- name: Install Neovim
run: |
brew install neovim
- name: Test
env:
RUST_BACKTRACE: full
run: |
cargo test -- -- -Z unstable-options --format json | cargo2junit > results.xml
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action/composite@v1
if: always()
with:
files: results.xml
- name: Build Release
run: |
cargo build --release
cargo install cargo-bundle
cargo bundle --release
- name: Create .dmg file
run: |
hdiutil create Neovide-uncompressed.dmg -volname "Neovide" -srcfolder target/release/bundle/osx
hdiutil convert Neovide-uncompressed.dmg -format UDZO -o Neovide.dmg
- uses: actions/upload-artifact@v1
with:
name: Neovide-m1.dmg
path: ./Neovide.dmg

@ -0,0 +1,26 @@
name: Snap
on: [push]
jobs:
snap:
runs-on: ubuntu-18.04
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- uses: snapcore/action-build@v1
env:
SNAPCRAFT_BUILD_ENVIRONMENT_MEMORY: 6G
id: snapcraft
- uses: actions/upload-artifact@v2
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}
- uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPCRAFT_TOKEN }}
snap: ${{ steps.snapcraft.outputs.snap }}

@ -1,31 +1,15 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "(Mac/Linux) Debug", "name": "Launch",
"type": "lldb", "type": "lldb",
"request": "launch", "request": "launch",
"program": "${workspaceRoot}/target/debug/neovide", "program": "${workspaceFolder}/target/debug/neovide.exe",
"args": [], "args": [],
"cwd": "${workspaceRoot}", },
"env": { ]
"RUST_LOG": "info", }
"RUST_BACKTRACE": "1"
}
},
{
"name": "(Windows) Debug",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceRoot}/target/debug/neovide.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceRoot}",
"environment": [],
"externalConsole": true
},
]
}

1647
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -8,62 +8,57 @@ description = "A simple GUI for Neovim."
resolver = "2" resolver = "2"
[workspace] [workspace]
members = [ members = ["neovide-derive"]
"neovide-derive"
]
[features] [features]
default = ["sdl2"] default = []
embed-fonts = [] embed-fonts = []
sdl2 = ["skulpin/skulpin_sdl2"]
winit = ["skulpin/skulpin_winit", "skulpin/winit-23"]
[dependencies] [dependencies]
neovide-derive = { path = "neovide-derive" } neovide-derive = { path = "neovide-derive" }
euclid = "0.20.7" euclid = "0.20.7"
font-kit = "0.10.0"
skribo = { git = "https://github.com/linebender/skribo" }
lru = "0.4.3" lru = "0.4.3"
skulpin = "0.11.0"
derive-new = "0.5" derive-new = "0.5"
rmpv = "0.4.4" rmpv = "0.4.4"
rust-embed = { version = "5.2.0", features = ["debug-embed"] } rust-embed = { version = "5.2.0", features = ["debug-embed"] }
image = "0.22.3" image = "0.22.3"
nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = [ "use_tokio" ] } nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = ["use_tokio"] }
tokio = { version = "0.2.9", features = [ "blocking", "process", "time", "tcp" ] } tokio = { version = "0.2.9", features = ["blocking", "process", "time", "tcp"] }
async-trait = "0.1.18" async-trait = "0.1.18"
crossfire = "0.1" crossfire = "0.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"
unicode-segmentation = "1.6.0" unicode-segmentation = "1.6.0"
log = "0.4.8" log = "0.4.8"
flexi_logger = { version = "0.14.6", default-features = false } flexi_logger = { version = "0.17.1", default-features = false }
anyhow = "1.0.26" parking_lot = "0.10.0"
parking_lot="0.10.0"
cfg-if = "0.1.10" cfg-if = "0.1.10"
which = "4" which = "4"
dirs = "2" dirs = "2"
rand = "0.7" rand = "0.7"
skia-safe = "0.32.1"
pin-project = "0.4.27" pin-project = "0.4.27"
futures = "0.3.12" futures = "0.3.12"
glutin = "0.26"
gl = "0.14.0"
regex = "1.5.4"
swash = "0.1.2"
clap="2.33.3"
[dev-dependencies] [dev-dependencies]
mockall = "0.7.0" mockall = "0.7.0"
[dev-dependencies.cargo-husky]
version = "1"
default-features = false
features = ["precommit-hook", "run-cargo-test", "run-cargo-clippy", "run-cargo-fmt"]
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] } winapi = { version = "0.3.9", features = ["winuser"] }
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1.11" winres = "0.1.11"
sdl2-sys = { version = "0.34.4", default-features = false, features = ["bundled", "static-link"] }
[target.'cfg(macos)'.build-dependencies] [target.'cfg(linux)'.dependencies.skia-safe]
sdl2-sys = { version = "0.34.4", default-features = false, features = ["bundled", "static-link"] } features = ["gl", "egl"]
version = "0.39.1"
[target.'cfg(not(linux))'.dependencies.skia-safe]
features = ["gl"]
version = "0.39.1"
[profile.release] [profile.release]
debug = true debug = true
@ -74,9 +69,9 @@ incremental = true
name = "Neovide" name = "Neovide"
identifier = "com.kethku.neovide" identifier = "com.kethku.neovide"
icon = ["assets/nvim.ico"] icon = ["assets/nvim.ico"]
version = "0.6.0" version = "0.7.0"
resources = [] resources = []
copyright = "Copyright (c) keith 2020. All rights reserved." copyright = "Copyright (c) Keith 2021. All rights reserved."
category = "Productivity" category = "Productivity"
short_description = "A simple GUI for Neovim." short_description = "A simple GUI for Neovim."
long_description = """ long_description = """

@ -1,7 +1,7 @@
# Neovide [![Gitter](https://badges.gitter.im/neovide/community.svg)](https://gitter.im/neovide/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Discussions](https://img.shields.io/badge/GitHub-Discussions-green?logo=github)](https://github.com/Kethku/neovide/discussions) # Neovide [![Gitter](https://badges.gitter.im/neovide/community.svg)](https://gitter.im/neovide/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Discussions](https://img.shields.io/badge/GitHub-Discussions-green?logo=github)](https://github.com/Kethku/neovide/discussions)
This is a simple graphical user interface for Neovim. Where possible there are some graphical improvements, but it should act This is a simple graphical user interface for [Neovim](https://github.com/neovim/neovim) (an aggressively refactored and updated
functionally like the terminal UI. Vim editor). Where possible there are some graphical improvements, but functionally it should act like the terminal UI.
![Basic Screen Cap](./assets/BasicScreenCap.png) ![Basic Screen Cap](./assets/BasicScreenCap.png)
@ -14,7 +14,7 @@ any critiques that you might have to offer. I won't take all of them, but I prom
## Features ## Features
Should be a standard full features Neovim GUI. Beyond that there are some visual niceties: Should be a standard fully featured Neovim GUI. Beyond that there are some visual niceties:
### Ligatures ### Ligatures
@ -84,6 +84,8 @@ By specifying to listen on localhost, you only allow connections from your local
ssh -L 6666:localhost:6666 ip.of.other.machine nvim --headless --listen localhost:6666 ssh -L 6666:localhost:6666 ip.of.other.machine nvim --headless --listen localhost:6666
``` ```
Finally, if you would like to leave the neovim server running, close the neovide application window instead of issuing a `:q` command.
### Some Nonsense ;) ### Some Nonsense ;)
```vim ```vim
@ -110,11 +112,25 @@ necessary. On Windows this should be enabled by default if you have a relatively
### From binary ### From binary
Relatively recent binaries can be found in the [project releases](https://github.com/Kethku/neovide/releases). But if you want the latest and greatest you should clone it and build yourself. Building instructions are somewhat limited at the moment. All the libraries I use are cross platform and should have support for Windows, Mac, and Linux. The rendering is based on opengl, so a good gpu driver will be
necessary. On Windows this should be enabled by default if you have a relatively recent system.
Installing should be as simple as downloading the binary, making sure `nvim.exe` with version 0.4 or greater is on your path, and running it. Everything should be self contained. Installing should be as simple as downloading the binary, making sure `nvim.exe` with version 0.4 or greater is on your path, and running it. Everything should be self contained.
### Windows (from source) ### Windows
#### Package manager
[Scoop](https://scoop.sh/) has Neovide in the `extras` bucket. Ensure you have the `extras` bucket, and install:
```
$ scoop bucket list
main
extras
$ scoop install neovide
```
#### From source
1. Install the latest version of Rust. I recommend <https://rustup.rs/> 1. Install the latest version of Rust. I recommend <https://rustup.rs/>
2. Install CMake. I use chocolatey: `choco install cmake --installargs '"ADD_CMAKE_TO_PATH=System"' -y` 2. Install CMake. I use chocolatey: `choco install cmake --installargs '"ADD_CMAKE_TO_PATH=System"' -y`
@ -134,16 +150,10 @@ Installing should be as simple as downloading the binary, making sure `nvim.exe`
1. Install the latest version of Rust. I recommend <https://rustup.rs/> 1. Install the latest version of Rust. I recommend <https://rustup.rs/>
2. Install CMake. Using homebrew: `brew install cmake` 2. Install CMake. Using homebrew: `brew install cmake`
3. Install the Vulkan SDK. I'm told `brew install apenngrace/vulkan/vulkan-sdk` works, but I can't test locally to find out. 3. `git clone https://github.com/Kethku/neovide`
4. Build and install Neovide: 4. `cd neovide`
5. `cargo build --release`
```sh 6. Copy `./target/release/neovide` to a known location and enjoy.
git clone https://github.com/Kethku/neovide
cd neovide
cargo build --release
```
5. Copy `./target/release/neovide` to a known location and enjoy.
### Linux ### Linux
@ -165,6 +175,20 @@ cd neovide-git
makepkg -si makepkg -si
``` ```
To install a non-default branch:
```sh
git clone https://aur.archlinux.org/neovide-git.git
cd neovide-git
nvim PKGBUILD
:%s/l}/l}#branch=branch-name-here/
:wq
makepkg -si
```
Note: Neovide requires that a font be set in `init.vim` otherwise errors might be encountered.
See [#527](https://github.com/Kethku/neovide/issues/527)
##### With non-default branch ##### With non-default branch
```sh ```sh
@ -186,40 +210,31 @@ makepkg -si
libbz2-dev libsndio-dev freeglut3-dev libxmu-dev libxi-dev libbz2-dev libsndio-dev freeglut3-dev libxmu-dev libxi-dev
``` ```
2. Install Vulkan SDK (adjust for your preferred package manager, may already be in your repository) 2. Install Rust
```sh
curl -sL "http://packages.lunarg.com/lunarg-signing-key-pub.asc" | sudo apt-key add -
sudo curl -sLo "/etc/apt/sources.list.d/lunarg-vulkan-1.2.131-bionic.list" "http://packages.lunarg.com/vulkan/1.2.131/lunarg-vulkan-1.2.131-bionic.list"
sudo apt update -y
sudo apt install -y vulkan-sdk
```
3. Install Rust
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf "https://sh.rustup.rs" | sh curl --proto '=https' --tlsv1.2 -sSf "https://sh.rustup.rs" | sh
``` ```
4. Clone the repository 3. Clone the repository
```sh ```sh
git clone "https://github.com/Kethku/neovide" git clone "https://github.com/Kethku/neovide"
``` ```
5. Build 4. Build
```sh ```sh
cd neovide && ~/.cargo/bin/cargo build --release cd neovide && ~/.cargo/bin/cargo build --release
``` ```
6. Copy `./target/release/neovide` to a known location and enjoy. 5. Copy `./target/release/neovide` to a known location and enjoy.
## Troubleshooting ## Troubleshooting
- Neovide requires that a font be set in `init.vim` otherwise errors might be encountered. This can be fixed by adding `set guifont=Your\ Font\ Name:h15` in init.vim file. Reference issue [#527](https://github.com/Kethku/neovide/issues/527). - Neovide requires that a font be set in `init.vim` otherwise errors might be encountered. This can be fixed by adding `set guifont=Your\ Font\ Name:h15` in init.vim file. Reference issue [#527](https://github.com/Kethku/neovide/issues/527).
- On OSX, if you run into issues with the vulkan libraries being reported as not verified, please reference issue [#167](https://github.com/Kethku/neovide/issues/167#issuecomment-593314579).
### Linux-specific ### Linux-specific
- If you recieve an error of `SdlError("Installed Vulkan doesn't implement the VK_KHR_surface extension")`, please try installing the AMD Vulkan Driver (commonly, `amdvlk`). Reference issue [#209](https://github.com/Kethku/neovide/issues/209).
- If you recieve errors complaining about DRI3 settings, please reference issue [#44](https://github.com/Kethku/neovide/issues/44#issuecomment-578618052). - If you recieve errors complaining about DRI3 settings, please reference issue [#44](https://github.com/Kethku/neovide/issues/44#issuecomment-578618052).
- If you recieve libsndio-related errors, try building without default features (this disables static linking of the SDL library).
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/neovide)

Binary file not shown.

@ -0,0 +1,86 @@
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

@ -0,0 +1,114 @@
name: neovide # you probably want to 'snapcraft register <name>'
base: core18 # the base snap is the execution environment for this snap
version: "0.7.1+git"
summary: The snappiest vim editor you are likely to find.
description: |
This is a simple graphical user interface for Neovim. Where possible there are some graphical improvements,
but it should act functionally like the terminal UI.
grade: devel # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
build-packages:
- cmake
- freeglut3-dev
- libbz2-dev
- libexpat1-dev
- libgl-dev
- libssl-dev
- libxmu-dev
- pkg-config
parts:
nvim:
source: https://github.com/neovim/neovim.git
override-pull: |
snapcraftctl pull
latest_tag="$(git tag -l --sort=refname|head -1)"
git checkout "${latest_tag}"
major="$(awk '/NVIM_VERSION_MAJOR/{gsub(")","",$2); print $2}' CMakeLists.txt)"
minor="$(awk '/NVIM_VERSION_MINOR/{gsub(")","",$2); print $2}' CMakeLists.txt)"
patch="$(awk '/NVIM_VERSION_PATCH/{gsub(")","",$2); print $2}' CMakeLists.txt)"
version_prefix="v$major.$minor.$patch"
git_described="$(git describe --first-parent --dirty 2> /dev/null | perl -lpe 's/v\d.\d.\d-//g')"
git_described="${git_described:-$(git describe --first-parent --tags --always --dirty)}"
if [ "${version_prefix}" != "${git_described}" ]; then
VERSION="${version_prefix}-${git_described}-${latest_tag}"
else
VERSION="${version_prefix}-${latest_tag}"
fi
snapcraftctl set-version "${VERSION}"
plugin: make
make-parameters:
- CMAKE_BUILD_TYPE=RelWithDebInfo
- CMAKE_INSTALL_PREFIX=/usr
- CMAKE_FLAGS=-DPREFER_LUA=ON
- DEPS_CMAKE_FLAGS="-DUSE_BUNDLED_LUA=ON -DUSE_BUNDLED_LUAJIT=OFF"
override-build: |
echo "Building on $SNAP_ARCH"
set -x
case "$SNAP_ARCH" in
"arm64" | "ppc64el" | "s390x")
make -j"${SNAPCRAFT_PARALLEL_BUILD_COUNT}" \
CMAKE_BUILD_TYPE=RelWithDebInfo \
CMAKE_INSTALL_PREFIX=/usr \
CMAKE_FLAGS=-DPREFER_LUA=ON \
DEPS_CMAKE_FLAGS="-DUSE_BUNDLED_LUA=ON -DUSE_BUNDLED_LUAJIT=OFF"
;;
*)
make -j"${SNAPCRAFT_PARALLEL_BUILD_COUNT}" \
CMAKE_BUILD_TYPE=RelWithDebInfo \
CMAKE_INSTALL_PREFIX=/usr
;;
esac
make DESTDIR="$SNAPCRAFT_PART_INSTALL" install
# Fix Desktop file
sed -i 's|^Exec=nvim|Exec=/snap/bin/nvim.nvim|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/nvim.desktop
sed -i 's|^TryExec=nvim|TryExec=/snap/bin/nvim.nvim|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/nvim.desktop
sed -i 's|^Icon=.*|Icon=${SNAP}/usr/share/pixmaps/nvim.png|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/nvim.desktop
build-packages:
- ninja-build
- libtool
- libtool-bin
- autoconf
- automake
- gawk
- g++
- git
- gettext
- unzip
- wget
prime:
- -usr/share/man
desktop-glib-only:
plugin: make
source: https://github.com/ubuntu/snapcraft-desktop-helpers.git
source-subdir: glib-only
neovide:
plugin: rust
source: .
build-packages:
- fontconfig
- libfontconfig1-dev
- libfreetype6-dev
stage-packages:
- fontconfig
- fonts-noto
- libfontconfig1
- libibus-1.0-5
- libpng16-16
- libx11-dev
- libx11-xcb1
- libxcursor1
- libxi-dev
- libxrandr-dev
- locales-all
- xdg-user-dirs
apps:
neovide:
command: bin/neovide
plugs:
- desktop
- desktop-legacy
command-chain:
- bin/desktop-launch

@ -1,40 +1,43 @@
use std::convert::TryInto; use std::convert::TryInto;
use std::error; use std::error;
use std::fmt; use std::fmt;
use std::fmt::Debug;
use rmpv::Value; use rmpv::Value;
use skulpin::skia_safe::Color4f; use skia_safe::Color4f;
use crate::editor::{Colors, CursorMode, CursorShape, Style}; use crate::editor::{Colors, CursorMode, CursorShape, Style};
#[derive(Debug, Clone)] #[derive(Clone, Debug)]
pub enum ParseError { pub enum ParseError {
InvalidArray(Value), Array(Value),
InvalidMap(Value), Map(Value),
InvalidString(Value), String(Value),
InvalidU64(Value), U64(Value),
InvalidI64(Value), I64(Value),
InvalidF64(Value), F64(Value),
InvalidBool(Value), Bool(Value),
InvalidWindowAnchor(Value), WindowAnchor(Value),
InvalidFormat, Format(String),
} }
type Result<T> = std::result::Result<T, ParseError>; type Result<T> = std::result::Result<T, ParseError>;
impl fmt::Display for ParseError { impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
ParseError::InvalidArray(value) => write!(f, "invalid array format {}", value), ParseError::Array(value) => write!(f, "invalid array format {}", value),
ParseError::InvalidMap(value) => write!(f, "invalid map format {}", value), ParseError::Map(value) => write!(f, "invalid map format {}", value),
ParseError::InvalidString(value) => write!(f, "invalid string format {}", value), ParseError::String(value) => write!(f, "invalid string format {}", value),
ParseError::InvalidU64(value) => write!(f, "invalid u64 format {}", value), ParseError::U64(value) => write!(f, "invalid u64 format {}", value),
ParseError::InvalidI64(value) => write!(f, "invalid i64 format {}", value), ParseError::I64(value) => write!(f, "invalid i64 format {}", value),
ParseError::InvalidF64(value) => write!(f, "invalid f64 format {}", value), ParseError::F64(value) => write!(f, "invalid f64 format {}", value),
ParseError::InvalidBool(value) => write!(f, "invalid bool format {}", value), ParseError::Bool(value) => write!(f, "invalid bool format {}", value),
ParseError::InvalidWindowAnchor(value) => { ParseError::WindowAnchor(value) => {
write!(f, "invalid window anchor format {}", value) write!(f, "invalid window anchor format {}", value)
} }
ParseError::InvalidFormat => write!(f, "invalid event format"), ParseError::Format(debug_text) => {
write!(f, "invalid event format {}", debug_text)
}
} }
} }
} }
@ -45,7 +48,7 @@ impl error::Error for ParseError {
} }
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct GridLineCell { pub struct GridLineCell {
pub text: String, pub text: String,
pub highlight_id: Option<u64>, pub highlight_id: Option<u64>,
@ -54,7 +57,7 @@ pub struct GridLineCell {
pub type StyledContent = Vec<(u64, String)>; pub type StyledContent = Vec<(u64, String)>;
#[derive(Debug)] #[derive(Clone, Debug)]
pub enum MessageKind { pub enum MessageKind {
Unknown, Unknown,
Confirm, Confirm,
@ -91,7 +94,7 @@ impl MessageKind {
} }
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub enum GuiOption { pub enum GuiOption {
ArabicShape(bool), ArabicShape(bool),
AmbiWidth(String), AmbiWidth(String),
@ -106,7 +109,7 @@ pub enum GuiOption {
Unknown(String, Value), Unknown(String, Value),
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub enum WindowAnchor { pub enum WindowAnchor {
NorthWest, NorthWest,
NorthEast, NorthEast,
@ -114,7 +117,7 @@ pub enum WindowAnchor {
SouthEast, SouthEast,
} }
#[derive(Debug, Clone)] #[derive(Clone, Debug)]
pub enum EditorMode { pub enum EditorMode {
// The set of modes reported will change in new versions of Nvim, for // The set of modes reported will change in new versions of Nvim, for
// instance more sub-modes and temporary states might be represented as // instance more sub-modes and temporary states might be represented as
@ -128,7 +131,7 @@ pub enum EditorMode {
Unknown(String), Unknown(String),
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub enum RedrawEvent { pub enum RedrawEvent {
SetTitle { SetTitle {
title: String, title: String,
@ -200,7 +203,7 @@ pub enum RedrawEvent {
anchor_row: f64, anchor_row: f64,
anchor_column: f64, anchor_column: f64,
focusable: bool, focusable: bool,
z_index: i64, sort_order: Option<u64>,
}, },
WindowExternalPosition { WindowExternalPosition {
grid: u64, grid: u64,
@ -356,7 +359,7 @@ fn extract_values<Arr: AsMut<[Value]>>(values: Vec<Value>, mut arr: Arr) -> Resu
let arr_ref = arr.as_mut(); let arr_ref = arr.as_mut();
if values.len() != arr_ref.len() { if values.len() != arr_ref.len() {
Err(ParseError::InvalidFormat) Err(ParseError::Format(format!("{:?}", values)))
} else { } else {
for (i, val) in values.into_iter().enumerate() { for (i, val) in values.into_iter().enumerate() {
arr_ref[i] = val; arr_ref[i] = val;
@ -367,31 +370,31 @@ fn extract_values<Arr: AsMut<[Value]>>(values: Vec<Value>, mut arr: Arr) -> Resu
} }
fn parse_array(array_value: Value) -> Result<Vec<Value>> { fn parse_array(array_value: Value) -> Result<Vec<Value>> {
array_value.try_into().map_err(ParseError::InvalidArray) array_value.try_into().map_err(ParseError::Array)
} }
fn parse_map(map_value: Value) -> Result<Vec<(Value, Value)>> { fn parse_map(map_value: Value) -> Result<Vec<(Value, Value)>> {
map_value.try_into().map_err(ParseError::InvalidMap) map_value.try_into().map_err(ParseError::Map)
} }
fn parse_string(string_value: Value) -> Result<String> { fn parse_string(string_value: Value) -> Result<String> {
string_value.try_into().map_err(ParseError::InvalidString) string_value.try_into().map_err(ParseError::String)
} }
fn parse_u64(u64_value: Value) -> Result<u64> { fn parse_u64(u64_value: Value) -> Result<u64> {
u64_value.try_into().map_err(ParseError::InvalidU64) u64_value.try_into().map_err(ParseError::U64)
} }
fn parse_i64(i64_value: Value) -> Result<i64> { fn parse_i64(i64_value: Value) -> Result<i64> {
i64_value.try_into().map_err(ParseError::InvalidI64) i64_value.try_into().map_err(ParseError::I64)
} }
fn parse_f64(f64_value: Value) -> Result<f64> { fn parse_f64(f64_value: Value) -> Result<f64> {
f64_value.try_into().map_err(ParseError::InvalidF64) f64_value.try_into().map_err(ParseError::F64)
} }
fn parse_bool(bool_value: Value) -> Result<bool> { fn parse_bool(bool_value: Value) -> Result<bool> {
bool_value.try_into().map_err(ParseError::InvalidBool) bool_value.try_into().map_err(ParseError::Bool)
} }
fn parse_set_title(set_title_arguments: Vec<Value>) -> Result<RedrawEvent> { fn parse_set_title(set_title_arguments: Vec<Value>) -> Result<RedrawEvent> {
@ -564,7 +567,7 @@ fn parse_grid_line_cell(grid_line_cell: Value) -> Result<GridLineCell> {
let text_value = cell_contents let text_value = cell_contents
.first_mut() .first_mut()
.map(|v| take_value(v)) .map(|v| take_value(v))
.ok_or(ParseError::InvalidFormat)?; .ok_or_else(|| ParseError::Format(format!("{:?}", cell_contents)))?;
let highlight_id = cell_contents let highlight_id = cell_contents
.get_mut(1) .get_mut(1)
@ -677,33 +680,59 @@ fn parse_window_anchor(value: Value) -> Result<WindowAnchor> {
"NE" => Ok(WindowAnchor::NorthEast), "NE" => Ok(WindowAnchor::NorthEast),
"SW" => Ok(WindowAnchor::SouthWest), "SW" => Ok(WindowAnchor::SouthWest),
"SE" => Ok(WindowAnchor::SouthEast), "SE" => Ok(WindowAnchor::SouthEast),
_ => Err(ParseError::InvalidWindowAnchor(value_str.into())), _ => Err(ParseError::WindowAnchor(value_str.into())),
} }
} }
fn parse_win_float_pos(win_float_pos_arguments: Vec<Value>) -> Result<RedrawEvent> { fn parse_win_float_pos(win_float_pos_arguments: Vec<Value>) -> Result<RedrawEvent> {
let values = [ if win_float_pos_arguments.len() == 8 {
Value::Nil, let values = [
Value::Nil, Value::Nil,
Value::Nil, Value::Nil,
Value::Nil, Value::Nil,
Value::Nil, Value::Nil,
Value::Nil, Value::Nil,
Value::Nil, Value::Nil,
Value::Nil, Value::Nil,
]; Value::Nil,
let [grid, _window, anchor, anchor_grid, anchor_row, anchor_column, focusable, z_index] = ];
extract_values(win_float_pos_arguments, values)?;
let [grid, _window, anchor, anchor_grid, anchor_row, anchor_column, focusable, sort_order] =
Ok(RedrawEvent::WindowFloatPosition { extract_values(win_float_pos_arguments, values)?;
grid: parse_u64(grid)?,
anchor: parse_window_anchor(anchor)?, Ok(RedrawEvent::WindowFloatPosition {
anchor_grid: parse_u64(anchor_grid)?, grid: parse_u64(grid)?,
anchor_row: parse_f64(anchor_row)?, anchor: parse_window_anchor(anchor)?,
anchor_column: parse_f64(anchor_column)?, anchor_grid: parse_u64(anchor_grid)?,
focusable: parse_bool(focusable)?, anchor_row: parse_f64(anchor_row)?,
z_index: parse_i64(z_index)?, anchor_column: parse_f64(anchor_column)?,
}) focusable: parse_bool(focusable)?,
sort_order: Some(parse_u64(sort_order)?),
})
} else {
let values = [
Value::Nil,
Value::Nil,
Value::Nil,
Value::Nil,
Value::Nil,
Value::Nil,
Value::Nil,
];
let [grid, _window, anchor, anchor_grid, anchor_row, anchor_column, focusable] =
extract_values(win_float_pos_arguments, values)?;
Ok(RedrawEvent::WindowFloatPosition {
grid: parse_u64(grid)?,
anchor: parse_window_anchor(anchor)?,
anchor_grid: parse_u64(anchor_grid)?,
anchor_row: parse_f64(anchor_row)?,
anchor_column: parse_f64(anchor_column)?,
focusable: parse_bool(focusable)?,
sort_order: None,
})
}
} }
fn parse_win_external_pos(win_external_pos_arguments: Vec<Value>) -> Result<RedrawEvent> { fn parse_win_external_pos(win_external_pos_arguments: Vec<Value>) -> Result<RedrawEvent> {
@ -896,7 +925,7 @@ pub fn parse_redraw_event(event_value: Value) -> Result<Vec<RedrawEvent>> {
let mut event_contents = parse_array(event_value)?.into_iter(); let mut event_contents = parse_array(event_value)?.into_iter();
let event_name = event_contents let event_name = event_contents
.next() .next()
.ok_or(ParseError::InvalidFormat) .ok_or_else(|| ParseError::Format(format!("{:?}", event_contents)))
.and_then(parse_string)?; .and_then(parse_string)?;
let events = event_contents; let events = event_contents;
@ -960,7 +989,7 @@ pub fn parse_channel_stream_type(channel_stream_value: Value) -> Result<ChannelS
"stderr" => Ok(ChannelStreamType::Stderr), "stderr" => Ok(ChannelStreamType::Stderr),
"socket" => Ok(ChannelStreamType::Socket), "socket" => Ok(ChannelStreamType::Socket),
"job" => Ok(ChannelStreamType::Job), "job" => Ok(ChannelStreamType::Job),
_ => Err(ParseError::InvalidFormat), stream_type => Err(ParseError::Format(format!("{:?}", stream_type))),
} }
} }
@ -969,7 +998,7 @@ pub fn parse_channel_mode(channel_mode_value: Value) -> Result<ChannelMode> {
"bytes" => Ok(ChannelMode::Bytes), "bytes" => Ok(ChannelMode::Bytes),
"terminal" => Ok(ChannelMode::Terminal), "terminal" => Ok(ChannelMode::Terminal),
"rpc" => Ok(ChannelMode::Rpc), "rpc" => Ok(ChannelMode::Rpc),
_ => Err(ParseError::InvalidFormat), channel_mode => Err(ParseError::Format(format!("{:?}", channel_mode))),
} }
} }
@ -1003,7 +1032,7 @@ pub fn parse_client_type(client_type_value: Value) -> Result<ClientType> {
"embedder" => Ok(ClientType::Embedder), "embedder" => Ok(ClientType::Embedder),
"host" => Ok(ClientType::Host), "host" => Ok(ClientType::Host),
"plugin" => Ok(ClientType::Plugin), "plugin" => Ok(ClientType::Plugin),
_ => Err(ParseError::InvalidFormat), client_type => Err(ParseError::Format(format!("{:?}", client_type))),
} }
} }

@ -1,7 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crossfire::mpsc::TxUnbounded;
use log::trace; use log::trace;
use nvim_rs::{Handler, Neovim}; use nvim_rs::{Handler, Neovim};
use parking_lot::Mutex; use parking_lot::Mutex;
@ -11,19 +10,20 @@ use tokio::task;
use super::events::{parse_redraw_event, RedrawEvent}; use super::events::{parse_redraw_event, RedrawEvent};
use super::ui_commands::UiCommand; use super::ui_commands::UiCommand;
use crate::bridge::TxWrapper; use crate::bridge::TxWrapper;
use crate::channel_utils::*;
use crate::error_handling::ResultPanicExplanation; use crate::error_handling::ResultPanicExplanation;
use crate::settings::SETTINGS; use crate::settings::SETTINGS;
#[derive(Clone)] #[derive(Clone)]
pub struct NeovimHandler { pub struct NeovimHandler {
ui_command_sender: Arc<Mutex<TxUnbounded<UiCommand>>>, ui_command_sender: Arc<Mutex<LoggingTx<UiCommand>>>,
redraw_event_sender: Arc<Mutex<TxUnbounded<RedrawEvent>>>, redraw_event_sender: Arc<Mutex<LoggingTx<RedrawEvent>>>,
} }
impl NeovimHandler { impl NeovimHandler {
pub fn new( pub fn new(
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: LoggingTx<UiCommand>,
redraw_event_sender: TxUnbounded<RedrawEvent>, redraw_event_sender: LoggingTx<RedrawEvent>,
) -> NeovimHandler { ) -> NeovimHandler {
NeovimHandler { NeovimHandler {
ui_command_sender: Arc::new(Mutex::new(ui_command_sender)), ui_command_sender: Arc::new(Mutex::new(ui_command_sender)),

@ -4,24 +4,25 @@ mod handler;
mod tx_wrapper; mod tx_wrapper;
mod ui_commands; mod ui_commands;
use std::env;
use std::path::Path; use std::path::Path;
use std::process::Stdio; use std::process::Stdio;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use crossfire::mpsc::{RxUnbounded, TxUnbounded}; use crossfire::mpsc::RxUnbounded;
use log::{error, info, warn}; use log::{error, info, warn};
use nvim_rs::UiAttachOptions; use nvim_rs::UiAttachOptions;
use rmpv::Value; use rmpv::Value;
use tokio::process::Command; use tokio::process::Command;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::error_handling::ResultPanicExplanation; use crate::channel_utils::*;
use crate::settings::*; use crate::settings::*;
use crate::window::window_geometry_or_default; use crate::window::window_geometry_or_default;
use crate::{cmd_line::CmdLineSettings, error_handling::ResultPanicExplanation};
pub use events::*; pub use events::*;
use handler::NeovimHandler; use handler::NeovimHandler;
use regex::Regex;
pub use tx_wrapper::{TxWrapper, WrapTx}; pub use tx_wrapper::{TxWrapper, WrapTx};
pub use ui_commands::UiCommand; pub use ui_commands::UiCommand;
@ -32,7 +33,7 @@ fn set_windows_creation_flags(cmd: &mut Command) {
#[cfg(windows)] #[cfg(windows)]
fn platform_build_nvim_cmd(bin: &str) -> Option<Command> { fn platform_build_nvim_cmd(bin: &str) -> Option<Command> {
if env::args().any(|arg| arg == "--wsl") { if SETTINGS.get::<CmdLineSettings>().wsl {
let mut cmd = Command::new("wsl"); let mut cmd = Command::new("wsl");
cmd.args(&[ cmd.args(&[
bin.trim(), bin.trim(),
@ -57,7 +58,7 @@ fn platform_build_nvim_cmd(bin: &str) -> Option<Command> {
} }
fn build_nvim_cmd() -> Command { fn build_nvim_cmd() -> Command {
if let Ok(path) = env::var("NEOVIM_BIN") { if let Some(path) = SETTINGS.get::<CmdLineSettings>().neovim_bin {
if let Some(cmd) = platform_build_nvim_cmd(&path) { if let Some(cmd) = platform_build_nvim_cmd(&path) {
return cmd; return cmd;
} else { } else {
@ -65,7 +66,7 @@ fn build_nvim_cmd() -> Command {
} }
} }
#[cfg(windows)] #[cfg(windows)]
if env::args().any(|arg| arg == "--wsl") { if SETTINGS.get::<CmdLineSettings>().wsl {
if let Ok(output) = std::process::Command::new("wsl") if let Ok(output) = std::process::Command::new("wsl")
.args(&["bash", "-ic", "which nvim"]) .args(&["bash", "-ic", "which nvim"])
.output() .output()
@ -125,7 +126,10 @@ pub fn create_nvim_command() -> Command {
let mut cmd = build_nvim_cmd(); let mut cmd = build_nvim_cmd();
cmd.arg("--embed") cmd.arg("--embed")
.args(SETTINGS.neovim_arguments.iter().skip(1)); .args(SETTINGS.get::<CmdLineSettings>().neovim_args.iter())
.args(SETTINGS.get::<CmdLineSettings>().files_to_open.iter());
info!("Starting neovim with: {:?}", cmd);
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
cmd.stderr(Stdio::piped()); cmd.stderr(Stdio::piped());
@ -145,20 +149,17 @@ enum ConnectionMode {
} }
fn connection_mode() -> ConnectionMode { fn connection_mode() -> ConnectionMode {
let tcp_prefix = "--remote-tcp="; if let Some(arg) = SETTINGS.get::<CmdLineSettings>().remote_tcp {
ConnectionMode::RemoteTcp(arg)
if let Some(arg) = std::env::args().find(|arg| arg.starts_with(tcp_prefix)) {
let input = &arg[tcp_prefix.len()..];
ConnectionMode::RemoteTcp(input.to_owned())
} else { } else {
ConnectionMode::Child ConnectionMode::Child
} }
} }
async fn start_neovim_runtime( async fn start_neovim_runtime(
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: LoggingTx<UiCommand>,
ui_command_receiver: RxUnbounded<UiCommand>, ui_command_receiver: RxUnbounded<UiCommand>,
redraw_event_sender: TxUnbounded<RedrawEvent>, redraw_event_sender: LoggingTx<RedrawEvent>,
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
) { ) {
let (width, height) = window_geometry_or_default(); let (width, height) = window_geometry_or_default();
@ -189,13 +190,14 @@ async fn start_neovim_runtime(
close_watcher_running.store(false, Ordering::Relaxed); close_watcher_running.store(false, Ordering::Relaxed);
}); });
if let Ok(Value::Integer(correct_version)) = nvim.eval("has(\"nvim-0.4\")").await { if let Ok(output) = nvim.command_output("version").await {
if correct_version.as_i64() != Some(1) { let re = Regex::new(r"NVIM v0.[4-9]\d*.\d+").unwrap();
error!("Neovide requires version 0.4 or higher"); if !re.is_match(&output) {
error!("Neovide requires nvim version 0.4 or higher. Download the latest version here https://github.com/neovim/neovim/wiki/Installing-Neovim");
std::process::exit(0); std::process::exit(0);
} }
} else { } else {
error!("Neovide requires version 0.4 or higher"); error!("Neovide requires nvim version 0.4 or higher. Download the latest version here https://github.com/neovim/neovim/wiki/Installing-Neovim");
std::process::exit(0); std::process::exit(0);
}; };
@ -276,7 +278,8 @@ async fn start_neovim_runtime(
let mut options = UiAttachOptions::new(); let mut options = UiAttachOptions::new();
options.set_linegrid_external(true); options.set_linegrid_external(true);
if env::args().any(|arg| arg == "--multiGrid") || env::var("NeovideMultiGrid").is_ok() {
if SETTINGS.get::<CmdLineSettings>().multi_grid {
options.set_multigrid_external(true); options.set_multigrid_external(true);
} }
options.set_rgb(true); options.set_rgb(true);
@ -320,9 +323,9 @@ pub struct Bridge {
} }
pub fn start_bridge( pub fn start_bridge(
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: LoggingTx<UiCommand>,
ui_command_receiver: RxUnbounded<UiCommand>, ui_command_receiver: RxUnbounded<UiCommand>,
redraw_event_sender: TxUnbounded<RedrawEvent>, redraw_event_sender: LoggingTx<RedrawEvent>,
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
) -> Bridge { ) -> Bridge {
let runtime = Runtime::new().unwrap(); let runtime = Runtime::new().unwrap();

@ -14,6 +14,7 @@ use crate::windows_utils::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum UiCommand { pub enum UiCommand {
Quit,
Resize { Resize {
width: u32, width: u32,
height: u32, height: u32,
@ -45,6 +46,9 @@ pub enum UiCommand {
impl UiCommand { impl UiCommand {
pub async fn execute(self, nvim: &Neovim<TxWrapper>) { pub async fn execute(self, nvim: &Neovim<TxWrapper>) {
match self { match self {
UiCommand::Quit => {
nvim.command("qa!").await.ok();
}
UiCommand::Resize { width, height } => nvim UiCommand::Resize { width, height } => nvim
.ui_try_resize(width.max(10) as i64, height.max(3) as i64) .ui_try_resize(width.max(10) as i64, height.max(3) as i64)
.await .await

@ -0,0 +1,54 @@
use std::fmt::Debug;
use std::sync::mpsc::{SendError, Sender};
use crossfire::mpsc::{SendError as TxError, TxUnbounded};
use log::trace;
#[derive(Clone)]
pub struct LoggingSender<T>
where
T: Debug,
{
sender: Sender<T>,
channel_name: String,
}
impl<T> LoggingSender<T>
where
T: Debug,
{
pub fn attach(sender: Sender<T>, channel_name: String) -> Self {
Self {
sender,
channel_name,
}
}
pub fn send(&self, message: T) -> Result<(), SendError<T>> {
trace!("{} {:?}", self.channel_name, &message);
self.sender.send(message)
}
}
#[derive(Clone)]
pub struct LoggingTx<T>
where
T: Debug,
{
tx: TxUnbounded<T>,
channel_name: String,
}
impl<T> LoggingTx<T>
where
T: Debug,
{
pub fn attach(tx: TxUnbounded<T>, channel_name: String) -> Self {
Self { tx, channel_name }
}
pub fn send(&self, message: T) -> Result<(), TxError<T>> {
trace!("{} {:?}", self.channel_name, &message);
self.tx.send(message)
}
}

@ -0,0 +1,136 @@
use crate::settings::*;
use clap::{App, Arg};
#[derive(Clone, Debug)]
pub struct CmdLineSettings {
pub verbosity: u64,
pub log_to_file: bool,
pub neovim_args: Vec<String>,
pub neovim_bin: Option<String>,
pub files_to_open: Vec<String>,
pub disowned: bool,
pub geometry: Option<String>,
pub wsl: bool,
pub remote_tcp: Option<String>,
pub multi_grid: bool,
pub maximized: bool,
pub frameless: bool,
}
impl Default for CmdLineSettings {
fn default() -> Self {
Self {
neovim_bin: None,
verbosity: 0,
log_to_file: false,
neovim_args: vec![],
files_to_open: vec![],
disowned: false,
geometry: None,
wsl: false,
remote_tcp: None,
multi_grid: false,
maximized: false,
frameless: false,
}
}
}
pub fn handle_command_line_arguments() {
let clapp = App::new("Neovide")
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.arg(
Arg::with_name("verbosity")
.short("v")
.multiple(true)
.help("Set the level of verbosity"),
)
.arg(
Arg::with_name("log_to_file")
.long("log")
.help("Log to a file"),
)
.arg(
Arg::with_name("disowned")
.long("disowned")
.help("Disown the process. (only on macos)"),
)
.arg(
Arg::with_name("maximized")
.long("maximized")
.help("Maximize the window"),
)
.arg(
Arg::with_name("multi_grid")
//.long("multi-grid") TODO: multiGrid is the current way to call this, but I
//personally would prefer sticking to a unix-y way of naming things...
.long("multiGrid")
.help("Enable Multigrid"),
)
.arg(
Arg::with_name("frameless")
.long("frameless")
.help("Removes the window frame. NOTE: Window might not be resizable after this setting is enabled.")
)
.arg(Arg::with_name("wsl").long("wsl").help("Run in WSL"))
.arg(
Arg::with_name("remote_tcp")
.long("remote-tcp")
.takes_value(true)
.help("Connect to Remote TCP"),
)
.arg(
Arg::with_name("geometry")
.long("geometry")
.takes_value(true)
.help("Specify the Geometry of the window"),
)
.arg(
Arg::with_name("files")
.multiple(true)
.takes_value(true)
.help("Specify the Geometry of the window"),
)
.arg(
Arg::with_name("neovim_args")
.multiple(true)
.takes_value(true)
.last(true)
.help("Specify Arguments to pass down to neovim"),
);
let matches = clapp.get_matches();
/*
* Integrate Environment Variables as Defaults to the command-line ones.
*
* NEOVIM_BIN
* NeovideMultiGrid || --multiGrid
*/
SETTINGS.set::<CmdLineSettings>(&CmdLineSettings {
neovim_bin: std::env::var("NEOVIM_BIN").ok(),
neovim_args: matches
.values_of("neovim_args")
.map(|opt| opt.map(|v| v.to_owned()).collect())
.unwrap_or_default(),
verbosity: matches.occurrences_of("verbosity"),
log_to_file: matches.is_present("log_to_file"),
files_to_open: matches
.values_of("files")
.map(|opt| opt.map(|v| v.to_owned()).collect())
.unwrap_or_default(),
maximized: matches.is_present("maximized") || std::env::var("NEOVIDE_MAXIMIZED").is_ok(),
multi_grid: std::env::var("NEOVIDE_MULTIGRID").is_ok()
|| std::env::var("NeovideMultiGrid").is_ok()
|| matches.is_present("multi_grid"),
remote_tcp: matches.value_of("remote_tcp").map(|i| i.to_owned()),
disowned: matches.is_present("disowned"),
wsl: matches.is_present("wsl"),
geometry: matches.value_of("geometry").map(|i| i.to_owned()),
frameless: matches.is_present("frameless") || std::env::var("NEOVIDE_FRAMELESS").is_ok(),
});
}

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use skulpin::skia_safe::Color4f; use skia_safe::Color4f;
use super::style::{Colors, Style}; use super::style::{Colors, Style};
@ -70,10 +70,9 @@ impl Cursor {
style style
.colors .colors
.foreground .foreground
.clone() .unwrap_or_else(|| default_colors.background.unwrap())
.unwrap_or_else(|| default_colors.background.clone().unwrap())
} else { } else {
default_colors.background.clone().unwrap() default_colors.background.unwrap()
} }
} }
@ -82,10 +81,9 @@ impl Cursor {
style style
.colors .colors
.background .background
.clone() .unwrap_or_else(|| default_colors.foreground.unwrap())
.unwrap_or_else(|| default_colors.foreground.clone().unwrap())
} else { } else {
default_colors.foreground.clone().unwrap() default_colors.foreground.unwrap()
} }
} }
@ -159,18 +157,18 @@ mod tests {
assert_eq!( assert_eq!(
cursor.foreground(&DEFAULT_COLORS), cursor.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap() DEFAULT_COLORS.background.unwrap()
); );
cursor.style = style.clone(); cursor.style = style.clone();
assert_eq!( assert_eq!(
cursor.foreground(&DEFAULT_COLORS), cursor.foreground(&DEFAULT_COLORS),
COLORS.foreground.clone().unwrap() COLORS.foreground.unwrap()
); );
cursor.style = Some(Arc::new(Style::new(NONE_COLORS))); cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
assert_eq!( assert_eq!(
cursor.foreground(&DEFAULT_COLORS), cursor.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap() DEFAULT_COLORS.background.unwrap()
); );
} }
@ -181,18 +179,18 @@ mod tests {
assert_eq!( assert_eq!(
cursor.background(&DEFAULT_COLORS), cursor.background(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap() DEFAULT_COLORS.foreground.unwrap()
); );
cursor.style = style.clone(); cursor.style = style.clone();
assert_eq!( assert_eq!(
cursor.background(&DEFAULT_COLORS), cursor.background(&DEFAULT_COLORS),
COLORS.background.clone().unwrap() COLORS.background.unwrap()
); );
cursor.style = Some(Arc::new(Style::new(NONE_COLORS))); cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
assert_eq!( assert_eq!(
cursor.background(&DEFAULT_COLORS), cursor.background(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap() DEFAULT_COLORS.foreground.unwrap()
); );
} }

@ -1,16 +1,17 @@
use std::sync::mpsc::{channel, Receiver, SendError, Sender}; use std::sync::mpsc::{channel, Receiver, SendError, Sender};
use super::DrawCommand; use super::DrawCommand;
use crate::channel_utils::*;
pub struct DrawCommandBatcher { pub struct DrawCommandBatcher {
window_draw_command_sender: Sender<DrawCommand>, window_draw_command_sender: Sender<DrawCommand>,
window_draw_command_receiver: Receiver<DrawCommand>, window_draw_command_receiver: Receiver<DrawCommand>,
batched_draw_command_sender: Sender<Vec<DrawCommand>>, batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>,
} }
impl DrawCommandBatcher { impl DrawCommandBatcher {
pub fn new(batched_draw_command_sender: Sender<Vec<DrawCommand>>) -> DrawCommandBatcher { pub fn new(batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>) -> DrawCommandBatcher {
let (sender, receiver) = channel(); let (sender, receiver) = channel();
DrawCommandBatcher { DrawCommandBatcher {

@ -2,7 +2,14 @@ use std::sync::Arc;
use super::style::Style; use super::style::Style;
pub type GridCell = Option<(String, Option<Arc<Style>>)>; pub type GridCell = (String, Option<Arc<Style>>);
#[macro_export]
macro_rules! default_cell {
() => {
(" ".to_owned(), None)
};
}
pub struct CharacterGrid { pub struct CharacterGrid {
pub width: u64, pub width: u64,
@ -16,7 +23,7 @@ impl CharacterGrid {
let (width, height) = size; let (width, height) = size;
let cell_count = (width * height) as usize; let cell_count = (width * height) as usize;
CharacterGrid { CharacterGrid {
characters: vec![None; cell_count], characters: vec![default_cell!(); cell_count],
width, width,
height, height,
} }
@ -24,8 +31,7 @@ impl CharacterGrid {
pub fn resize(&mut self, width: u64, height: u64) { pub fn resize(&mut self, width: u64, height: u64) {
let new_cell_count = (width * height) as usize; let new_cell_count = (width * height) as usize;
let default_cell: GridCell = None; let mut new_characters = vec![default_cell!(); new_cell_count];
let mut new_characters = vec![default_cell; new_cell_count];
for x in 0..self.width.min(width) { for x in 0..self.width.min(width) {
for y in 0..self.height.min(height) { for y in 0..self.height.min(height) {
@ -41,7 +47,7 @@ impl CharacterGrid {
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.set_characters_all(None); self.set_all_characters(default_cell!());
} }
fn cell_index(&self, x: u64, y: u64) -> Option<usize> { fn cell_index(&self, x: u64, y: u64) -> Option<usize> {
@ -61,12 +67,10 @@ impl CharacterGrid {
.map(move |idx| &mut self.characters[idx]) .map(move |idx| &mut self.characters[idx])
} }
pub fn set_characters_all(&mut self, value: GridCell) { pub fn set_all_characters(&mut self, value: GridCell) {
self.characters.clear(); self.characters.clear();
self.characters self.characters
.resize_with((self.width * self.height) as usize, || { .resize_with((self.width * self.height) as usize, || value.clone());
value.as_ref().cloned()
});
} }
pub fn row(&self, row_index: u64) -> Option<&[GridCell]> { pub fn row(&self, row_index: u64) -> Option<&[GridCell]> {
@ -123,25 +127,28 @@ mod tests {
} }
#[test] #[test]
fn test_new() { fn new_constructsGrid() {
let context = Context::new(); let context = Context::new();
// RUN FUNCTION // RUN FUNCTION
let character_grid = CharacterGrid::new(context.size); let character_grid = CharacterGrid::new(context.size);
assert_eq!(character_grid.width, context.size.0); assert_eq!(character_grid.width, context.size.0);
assert_eq!(character_grid.height, context.size.1); assert_eq!(character_grid.height, context.size.1);
assert_eq!(character_grid.characters, vec![None; context.area]); assert_eq!(
character_grid.characters,
vec![default_cell!(); context.area]
);
} }
#[test] #[test]
fn test_get_cell() { fn getCell_returnsExpectedCell() {
let context = Context::new(); let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size); let mut character_grid = CharacterGrid::new(context.size);
character_grid.characters[context.index] = Some(( character_grid.characters[context.index] = (
"foo".to_string(), "foo".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))), Some(Arc::new(Style::new(context.none_colors.clone()))),
)); );
let result = ( let result = (
"foo".to_string(), "foo".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))), Some(Arc::new(Style::new(context.none_colors.clone()))),
@ -149,24 +156,20 @@ mod tests {
// RUN FUNCTION // RUN FUNCTION
assert_eq!( assert_eq!(
character_grid character_grid.get_cell(context.x, context.y).unwrap(),
.get_cell(context.x, context.y)
.unwrap()
.as_ref()
.unwrap(),
&result &result
); );
} }
#[test] #[test]
fn test_get_cell_mut() { fn getCellMut_modifiersGridProperly() {
let context = Context::new(); let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size); let mut character_grid = CharacterGrid::new(context.size);
character_grid.characters[context.index] = Some(( character_grid.characters[context.index] = (
"foo".to_string(), "foo".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))), Some(Arc::new(Style::new(context.none_colors.clone()))),
)); );
let result = ( let result = (
"bar".to_string(), "bar".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))), Some(Arc::new(Style::new(context.none_colors.clone()))),
@ -174,32 +177,28 @@ mod tests {
// RUN FUNCTION // RUN FUNCTION
let cell = character_grid.get_cell_mut(context.x, context.y).unwrap(); let cell = character_grid.get_cell_mut(context.x, context.y).unwrap();
*cell = Some(( *cell = (
"bar".to_string(), "bar".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))), Some(Arc::new(Style::new(context.none_colors.clone()))),
)); );
assert_eq!( assert_eq!(
character_grid character_grid.get_cell_mut(context.x, context.y).unwrap(),
.get_cell_mut(context.x, context.y)
.unwrap()
.as_ref()
.unwrap(),
&result &result
); );
} }
#[test] #[test]
fn test_set_characters_all() { fn setAllCharacters_setsAllCellsToGivenCharacter() {
let context = Context::new(); let context = Context::new();
let grid_cell = Some(( let grid_cell = (
"foo".to_string(), "foo".to_string(),
Some(Arc::new(Style::new(context.none_colors))), Some(Arc::new(Style::new(context.none_colors))),
)); );
let mut character_grid = CharacterGrid::new(context.size); let mut character_grid = CharacterGrid::new(context.size);
// RUN FUNCTION // RUN FUNCTION
character_grid.set_characters_all(grid_cell.clone()); character_grid.set_all_characters(grid_cell.clone());
assert_eq!( assert_eq!(
character_grid.characters, character_grid.characters,
vec![grid_cell.clone(); context.area] vec![grid_cell.clone(); context.area]
@ -207,14 +206,14 @@ mod tests {
} }
#[test] #[test]
fn test_clear() { fn clear_emptiesBuffer() {
let context = Context::new(); let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size); let mut character_grid = CharacterGrid::new(context.size);
let grid_cell = Some(( let grid_cell = (
"foo".to_string(), "foo".to_string(),
Some(Arc::new(Style::new(context.none_colors))), Some(Arc::new(Style::new(context.none_colors))),
)); );
character_grid.characters = vec![grid_cell.clone(); context.area]; character_grid.characters = vec![grid_cell.clone(); context.area];
// RUN FUNCTION // RUN FUNCTION
@ -222,11 +221,14 @@ mod tests {
assert_eq!(character_grid.width, context.size.0); assert_eq!(character_grid.width, context.size.0);
assert_eq!(character_grid.height, context.size.1); assert_eq!(character_grid.height, context.size.1);
assert_eq!(character_grid.characters, vec![None; context.area]); assert_eq!(
character_grid.characters,
vec![default_cell!(); context.area]
);
} }
#[test] #[test]
fn test_resize() { fn resize_clearsAndResizesGrid() {
let context = Context::new(); let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size); let mut character_grid = CharacterGrid::new(context.size);
let (width, height) = ( let (width, height) = (
@ -234,10 +236,10 @@ mod tests {
(thread_rng().gen::<u64>() % 500) + 1, (thread_rng().gen::<u64>() % 500) + 1,
); );
let grid_cell = Some(( let grid_cell = (
"foo".to_string(), "foo".to_string(),
Some(Arc::new(Style::new(context.none_colors))), Some(Arc::new(Style::new(context.none_colors))),
)); );
character_grid.characters = vec![grid_cell.clone(); context.area]; character_grid.characters = vec![grid_cell.clone(); context.area];
// RUN FUNCTION // RUN FUNCTION
@ -255,7 +257,7 @@ mod tests {
for x in original_width..width { for x in original_width..width {
for y in original_height..height { for y in original_height..height {
assert_eq!(character_grid.get_cell(x, y).unwrap(), &None); assert_eq!(character_grid.get_cell(x, y).unwrap(), &default_cell!());
} }
} }
} }

@ -6,14 +6,14 @@ mod window;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::sync::mpsc::Sender;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use crossfire::mpsc::RxUnbounded; use crossfire::mpsc::RxUnbounded;
use log::{error, trace, warn}; use log::{error, trace};
use crate::bridge::{EditorMode, GuiOption, RedrawEvent, WindowAnchor}; use crate::bridge::{EditorMode, GuiOption, RedrawEvent, WindowAnchor};
use crate::channel_utils::*;
use crate::redraw_scheduler::REDRAW_SCHEDULER; use crate::redraw_scheduler::REDRAW_SCHEDULER;
pub use cursor::{Cursor, CursorMode, CursorShape}; pub use cursor::{Cursor, CursorMode, CursorShape};
pub use draw_command_batcher::DrawCommandBatcher; pub use draw_command_batcher::DrawCommandBatcher;
@ -21,11 +21,13 @@ pub use grid::CharacterGrid;
pub use style::{Colors, Style}; pub use style::{Colors, Style};
pub use window::*; pub use window::*;
#[derive(Clone)]
pub struct AnchorInfo { pub struct AnchorInfo {
pub anchor_grid_id: u64, pub anchor_grid_id: u64,
pub anchor_type: WindowAnchor, pub anchor_type: WindowAnchor,
pub anchor_left: f64, pub anchor_left: f64,
pub anchor_top: f64, pub anchor_top: f64,
pub sort_order: u64,
} }
impl WindowAnchor { impl WindowAnchor {
@ -57,6 +59,7 @@ pub enum DrawCommand {
ModeChanged(EditorMode), ModeChanged(EditorMode),
} }
#[derive(Debug)]
pub enum WindowCommand { pub enum WindowCommand {
TitleChanged(String), TitleChanged(String),
SetMouseEnabled(bool), SetMouseEnabled(bool),
@ -83,13 +86,13 @@ pub struct Editor {
pub defined_styles: HashMap<u64, Arc<Style>>, pub defined_styles: HashMap<u64, Arc<Style>>,
pub mode_list: Vec<CursorMode>, pub mode_list: Vec<CursorMode>,
pub draw_command_batcher: Arc<DrawCommandBatcher>, pub draw_command_batcher: Arc<DrawCommandBatcher>,
pub window_command_sender: Sender<WindowCommand>, pub window_command_sender: LoggingSender<WindowCommand>,
} }
impl Editor { impl Editor {
pub fn new( pub fn new(
batched_draw_command_sender: Sender<Vec<DrawCommand>>, batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>,
window_command_sender: Sender<WindowCommand>, window_command_sender: LoggingSender<WindowCommand>,
) -> Editor { ) -> Editor {
Editor { Editor {
windows: HashMap::new(), windows: HashMap::new(),
@ -208,8 +211,16 @@ impl Editor {
anchor_grid, anchor_grid,
anchor_column: anchor_left, anchor_column: anchor_left,
anchor_row: anchor_top, anchor_row: anchor_top,
sort_order,
.. ..
} => self.set_window_float_position(grid, anchor_grid, anchor, anchor_left, anchor_top), } => self.set_window_float_position(
grid,
anchor_grid,
anchor,
anchor_left,
anchor_top,
sort_order,
),
RedrawEvent::WindowHide { grid } => { RedrawEvent::WindowHide { grid } => {
let window = self.windows.get(&grid); let window = self.windows.get(&grid);
if let Some(window) = window { if let Some(window) = window {
@ -240,7 +251,7 @@ impl Editor {
} }
fn resize_window(&mut self, grid: u64, width: u64, height: u64) { fn resize_window(&mut self, grid: u64, width: u64, height: u64) {
warn!("editor resize {}", grid); trace!("editor resize {}", grid);
if let Some(window) = self.windows.get_mut(&grid) { if let Some(window) = self.windows.get_mut(&grid) {
window.resize(width, height); window.resize(width, height);
} else { } else {
@ -265,7 +276,6 @@ impl Editor {
width: u64, width: u64,
height: u64, height: u64,
) { ) {
warn!("position {}", grid);
if let Some(window) = self.windows.get_mut(&grid) { if let Some(window) = self.windows.get_mut(&grid) {
window.position(width, height, None, start_left as f64, start_top as f64); window.position(width, height, None, start_left as f64, start_top as f64);
window.show(); window.show();
@ -290,8 +300,8 @@ impl Editor {
anchor_type: WindowAnchor, anchor_type: WindowAnchor,
anchor_left: f64, anchor_left: f64,
anchor_top: f64, anchor_top: f64,
sort_order: Option<u64>,
) { ) {
warn!("floating position {}", grid);
let parent_position = self.get_window_top_left(anchor_grid); let parent_position = self.get_window_top_left(anchor_grid);
if let Some(window) = self.windows.get_mut(&grid) { if let Some(window) = self.windows.get_mut(&grid) {
let width = window.get_width(); let width = window.get_width();
@ -312,6 +322,7 @@ impl Editor {
anchor_type, anchor_type,
anchor_left, anchor_left,
anchor_top, anchor_top,
sort_order: sort_order.unwrap_or(grid),
}), }),
modified_left, modified_left,
modified_top, modified_top,
@ -323,7 +334,6 @@ impl Editor {
} }
fn set_message_position(&mut self, grid: u64, grid_top: u64) { fn set_message_position(&mut self, grid: u64, grid_top: u64) {
warn!("message position {}", grid);
let parent_width = self let parent_width = self
.windows .windows
.get(&1) .get(&1)
@ -335,6 +345,7 @@ impl Editor {
anchor_type: WindowAnchor::NorthWest, anchor_type: WindowAnchor::NorthWest,
anchor_left: 0.0, anchor_left: 0.0,
anchor_top: grid_top as f64, anchor_top: grid_top as f64,
sort_order: std::u64::MAX,
}; };
if let Some(window) = self.windows.get_mut(&grid) { if let Some(window) = self.windows.get_mut(&grid) {
@ -422,15 +433,15 @@ impl Editor {
if let Some(window) = self.windows.get_mut(&grid) { if let Some(window) = self.windows.get_mut(&grid) {
window.update_viewport(top_line, bottom_line); window.update_viewport(top_line, bottom_line);
} else { } else {
warn!("viewport event received before window initialized"); trace!("viewport event received before window initialized");
} }
} }
} }
pub fn start_editor( pub fn start_editor(
redraw_event_receiver: RxUnbounded<RedrawEvent>, redraw_event_receiver: RxUnbounded<RedrawEvent>,
batched_draw_command_sender: Sender<Vec<DrawCommand>>, batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>,
window_command_sender: Sender<WindowCommand>, window_command_sender: LoggingSender<WindowCommand>,
) { ) {
thread::spawn(move || { thread::spawn(move || {
let mut editor = Editor::new(batched_draw_command_sender, window_command_sender); let mut editor = Editor::new(batched_draw_command_sender, window_command_sender);

@ -1,4 +1,4 @@
use skulpin::skia_safe::Color4f; use skia_safe::Color4f;
#[derive(new, PartialEq, Debug, Clone)] #[derive(new, PartialEq, Debug, Clone)]
pub struct Colors { pub struct Colors {
@ -31,13 +31,11 @@ impl Style {
if self.reverse { if self.reverse {
self.colors self.colors
.background .background
.clone() .unwrap_or_else(|| default_colors.background.unwrap())
.unwrap_or_else(|| default_colors.background.clone().unwrap())
} else { } else {
self.colors self.colors
.foreground .foreground
.clone() .unwrap_or_else(|| default_colors.foreground.unwrap())
.unwrap_or_else(|| default_colors.foreground.clone().unwrap())
} }
} }
@ -45,21 +43,18 @@ impl Style {
if self.reverse { if self.reverse {
self.colors self.colors
.foreground .foreground
.clone() .unwrap_or_else(|| default_colors.foreground.unwrap())
.unwrap_or_else(|| default_colors.foreground.clone().unwrap())
} else { } else {
self.colors self.colors
.background .background
.clone() .unwrap_or_else(|| default_colors.background.unwrap())
.unwrap_or_else(|| default_colors.background.clone().unwrap())
} }
} }
pub fn special(&self, default_colors: &Colors) -> Color4f { pub fn special(&self, default_colors: &Colors) -> Color4f {
self.colors self.colors
.special .special
.clone() .unwrap_or_else(|| default_colors.special.unwrap())
.unwrap_or_else(|| default_colors.special.clone().unwrap())
} }
} }
@ -85,12 +80,12 @@ mod tests {
assert_eq!( assert_eq!(
style.foreground(&DEFAULT_COLORS), style.foreground(&DEFAULT_COLORS),
COLORS.foreground.clone().unwrap() COLORS.foreground.unwrap()
); );
style.colors.foreground = None; style.colors.foreground = None;
assert_eq!( assert_eq!(
style.foreground(&DEFAULT_COLORS), style.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap() DEFAULT_COLORS.foreground.unwrap()
); );
} }
@ -101,12 +96,12 @@ mod tests {
assert_eq!( assert_eq!(
style.foreground(&DEFAULT_COLORS), style.foreground(&DEFAULT_COLORS),
COLORS.background.clone().unwrap() COLORS.background.unwrap()
); );
style.colors.background = None; style.colors.background = None;
assert_eq!( assert_eq!(
style.foreground(&DEFAULT_COLORS), style.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap() DEFAULT_COLORS.background.unwrap()
); );
} }
@ -116,12 +111,12 @@ mod tests {
assert_eq!( assert_eq!(
style.background(&DEFAULT_COLORS), style.background(&DEFAULT_COLORS),
COLORS.background.clone().unwrap() COLORS.background.unwrap()
); );
style.colors.background = None; style.colors.background = None;
assert_eq!( assert_eq!(
style.background(&DEFAULT_COLORS), style.background(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap() DEFAULT_COLORS.background.unwrap()
); );
} }
@ -132,12 +127,12 @@ mod tests {
assert_eq!( assert_eq!(
style.background(&DEFAULT_COLORS), style.background(&DEFAULT_COLORS),
COLORS.foreground.clone().unwrap() COLORS.foreground.unwrap()
); );
style.colors.foreground = None; style.colors.foreground = None;
assert_eq!( assert_eq!(
style.background(&DEFAULT_COLORS), style.background(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap() DEFAULT_COLORS.foreground.unwrap()
); );
} }
@ -145,14 +140,11 @@ mod tests {
fn test_special() { fn test_special() {
let mut style = Style::new(COLORS); let mut style = Style::new(COLORS);
assert_eq!( assert_eq!(style.special(&DEFAULT_COLORS), COLORS.special.unwrap());
style.special(&DEFAULT_COLORS),
COLORS.special.clone().unwrap()
);
style.colors.special = None; style.colors.special = None;
assert_eq!( assert_eq!(
style.special(&DEFAULT_COLORS), style.special(&DEFAULT_COLORS),
DEFAULT_COLORS.special.clone().unwrap() DEFAULT_COLORS.special.unwrap()
); );
} }
} }

@ -1,5 +1,4 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use log::warn; use log::warn;
@ -10,20 +9,20 @@ use super::style::Style;
use super::{AnchorInfo, DrawCommand, DrawCommandBatcher}; use super::{AnchorInfo, DrawCommand, DrawCommandBatcher};
use crate::bridge::GridLineCell; use crate::bridge::GridLineCell;
#[derive(new, Clone)] #[derive(new, Clone, Debug)]
pub enum WindowDrawCommand { pub enum WindowDrawCommand {
Position { Position {
grid_left: f64, grid_left: f64,
grid_top: f64, grid_top: f64,
width: u64, width: u64,
height: u64, height: u64,
floating: bool, floating_order: Option<u64>,
}, },
Cell { Cells {
text: String, cells: Vec<String>,
cell_width: u64,
window_left: u64, window_left: u64,
window_top: u64, window_top: u64,
width: u64,
style: Option<Arc<Style>>, style: Option<Arc<Style>>,
}, },
Scroll { Scroll {
@ -44,36 +43,6 @@ pub enum WindowDrawCommand {
}, },
} }
impl fmt::Debug for WindowDrawCommand {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WindowDrawCommand::Position {
grid_left,
grid_top,
..
} => write!(
formatter,
"Position {{ left: {}, right: {} }}",
grid_left, grid_top
),
WindowDrawCommand::Cell { .. } => write!(formatter, "Cell"),
WindowDrawCommand::Scroll { .. } => write!(formatter, "Scroll"),
WindowDrawCommand::Clear => write!(formatter, "Clear"),
WindowDrawCommand::Show => write!(formatter, "Show"),
WindowDrawCommand::Hide => write!(formatter, "Hide"),
WindowDrawCommand::Close => write!(formatter, "Close"),
WindowDrawCommand::Viewport {
top_line,
bottom_line,
} => write!(
formatter,
"Viewport {{ top: {}, bottom: {} }}",
top_line, bottom_line
),
}
}
}
pub struct Window { pub struct Window {
grid_id: u64, grid_id: u64,
grid: CharacterGrid, grid: CharacterGrid,
@ -123,18 +92,18 @@ impl Window {
grid_top: self.grid_top, grid_top: self.grid_top,
width: self.grid.width, width: self.grid.width,
height: self.grid.height, height: self.grid.height,
floating: self.anchor_info.is_some(), floating_order: self.anchor_info.clone().map(|anchor| anchor.sort_order),
}); });
} }
pub fn get_cursor_character(&self, window_left: u64, window_top: u64) -> (String, bool) { pub fn get_cursor_character(&self, window_left: u64, window_top: u64) -> (String, bool) {
let character = match self.grid.get_cell(window_left, window_top) { let character = match self.grid.get_cell(window_left, window_top) {
Some(Some((character, _))) => character.clone(), Some((character, _)) => character.clone(),
_ => ' '.to_string(), _ => ' '.to_string(),
}; };
let double_width = match self.grid.get_cell(window_left + 1, window_top) { let double_width = match self.grid.get_cell(window_left + 1, window_top) {
Some(Some((character, _))) => character.is_empty(), Some((character, _)) => character.is_empty(),
_ => false, _ => false,
}; };
@ -199,80 +168,58 @@ impl Window {
// Insert the contents of the cell into the grid. // Insert the contents of the cell into the grid.
if text.is_empty() { if text.is_empty() {
if let Some(cell) = self.grid.get_cell_mut(*column_pos, row_index) { if let Some(cell) = self.grid.get_cell_mut(*column_pos, row_index) {
*cell = Some((" ".to_string(), style.clone())); *cell = (text, style.clone());
} }
*column_pos += 1; *column_pos += 1;
} else { } else {
for (i, character) in text.graphemes(true).enumerate() { for character in text.graphemes(true) {
if let Some(cell) = self.grid.get_cell_mut(i as u64 + *column_pos, row_index) { if let Some(cell) = self.grid.get_cell_mut(*column_pos, row_index) {
*cell = Some((character.to_string(), style.clone())); *cell = (character.to_string(), style.clone());
} }
*column_pos += 1;
} }
*column_pos += text.graphemes(true).count() as u64;
} }
*previous_style = style; *previous_style = style;
} }
// Send a draw command for the given row starting from current_start up until the next style // Send a draw command for the given row starting from current_start up until the next style
// change. If the current_start is the same as line_start, this will also work backwards in the // change or double width character.
// line in order to ensure that ligatures before the beginning of the grid cell are also fn send_draw_command(&self, row_index: u64, start: u64) -> Option<u64> {
// updated.
fn send_draw_command(
&self,
row_index: u64,
line_start: u64,
current_start: u64,
) -> Option<u64> {
let row = self.grid.row(row_index).unwrap(); let row = self.grid.row(row_index).unwrap();
let (_, style) = &row[current_start as usize].as_ref()?; let (_, style) = &row[start as usize];
let mut draw_command_start_index = current_start; let mut cells = Vec::new();
if current_start == line_start { let mut width = 0;
// Locate contiguous same styled cells before the inserted cells. for possible_end_index in start..self.grid.width {
// This way any ligatures are correctly rerendered. let (character, possible_end_style) = &row[possible_end_index as usize];
// This could be sped up if we knew what characters were a part of a ligature, but in the
// current system we do not. // Style doesn't match. Draw what we've got
for possible_start_index in (0..current_start).rev() { if style != possible_end_style {
if let Some((_, possible_start_style)) = &row[possible_start_index as usize] {
if style == possible_start_style {
draw_command_start_index = possible_start_index;
continue;
}
}
break; break;
} }
}
let mut draw_command_end_index = current_start; width += 1;
for possible_end_index in draw_command_start_index..self.grid.width { // The previous character is double width, so send this as its own draw command
if let Some((_, possible_end_style)) = &row[possible_end_index as usize] { if character.is_empty() {
if style == possible_end_style { break;
draw_command_end_index = possible_end_index;
continue;
}
} }
break;
}
// Build up the actual text to be rendered including the contiguously styled bits. // Add the grid cell to the cells to render
let mut text = String::new(); cells.push(character.clone());
for x in draw_command_start_index..(draw_command_end_index + 1) {
let (character, _) = row[x as usize].as_ref().unwrap();
text.push_str(character);
} }
// Send a window draw command to the current window. // Send a window draw command to the current window.
self.send_command(WindowDrawCommand::Cell { self.send_command(WindowDrawCommand::Cells {
text, cells,
cell_width: draw_command_end_index - draw_command_start_index + 1, window_left: start,
window_left: draw_command_start_index,
window_top: row_index, window_top: row_index,
width,
style: style.clone(), style: style.clone(),
}); });
Some(draw_command_end_index + 1) Some(start + width)
} }
pub fn draw_grid_line( pub fn draw_grid_line(
@ -295,9 +242,11 @@ impl Window {
); );
} }
let mut current_start = column_start; // Redraw the participating line by calling send_draw_command starting at 0
while current_start < column_pos { // until current_start is greater than the grid width
if let Some(next_start) = self.send_draw_command(row, column_start, current_start) { let mut current_start = 0;
while current_start < self.grid.width {
if let Some(next_start) = self.send_draw_command(row, current_start) {
current_start = next_start; current_start = next_start;
} else { } else {
break; break;
@ -369,7 +318,7 @@ impl Window {
for row in 0..self.grid.height { for row in 0..self.grid.height {
let mut current_start = 0; let mut current_start = 0;
while current_start < self.grid.width { while current_start < self.grid.width {
if let Some(next_start) = self.send_draw_command(row, 0, current_start) { if let Some(next_start) = self.send_draw_command(row, current_start) {
current_start = next_start; current_start = next_start;
} else { } else {
break; break;
@ -397,3 +346,54 @@ impl Window {
}); });
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::channel_utils::*;
use std::collections::HashMap;
use std::sync::mpsc::*;
fn build_test_channels() -> (Receiver<Vec<DrawCommand>>, Arc<DrawCommandBatcher>) {
let (batched_draw_command_sender, batched_draw_command_receiver) = channel();
let logging_batched_draw_command_sender = LoggingSender::attach(
batched_draw_command_sender,
"batched_draw_command".to_owned(),
);
let draw_command_batcher =
Arc::new(DrawCommandBatcher::new(logging_batched_draw_command_sender));
(batched_draw_command_receiver, draw_command_batcher)
}
#[test]
fn windowSeparator_modifiesGridAndSendsDrawCommand() {
let (batched_receiver, batched_sender) = build_test_channels();
let mut window = Window::new(1, 114, 64, None, 0.0, 0.0, batched_sender.clone());
batched_sender
.send_batch()
.expect("Could not send batch of commands");
batched_receiver.recv().expect("Could not receive commands");
window.draw_grid_line(
1,
70,
vec![GridLineCell {
text: "|".to_owned(),
highlight_id: None,
repeat: None,
}],
&HashMap::new(),
);
assert_eq!(window.grid.get_cell(70, 1), Some(&("|".to_owned(), None)));
batched_sender
.send_batch()
.expect("Could not send batch of commands");
let sent_commands = batched_receiver.recv().expect("Could not receive commands");
assert!(sent_commands.len() != 0);
}
}

@ -1,9 +1,20 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[cfg(not(test))]
use flexi_logger::{Cleanup, Criterion, Duplicate, Logger, Naming};
// Test naming occasionally uses camelCase with underscores to separate sections of
// the test name.
#[cfg_attr(test, allow(non_snake_case))]
#[macro_use] #[macro_use]
extern crate neovide_derive; extern crate neovide_derive;
#[macro_use]
extern crate clap;
mod bridge; mod bridge;
mod channel_utils;
mod cmd_line;
mod editor; mod editor;
mod error_handling; mod error_handling;
mod redraw_scheduler; mod redraw_scheduler;
@ -24,11 +35,15 @@ use std::sync::{atomic::AtomicBool, mpsc::channel, Arc};
use crossfire::mpsc::unbounded_future; use crossfire::mpsc::unbounded_future;
use bridge::start_bridge; use bridge::start_bridge;
#[cfg(not(test))]
use cmd_line::CmdLineSettings;
use editor::start_editor; use editor::start_editor;
use renderer::{cursor_renderer::CursorSettings, RendererSettings}; use renderer::{cursor_renderer::CursorSettings, RendererSettings};
#[cfg(not(test))]
use settings::SETTINGS;
use window::{create_window, window_geometry, KeyboardSettings, WindowSettings}; use window::{create_window, window_geometry, KeyboardSettings, WindowSettings};
use windows_utils::attach_parent_console;
pub use channel_utils::*;
pub const INITIAL_DIMENSIONS: (u64, u64) = (100, 50); pub const INITIAL_DIMENSIONS: (u64, u64) = (100, 50);
fn main() { fn main() {
@ -101,28 +116,21 @@ fn main() {
// Multiple other parts of the app "queue_next_frame" function to ensure animations continue // Multiple other parts of the app "queue_next_frame" function to ensure animations continue
// properly or updates to the graphics are pushed to the screen. // properly or updates to the graphics are pushed to the screen.
if std::env::args().any(|arg| arg == "--version" || arg == "-v") { cmd_line::handle_command_line_arguments(); //Will exit if -h or -v
attach_parent_console();
println!("Neovide version: {}", env!("CARGO_PKG_VERSION"));
return;
}
if std::env::args().any(|arg| arg == "--help" || arg == "-h") {
attach_parent_console();
println!("Neovide: {}", env!("CARGO_PKG_DESCRIPTION"));
return;
}
if let Err(err) = window_geometry() { if let Err(err) = window_geometry() {
eprintln!("{}", err); eprintln!("{}", err);
return; return;
} }
#[cfg(not(test))]
init_logger();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// incase of app bundle, we can just pass --disowned option straight away to bypass this check // incase of app bundle, we can just pass --disowned option straight away to bypass this check
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
if !std::env::args().any(|f| f == "--disowned") { if !SETTINGS.get::<CmdLineSettings>().disowned {
if let Ok(curr_exe) = std::env::current_exe() { if let Ok(curr_exe) = std::env::current_exe() {
assert!(std::process::Command::new(curr_exe) assert!(std::process::Command::new(curr_exe)
.args(std::env::args().skip(1)) .args(std::env::args().skip(1))
@ -156,33 +164,72 @@ fn main() {
KeyboardSettings::register(); KeyboardSettings::register();
WindowSettings::register(); WindowSettings::register();
redraw_scheduler::RedrawSettings::register();
RendererSettings::register(); RendererSettings::register();
CursorSettings::register(); CursorSettings::register();
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let (redraw_event_sender, redraw_event_receiver) = unbounded_future(); let (redraw_event_sender, redraw_event_receiver) = unbounded_future();
let logging_redraw_event_sender =
LoggingTx::attach(redraw_event_sender, "redraw_event".to_owned());
let (batched_draw_command_sender, batched_draw_command_receiver) = channel(); let (batched_draw_command_sender, batched_draw_command_receiver) = channel();
let logging_batched_draw_command_sender = LoggingSender::attach(
batched_draw_command_sender,
"batched_draw_command".to_owned(),
);
let (ui_command_sender, ui_command_receiver) = unbounded_future(); let (ui_command_sender, ui_command_receiver) = unbounded_future();
let logging_ui_command_sender = LoggingTx::attach(ui_command_sender, "ui_command".to_owned());
let (window_command_sender, window_command_receiver) = channel(); let (window_command_sender, window_command_receiver) = channel();
let logging_window_command_sender =
LoggingSender::attach(window_command_sender, "window_command".to_owned());
// We need to keep the bridge reference around to prevent the tokio runtime from getting freed // We need to keep the bridge reference around to prevent the tokio runtime from getting freed
let _bridge = start_bridge( let _bridge = start_bridge(
ui_command_sender.clone(), logging_ui_command_sender.clone(),
ui_command_receiver, ui_command_receiver,
redraw_event_sender, logging_redraw_event_sender,
running.clone(), running.clone(),
); );
start_editor( start_editor(
redraw_event_receiver, redraw_event_receiver,
batched_draw_command_sender, logging_batched_draw_command_sender,
window_command_sender, logging_window_command_sender,
); );
create_window( create_window(
batched_draw_command_receiver, batched_draw_command_receiver,
window_command_receiver, window_command_receiver,
ui_command_sender, logging_ui_command_sender,
running, running,
); );
} }
#[cfg(not(test))]
pub fn init_logger() {
let verbosity = match SETTINGS.get::<CmdLineSettings>().verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let log_to_file = SETTINGS.get::<CmdLineSettings>().log_to_file;
if log_to_file {
Logger::with_env_or_str("neovide")
.duplicate_to_stderr(Duplicate::Error)
.log_to_file()
.rotate(
Criterion::Size(10_000_000),
Naming::Timestamps,
Cleanup::KeepLogFiles(1),
)
.start()
.expect("Could not start logger");
} else {
Logger::with_env_or_str(format!("neovide = {}", verbosity))
.start()
.expect("Could not start logger");
}
}

@ -1,10 +1,10 @@
use std::sync::atomic::{AtomicU16, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Instant; use std::time::Instant;
use log::trace; use log::trace;
use crate::settings::*; use crate::{cmd_line::CmdLineSettings, settings::*};
lazy_static! { lazy_static! {
pub static ref REDRAW_SCHEDULER: RedrawScheduler = RedrawScheduler::new(); pub static ref REDRAW_SCHEDULER: RedrawScheduler = RedrawScheduler::new();
@ -19,7 +19,8 @@ impl Default for RedrawSettings {
fn default() -> Self { fn default() -> Self {
Self { Self {
extra_buffer_frames: if SETTINGS extra_buffer_frames: if SETTINGS
.neovim_arguments .get::<CmdLineSettings>()
.neovim_args
.contains(&"--extraBufferFrames".to_string()) .contains(&"--extraBufferFrames".to_string())
{ {
60 60
@ -31,15 +32,15 @@ impl Default for RedrawSettings {
} }
pub struct RedrawScheduler { pub struct RedrawScheduler {
frames_queued: AtomicU16,
scheduled_frame: Mutex<Option<Instant>>, scheduled_frame: Mutex<Option<Instant>>,
frame_queued: AtomicBool,
} }
impl RedrawScheduler { impl RedrawScheduler {
pub fn new() -> RedrawScheduler { pub fn new() -> RedrawScheduler {
RedrawScheduler { RedrawScheduler {
frames_queued: AtomicU16::new(1),
scheduled_frame: Mutex::new(None), scheduled_frame: Mutex::new(None),
frame_queued: AtomicBool::new(true),
} }
} }
@ -58,18 +59,12 @@ impl RedrawScheduler {
pub fn queue_next_frame(&self) { pub fn queue_next_frame(&self) {
trace!("Next frame queued"); trace!("Next frame queued");
let buffer_frames = SETTINGS.get::<RedrawSettings>().extra_buffer_frames; self.frame_queued.store(true, Ordering::Relaxed);
self.frames_queued
.store(buffer_frames as u16, Ordering::Relaxed);
} }
pub fn should_draw(&self) -> bool { pub fn should_draw(&self) -> bool {
let frames_queued = self.frames_queued.load(Ordering::Relaxed); if self.frame_queued.load(Ordering::Relaxed) {
self.frame_queued.store(false, Ordering::Relaxed);
if frames_queued > 0 {
self.frames_queued
.store(frames_queued - 1, Ordering::Relaxed);
true true
} else { } else {
let mut next_scheduled_frame = self.scheduled_frame.lock().unwrap(); let mut next_scheduled_frame = self.scheduled_frame.lock().unwrap();

@ -1,4 +1,4 @@
use skulpin::skia_safe::Point; use skia_safe::Point;
#[allow(dead_code)] #[allow(dead_code)]
pub fn ease_linear(t: f32) -> f32 { pub fn ease_linear(t: f32) -> f32 {

@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
use crate::editor::Cursor; use crate::editor::Cursor;
use crate::redraw_scheduler::REDRAW_SCHEDULER; use crate::redraw_scheduler::REDRAW_SCHEDULER;
#[derive(Debug)]
pub enum BlinkState { pub enum BlinkState {
Waiting, Waiting,
On, On,
@ -74,8 +75,8 @@ impl BlinkStatus {
} }
match self.state { match self.state {
BlinkState::Waiting | BlinkState::Off => false, BlinkState::Off => false,
BlinkState::On => true, BlinkState::On | BlinkState::Waiting => true,
} }
} }
} }

@ -1,5 +1,5 @@
use log::error; use log::error;
use skulpin::skia_safe::{paint::Style, BlendMode, Canvas, Color, Paint, Point, Rect}; use skia_safe::{paint::Style, BlendMode, Canvas, Color, Paint, Point, Rect};
use super::CursorSettings; use super::CursorSettings;
use crate::editor::{Colors, Cursor}; use crate::editor::{Colors, Cursor};
@ -11,7 +11,7 @@ pub trait CursorVfx {
&mut self, &mut self,
settings: &CursorSettings, settings: &CursorSettings,
current_cursor_destination: Point, current_cursor_destination: Point,
font_size: (f32, f32), font_size: (u64, u64),
dt: f32, dt: f32,
) -> bool; ) -> bool;
fn restart(&mut self, position: Point); fn restart(&mut self, position: Point);
@ -21,7 +21,7 @@ pub trait CursorVfx {
canvas: &mut Canvas, canvas: &mut Canvas,
cursor: &Cursor, cursor: &Cursor,
colors: &Colors, colors: &Colors,
font_size: (f32, f32), font_size: (u64, u64),
); );
} }
@ -111,7 +111,7 @@ impl CursorVfx for PointHighlight {
&mut self, &mut self,
_settings: &CursorSettings, _settings: &CursorSettings,
_current_cursor_destination: Point, _current_cursor_destination: Point,
_font_size: (f32, f32), _font_size: (u64, u64),
dt: f32, dt: f32,
) -> bool { ) -> bool {
self.t = (self.t + dt * 5.0).min(1.0); // TODO - speed config self.t = (self.t + dt * 5.0).min(1.0); // TODO - speed config
@ -129,23 +129,23 @@ impl CursorVfx for PointHighlight {
canvas: &mut Canvas, canvas: &mut Canvas,
cursor: &Cursor, cursor: &Cursor,
colors: &Colors, colors: &Colors,
font_size: (f32, f32), font_size: (u64, u64),
) { ) {
if (self.t - 1.0).abs() < std::f32::EPSILON { if (self.t - 1.0).abs() < std::f32::EPSILON {
return; return;
} }
let mut paint = Paint::new(skulpin::skia_safe::colors::WHITE, None); let mut paint = Paint::new(skia_safe::colors::WHITE, None);
paint.set_blend_mode(BlendMode::SrcOver); paint.set_blend_mode(BlendMode::SrcOver);
let base_color: Color = cursor.background(&colors).to_color(); let base_color: Color = cursor.background(colors).to_color();
let alpha = ease(ease_in_quad, settings.vfx_opacity, 0.0, self.t) as u8; let alpha = ease(ease_in_quad, settings.vfx_opacity, 0.0, self.t) as u8;
let color = Color::from_argb(alpha, base_color.r(), base_color.g(), base_color.b()); let color = Color::from_argb(alpha, base_color.r(), base_color.g(), base_color.b());
paint.set_color(color); paint.set_color(color);
let size = 3.0 * font_size.1; let size = 3 * font_size.1;
let radius = self.t * size; let radius = self.t * size as f32;
let hr = radius * 0.5; let hr = radius * 0.5;
let rect = Rect::from_xywh( let rect = Rect::from_xywh(
self.center_position.x - hr, self.center_position.x - hr,
@ -160,12 +160,12 @@ impl CursorVfx for PointHighlight {
} }
HighlightMode::Ripple => { HighlightMode::Ripple => {
paint.set_style(Style::Stroke); paint.set_style(Style::Stroke);
paint.set_stroke_width(font_size.1 * 0.2); paint.set_stroke_width(font_size.1 as f32 * 0.2);
canvas.draw_oval(&rect, &paint); canvas.draw_oval(&rect, &paint);
} }
HighlightMode::Wireframe => { HighlightMode::Wireframe => {
paint.set_style(Style::Stroke); paint.set_style(Style::Stroke);
paint.set_stroke_width(font_size.1 * 0.2); paint.set_stroke_width(font_size.1 as f32 * 0.2);
canvas.draw_rect(&rect, &paint); canvas.draw_rect(&rect, &paint);
} }
} }
@ -218,7 +218,7 @@ impl CursorVfx for ParticleTrail {
&mut self, &mut self,
settings: &CursorSettings, settings: &CursorSettings,
current_cursor_dest: Point, current_cursor_dest: Point,
font_size: (f32, f32), font_size: (u64, u64),
dt: f32, dt: f32,
) -> bool { ) -> bool {
// Update lifetimes and remove dead particles // Update lifetimes and remove dead particles
@ -246,7 +246,7 @@ impl CursorVfx for ParticleTrail {
let travel_distance = travel.length(); let travel_distance = travel.length();
// Increase amount of particles when cursor travels further // Increase amount of particles when cursor travels further
let particle_count = ((travel_distance / font_size.0).powf(1.5) let particle_count = ((travel_distance / font_size.0 as f32).powf(1.5)
* settings.vfx_particle_density * settings.vfx_particle_density
* 0.01) as usize; * 0.01) as usize;
@ -259,7 +259,7 @@ impl CursorVfx for ParticleTrail {
TrailMode::Railgun => { TrailMode::Railgun => {
let phase = t / std::f32::consts::PI let phase = t / std::f32::consts::PI
* settings.vfx_particle_phase * settings.vfx_particle_phase
* (travel_distance / font_size.0); * (travel_distance / font_size.0 as f32);
Point::new(phase.sin(), phase.cos()) * 2.0 * settings.vfx_particle_speed Point::new(phase.sin(), phase.cos()) * 2.0 * settings.vfx_particle_speed
} }
TrailMode::Torpedo => { TrailMode::Torpedo => {
@ -280,7 +280,9 @@ impl CursorVfx for ParticleTrail {
let pos = match self.trail_mode { let pos = match self.trail_mode {
TrailMode::Railgun => prev_p + travel * t, TrailMode::Railgun => prev_p + travel * t,
TrailMode::PixieDust | TrailMode::Torpedo => { TrailMode::PixieDust | TrailMode::Torpedo => {
prev_p + travel * self.rng.next_f32() + Point::new(0.0, font_size.1 * 0.5) prev_p
+ travel * self.rng.next_f32()
+ Point::new(0.0, font_size.1 as f32 * 0.5)
} }
}; };
@ -316,30 +318,30 @@ impl CursorVfx for ParticleTrail {
canvas: &mut Canvas, canvas: &mut Canvas,
cursor: &Cursor, cursor: &Cursor,
colors: &Colors, colors: &Colors,
font_size: (f32, f32), font_size: (u64, u64),
) { ) {
let mut paint = Paint::new(skulpin::skia_safe::colors::WHITE, None); let mut paint = Paint::new(skia_safe::colors::WHITE, None);
match self.trail_mode { match self.trail_mode {
TrailMode::Torpedo | TrailMode::Railgun => { TrailMode::Torpedo | TrailMode::Railgun => {
paint.set_style(Style::Stroke); paint.set_style(Style::Stroke);
paint.set_stroke_width(font_size.1 * 0.2); paint.set_stroke_width(font_size.1 as f32 * 0.2);
} }
_ => {} _ => {}
} }
let base_color: Color = cursor.background(&colors).to_color(); let base_color: Color = cursor.background(colors).to_color();
paint.set_blend_mode(BlendMode::SrcOver); paint.set_blend_mode(BlendMode::SrcOver);
self.particles.iter().for_each(|particle| { self.particles.iter().for_each(|particle| {
let l = particle.lifetime / settings.vfx_particle_lifetime; let lifetime = particle.lifetime / settings.vfx_particle_lifetime;
let alpha = (l * settings.vfx_opacity) as u8; let alpha = (lifetime * settings.vfx_opacity) as u8;
let color = Color::from_argb(alpha, base_color.r(), base_color.g(), base_color.b()); let color = Color::from_argb(alpha, base_color.r(), base_color.g(), base_color.b());
paint.set_color(color); paint.set_color(color);
let radius = match self.trail_mode { let radius = match self.trail_mode {
TrailMode::Torpedo | TrailMode::Railgun => font_size.0 * 0.5 * l, TrailMode::Torpedo | TrailMode::Railgun => font_size.0 as f32 * 0.5 * lifetime,
TrailMode::PixieDust => font_size.0 * 0.2, TrailMode::PixieDust => font_size.0 as f32 * 0.2,
}; };
let hr = radius * 0.5; let hr = radius * 0.5;

@ -4,7 +4,7 @@ mod cursor_vfx;
use std::collections::HashMap; use std::collections::HashMap;
// use neovide_derive::SettingGroup; // use neovide_derive::SettingGroup;
use skulpin::skia_safe::{Canvas, Paint, Path, Point}; use skia_safe::{Canvas, Paint, Path, Point};
use super::RenderedWindow; use super::RenderedWindow;
use crate::bridge::EditorMode; use crate::bridge::EditorMode;
@ -20,8 +20,9 @@ const DEFAULT_CELL_PERCENTAGE: f32 = 1.0 / 8.0;
const STANDARD_CORNERS: &[(f32, f32); 4] = &[(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)]; const STANDARD_CORNERS: &[(f32, f32); 4] = &[(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)];
#[derive(Clone, SettingGroup)] #[derive(SettingGroup)]
#[setting_prefix = "cursor"] #[setting_prefix = "cursor"]
#[derive(Clone)]
pub struct CursorSettings { pub struct CursorSettings {
antialiasing: bool, antialiasing: bool,
animation_length: f32, animation_length: f32,
@ -42,7 +43,8 @@ impl Default for CursorSettings {
fn default() -> Self { fn default() -> Self {
CursorSettings { CursorSettings {
antialiasing: true, antialiasing: true,
animation_length: 0.13, animation_length: 0.06,
distance_length_adjust: true,
animate_in_insert_mode: true, animate_in_insert_mode: true,
animate_command_line: true, animate_command_line: true,
trail_size: 0.7, trail_size: 0.7,
@ -146,7 +148,8 @@ impl Corner {
(1.0 - settings.trail_size).max(0.0).min(1.0), (1.0 - settings.trail_size).max(0.0).min(1.0),
-direction_alignment, -direction_alignment,
); );
self.t = (self.t + corner_dt / (settings.animation_length * self.length_multiplier)).min(1.0) self.t =
(self.t + corner_dt / (settings.animation_length * self.length_multiplier)).min(1.0)
} }
self.current_position = ease_point( self.current_position = ease_point(
@ -223,32 +226,30 @@ impl CursorRenderer {
pub fn update_cursor_destination( pub fn update_cursor_destination(
&mut self, &mut self,
font_width: f32, font_width: u64,
font_height: f32, font_height: u64,
windows: &HashMap<u64, RenderedWindow>, windows: &HashMap<u64, RenderedWindow>,
current_mode: &EditorMode, current_mode: &EditorMode,
) { ) {
let (cursor_grid_x, cursor_grid_y) = self.cursor.grid_position; let (cursor_grid_x, cursor_grid_y) = self.cursor.grid_position;
if let Some(window) = windows.get(&self.cursor.parent_window_id) { if let Some(window) = windows.get(&self.cursor.parent_window_id) {
if cursor_grid_y < window.grid_height-1 || matches!(current_mode, EditorMode::CmdLine) { let grid_x = cursor_grid_x as f32 + window.grid_current_position.x;
let grid_x = cursor_grid_x as f32 + window.grid_current_position.x; let mut grid_y = cursor_grid_y as f32 + window.grid_current_position.y
let mut grid_y = cursor_grid_y as f32 + window.grid_current_position.y - (window.current_scroll - window.current_surface.top_line as f32);
- (window.current_scroll - window.current_surfaces.top_line);
// Prevent the cursor from targeting a position outside its current window. Since only
// Prevent the cursor from targeting a position outside its current window. Since only // the vertical direction is effected by scrolling, we only have to clamp the vertical
// the vertical direction is effected by scrolling, we only have to clamp the vertical // grid position.
// grid position. grid_y = grid_y
grid_y = grid_y .max(window.grid_current_position.y)
.max(window.grid_current_position.y) .min(window.grid_current_position.y + window.grid_height as f32 - 1.0);
.min(window.grid_current_position.y + window.grid_height as f32 - 1.0);
self.destination = (grid_x * font_width as f32, grid_y * font_height as f32).into();
self.destination = (grid_x * font_width, grid_y * font_height).into();
}
} else { } else {
self.destination = ( self.destination = (
cursor_grid_x as f32 * font_width, (cursor_grid_x * font_width) as f32,
cursor_grid_y as f32 * font_height, (cursor_grid_y * font_height) as f32,
) )
.into(); .into();
} }
@ -257,7 +258,7 @@ impl CursorRenderer {
pub fn draw( pub fn draw(
&mut self, &mut self,
default_colors: &Colors, default_colors: &Colors,
font_size: (f32, f32), font_size: (u64, u64),
current_mode: &EditorMode, current_mode: &EditorMode,
shaper: &mut CachingShaper, shaper: &mut CachingShaper,
canvas: &mut Canvas, canvas: &mut Canvas,
@ -272,21 +273,22 @@ impl CursorRenderer {
self.previous_vfx_mode = settings.vfx_mode.clone(); self.previous_vfx_mode = settings.vfx_mode.clone();
} }
let mut paint = Paint::new(skulpin::skia_safe::colors::WHITE, None); let mut paint = Paint::new(skia_safe::colors::WHITE, None);
paint.set_anti_alias(settings.antialiasing); paint.set_anti_alias(settings.antialiasing);
let character = self.cursor.character.clone(); let character = self.cursor.character.clone();
let font_width = match (self.cursor.double_width, &self.cursor.shape) { let font_width = match (self.cursor.double_width, &self.cursor.shape) {
(true, CursorShape::Block) => font_width * 2.0, (true, CursorShape::Block) => font_width * 2,
_ => font_width, _ => font_width,
}; };
let font_dimensions: Point = (font_width, font_height).into(); let font_dimensions: Point = (font_width as f32, font_height as f32).into();
let in_insert_mode = matches!(current_mode, EditorMode::Insert); let in_insert_mode = matches!(current_mode, EditorMode::Insert);
let changed_to_from_cmdline = !matches!(self.previous_editor_mode, EditorMode::CmdLine)
^ matches!(current_mode, EditorMode::CmdLine); let changed_to_from_cmdline = !matches!(self.previous_editor_mode, EditorMode::CmdLine)
^ matches!(current_mode, EditorMode::CmdLine);
let center_destination = self.destination + font_dimensions * 0.5; let center_destination = self.destination + font_dimensions * 0.5;
let new_cursor = Some(self.cursor.shape.clone()); let new_cursor = Some(self.cursor.shape.clone());
@ -309,13 +311,15 @@ impl CursorRenderer {
if !center_destination.is_zero() { if !center_destination.is_zero() {
for corner in self.corners.iter_mut() { for corner in self.corners.iter_mut() {
let immediate_movement = !settings.animate_in_insert_mode && in_insert_mode
|| !settings.animate_command_line && !changed_to_from_cmdline;
let corner_animating = corner.update( let corner_animating = corner.update(
&settings, &settings,
font_dimensions, font_dimensions,
center_destination, center_destination,
dt, dt,
!settings.animate_in_insert_mode && in_insert_mode immediate_movement,
|| !settings.animate_command_line && !changed_to_from_cmdline,
); );
animating |= corner_animating; animating |= corner_animating;
@ -338,7 +342,7 @@ impl CursorRenderer {
if self.cursor.enabled && render { if self.cursor.enabled && render {
// Draw Background // Draw Background
paint.set_color(self.cursor.background(&default_colors).to_color()); paint.set_color(self.cursor.background(default_colors).to_color());
// The cursor is made up of four points, so I create a path with each of the four // The cursor is made up of four points, so I create a path with each of the four
// corners. // corners.
@ -353,15 +357,20 @@ impl CursorRenderer {
canvas.draw_path(&path, &paint); canvas.draw_path(&path, &paint);
// Draw foreground // Draw foreground
paint.set_color(self.cursor.foreground(&default_colors).to_color()); paint.set_color(self.cursor.foreground(default_colors).to_color());
canvas.save(); canvas.save();
canvas.clip_path(&path, None, Some(false)); canvas.clip_path(&path, None, Some(false));
let blobs = &shaper.shape_cached(&character, false, false); let y_adjustment = shaper.y_adjustment();
let blobs = &shaper.shape_cached(&[character], false, false);
for blob in blobs.iter() { for blob in blobs.iter() {
canvas.draw_text_blob(&blob, self.destination, &paint); canvas.draw_text_blob(
&blob,
(self.destination.x, self.destination.y + y_adjustment as f32),
&paint,
);
} }
canvas.restore(); canvas.restore();
@ -371,7 +380,7 @@ impl CursorRenderer {
&settings, &settings,
canvas, canvas,
&self.cursor, &self.cursor,
&default_colors, default_colors,
(font_width, font_height), (font_width, font_height),
); );
} }

@ -1,190 +1,266 @@
use std::collections::HashMap; use std::sync::Arc;
use font_kit::metrics::Metrics;
use log::{trace, warn}; use log::{trace, warn};
use lru::LruCache; use lru::LruCache;
use skribo::{FontCollection, FontRef as SkriboFont, LayoutSession, TextStyle}; use skia_safe::{TextBlob, TextBlobBuilder};
use skulpin::skia_safe::{Font as SkiaFont, TextBlob, TextBlobBuilder}; use swash::shape::ShapeContext;
use swash::text::cluster::{CharCluster, Parser, Status, Token};
use swash::text::Script;
use swash::Metrics;
use unicode_segmentation::UnicodeSegmentation;
use super::font_loader::*; use super::font_loader::*;
use super::font_options::*; use super::font_options::*;
use super::utils::*;
const STANDARD_CHARACTER_STRING: &str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
#[cfg(any(feature = "embed-fonts", test))]
#[derive(RustEmbed)]
#[folder = "assets/fonts/"]
pub struct Asset;
const DEFAULT_FONT_SIZE: f32 = 14.0; const DEFAULT_FONT_SIZE: f32 = 14.0;
#[derive(new, Clone, Hash, PartialEq, Eq, Debug)] #[derive(new, Clone, Hash, PartialEq, Eq, Debug)]
struct ShapeKey { struct ShapeKey {
pub text: String, pub cells: Vec<String>,
pub bold: bool, pub bold: bool,
pub italic: bool, pub italic: bool,
} }
struct FontSet {
normal: FontCollection,
bold: FontCollection,
italic: FontCollection,
}
impl FontSet {
fn new(fallback_list: &[String], loader: &mut FontLoader) -> FontSet {
FontSet {
normal: loader
.build_collection_by_font_name(fallback_list, build_properties(false, false)),
bold: loader
.build_collection_by_font_name(fallback_list, build_properties(true, false)),
italic: loader
.build_collection_by_font_name(fallback_list, build_properties(false, true)),
}
}
fn get(&self, bold: bool, italic: bool) -> &FontCollection {
match (bold, italic) {
(true, _) => &self.bold,
(false, false) => &self.normal,
(false, true) => &self.italic,
}
}
}
pub struct CachingShaper { pub struct CachingShaper {
pub options: FontOptions, pub options: Option<FontOptions>,
font_set: FontSet,
font_loader: FontLoader, font_loader: FontLoader,
font_cache: LruCache<String, SkiaFont>,
blob_cache: LruCache<ShapeKey, Vec<TextBlob>>, blob_cache: LruCache<ShapeKey, Vec<TextBlob>>,
shape_context: ShapeContext,
} }
impl CachingShaper { impl CachingShaper {
pub fn new() -> CachingShaper { pub fn new() -> CachingShaper {
let options = FontOptions::new(String::from(SYSTEM_DEFAULT_FONT), DEFAULT_FONT_SIZE);
let mut loader = FontLoader::new();
let font_set = FontSet::new(&options.fallback_list, &mut loader);
CachingShaper { CachingShaper {
options, options: None,
font_set, font_loader: FontLoader::new(DEFAULT_FONT_SIZE),
font_loader: loader,
font_cache: LruCache::new(10),
blob_cache: LruCache::new(10000), blob_cache: LruCache::new(10000),
shape_context: ShapeContext::new(),
} }
} }
fn get_skia_font(&mut self, skribo_font: &SkriboFont) -> Option<&SkiaFont> { fn current_font_pair(&mut self) -> Arc<FontPair> {
let font_name = skribo_font.font.postscript_name()?; let font_key = self
if !self.font_cache.contains(&font_name) { .options
let font = build_skia_font_from_skribo_font(skribo_font, self.options.size)?; .as_ref()
self.font_cache.put(font_name.clone(), font); .map(|options| options.fallback_list.first().unwrap().clone().into())
.unwrap_or(FontKey::Default);
self.font_loader
.get_or_load(font_key)
.expect("Could not load font")
}
pub fn current_size(&self) -> f32 {
self.options
.as_ref()
.map(|options| options.size)
.unwrap_or(DEFAULT_FONT_SIZE)
}
pub fn update_font(&mut self, guifont_setting: &str) -> bool {
let new_options = FontOptions::parse(guifont_setting, DEFAULT_FONT_SIZE);
if new_options != self.options && new_options.is_some() {
self.font_loader = FontLoader::new(new_options.as_ref().unwrap().size);
self.blob_cache.clear();
self.options = new_options;
true
} else {
false
} }
}
fn metrics(&mut self) -> Metrics {
let font_pair = self.current_font_pair();
let size = self.current_size();
let shaper = self
.shape_context
.builder(font_pair.swash_font.as_ref())
.size(size)
.build();
self.font_cache.get(&font_name) shaper.metrics()
} }
fn metrics(&self) -> Metrics { pub fn font_base_dimensions(&mut self) -> (u64, u64) {
self.font_set let metrics = self.metrics();
.normal let font_height = (metrics.ascent + metrics.descent + metrics.leading).ceil() as u64;
.itemize("a") let font_width = metrics.average_width as u64;
.next()
.expect("Cannot get font metrics") (font_width, font_height)
.1
.font
.metrics()
} }
pub fn shape(&mut self, text: &str, bold: bool, italic: bool) -> Vec<TextBlob> { pub fn underline_position(&mut self) -> u64 {
let style = TextStyle { self.metrics().underline_offset as u64
size: self.options.size, }
};
let session = LayoutSession::create(text, &style, &self.font_set.get(bold, italic)); pub fn y_adjustment(&mut self) -> u64 {
let metrics = self.metrics(); let metrics = self.metrics();
let ascent = metrics.ascent * self.options.size / metrics.units_per_em as f32; (metrics.ascent + metrics.leading) as u64
let mut blobs = Vec::new(); }
fn build_clusters(&mut self, text: &str) -> Vec<(Vec<CharCluster>, Arc<FontPair>)> {
let mut cluster = CharCluster::new();
// Enumerate the characters storing the glyph index in the user data so that we can position
// glyphs according to Neovim's grid rules
let mut character_index = 0;
let mut parser = Parser::new(
Script::Latin,
text.graphemes(true)
.enumerate()
.map(|(glyph_index, unicode_segment)| {
unicode_segment.chars().map(move |character| {
let token = Token {
ch: character,
offset: character_index as u32,
len: character.len_utf8() as u8,
info: character.into(),
data: glyph_index as u32,
};
character_index += 1;
token
})
})
.flatten(),
);
for layout_run in session.iter_all() { let mut results = Vec::new();
let skribo_font = layout_run.font(); 'cluster: while parser.next(&mut cluster) {
let mut font_fallback_keys = Vec::new();
if let Some(skia_font) = self.get_skia_font(&skribo_font) { // Add guifont fallback list
let mut blob_builder = TextBlobBuilder::new(); if let Some(options) = &self.options {
let count = layout_run.glyphs().count(); font_fallback_keys.extend(
let (glyphs, positions) = options
blob_builder.alloc_run_pos_h(&skia_font, count, ascent, None); .fallback_list
.iter()
.map(|font_name| font_name.into()),
);
}
// Add default font
font_fallback_keys.push(FontKey::Default);
// Add skia fallback
font_fallback_keys.push(cluster.chars()[0].ch.into());
for (i, glyph) in layout_run.glyphs().enumerate() { let mut best = None;
glyphs[i] = glyph.glyph_id as u16; // Use the cluster.map function to select a viable font from the fallback list
positions[i] = glyph.offset.x(); for fallback_key in font_fallback_keys.into_iter() {
if let Some(font_pair) = self.font_loader.get_or_load(fallback_key) {
let charmap = font_pair.swash_font.as_ref().charmap();
match cluster.map(|ch| charmap.map(ch)) {
Status::Complete => {
results.push((cluster.to_owned(), font_pair.clone()));
continue 'cluster;
}
Status::Keep => best = Some(font_pair),
Status::Discard => {}
}
} }
}
// If we find a font with partial coverage of the cluster, select it
if let Some(best) = best {
results.push((cluster.to_owned(), best.clone()));
continue 'cluster;
} else {
warn!("No valid font for {:?}", cluster.chars());
blobs.push(blob_builder.make().unwrap()); // No good match. Just render with the default font.
let default_font = self
.font_loader
.get_or_load(FontKey::Default)
.expect("Could not load default font");
results.push((cluster.to_owned(), default_font));
}
}
// Now we have to group clusters by the font used so that the shaper can actually form
// ligatures across clusters
let mut grouped_results = Vec::new();
let mut current_group = Vec::new();
let mut current_font_option = None;
for (cluster, font) in results {
if let Some(current_font) = current_font_option.clone() {
if current_font == font {
current_group.push(cluster);
} else {
grouped_results.push((current_group, current_font));
current_group = Vec::new();
current_font_option = Some(font);
}
} else { } else {
warn!("Could not load skribo font"); current_group = vec![cluster];
current_font_option = Some(font);
} }
} }
blobs if !current_group.is_empty() {
grouped_results.push((current_group, current_font_option.unwrap()));
}
grouped_results
} }
pub fn shape_cached(&mut self, text: &str, bold: bool, italic: bool) -> &Vec<TextBlob> { pub fn shape(&mut self, cells: &[String]) -> Vec<TextBlob> {
let key = ShapeKey::new(text.to_string(), bold, italic); let current_size = self.current_size();
let (glyph_width, _) = self.font_base_dimensions();
if !self.blob_cache.contains(&key) { let mut resulting_blobs = Vec::new();
let blobs = self.shape(text, bold, italic);
self.blob_cache.put(key.clone(), blobs);
}
self.blob_cache.get(&key).unwrap() let text = cells.concat();
} trace!("Shaping text: {}", text);
pub fn update_font(&mut self, guifont_setting: &str) -> bool { for (cluster_group, font_pair) in self.build_clusters(&text) {
let updated = self.options.update(guifont_setting); let mut shaper = self
if updated { .shape_context
trace!("Font changed: {:?}", self.options); .builder(font_pair.swash_font.as_ref())
self.font_set = FontSet::new(&self.options.fallback_list, &mut self.font_loader); .size(current_size)
self.font_cache.clear(); .build();
self.blob_cache.clear();
}
updated
}
pub fn font_base_dimensions(&mut self) -> (f32, f32) { let charmap = font_pair.swash_font.as_ref().charmap();
let metrics = self.metrics(); for mut cluster in cluster_group {
let font_height = cluster.map(|ch| charmap.map(ch));
(metrics.ascent - metrics.descent) * self.options.size / metrics.units_per_em as f32; shaper.add_cluster(&cluster);
let style = TextStyle { }
size: self.options.size,
};
let session =
LayoutSession::create(STANDARD_CHARACTER_STRING, &style, &self.font_set.normal);
let layout_run = session.iter_all().next().unwrap();
let glyph_offsets: Vec<f32> = layout_run.glyphs().map(|glyph| glyph.offset.x()).collect();
let glyph_advances: Vec<f32> = glyph_offsets
.windows(2)
.map(|pair| pair[1] - pair[0])
.collect();
let mut amounts = HashMap::new();
for advance in glyph_advances.iter() {
amounts
.entry(advance.to_string())
.and_modify(|e| *e += 1)
.or_insert(1);
}
let (font_width, _) = amounts.into_iter().max_by_key(|(_, count)| *count).unwrap(); let mut glyph_data = Vec::new();
let font_width = font_width.parse::<f32>().unwrap();
(font_width, font_height) shaper.shape_with(|glyph_cluster| {
for glyph in glyph_cluster.glyphs {
glyph_data.push((glyph.id, glyph.data as u64 * glyph_width));
}
});
if glyph_data.is_empty() {
return Vec::new();
}
let mut blob_builder = TextBlobBuilder::new();
let (glyphs, positions) =
blob_builder.alloc_run_pos_h(&font_pair.skia_font, glyph_data.len(), 0.0, None);
for (i, (glyph_id, glyph_x_position)) in glyph_data.iter().enumerate() {
glyphs[i] = *glyph_id;
positions[i] = *glyph_x_position as f32;
}
let blob = blob_builder.make();
resulting_blobs.push(blob.expect("Could not create textblob"));
}
resulting_blobs
} }
pub fn underline_position(&mut self) -> f32 { pub fn shape_cached(&mut self, cells: &[String], bold: bool, italic: bool) -> &Vec<TextBlob> {
let metrics = self.metrics(); let key = ShapeKey::new(cells.to_vec(), bold, italic);
-metrics.underline_position * self.options.size / metrics.units_per_em as f32
if !self.blob_cache.contains(&key) {
let blobs = self.shape(cells);
self.blob_cache.put(key.clone(), blobs);
}
self.blob_cache.get(&key).unwrap()
} }
} }

@ -1,116 +0,0 @@
use font_kit::{family_handle::FamilyHandle, font::Font, properties::Properties};
use skribo::{FontFamily, FontRef as SkriboFont};
#[derive(Clone)]
pub struct ExtendedFontFamily {
pub fonts: Vec<SkriboFont>,
}
impl Default for ExtendedFontFamily {
fn default() -> Self {
Self::new()
}
}
impl ExtendedFontFamily {
pub fn new() -> ExtendedFontFamily {
ExtendedFontFamily { fonts: Vec::new() }
}
pub fn add_font(&mut self, font: SkriboFont) {
self.fonts.push(font);
}
pub fn get(&self, props: Properties) -> Option<&Font> {
if let Some(first_handle) = &self.fonts.first() {
for handle in &self.fonts {
let font = &handle.font;
let properties = font.properties();
if properties.weight == props.weight
&& properties.style == props.style
&& properties.stretch == props.stretch
{
return Some(&font);
}
}
return Some(&first_handle.font);
}
None
}
}
impl From<FamilyHandle> for ExtendedFontFamily {
fn from(handle: FamilyHandle) -> Self {
handle
.fonts()
.iter()
.fold(ExtendedFontFamily::new(), |mut family, font| {
if let Ok(font) = font.load() {
family.add_font(SkriboFont::new(font));
}
family
})
}
}
impl From<ExtendedFontFamily> for FontFamily {
fn from(extended_font_family: ExtendedFontFamily) -> Self {
extended_font_family
.fonts
.iter()
.fold(FontFamily::new(), |mut new_family, font| {
new_family.add_font(font.clone());
new_family
})
}
}
#[cfg(test)]
mod test {
use font_kit::properties::{Properties, Stretch, Style, Weight};
use super::*;
use crate::renderer::fonts::caching_shaper::Asset;
const PROPERTIES: Properties = Properties {
weight: Weight::NORMAL,
style: Style::Normal,
stretch: Stretch::NORMAL,
};
const EXTRA_SYMBOL_FONT: &str = "Extra Symbols.otf";
fn dummy_font() -> SkriboFont {
SkriboFont::new(
Asset::get(EXTRA_SYMBOL_FONT)
.and_then(|font_data| Font::from_bytes(font_data.to_vec().into(), 0).ok())
.unwrap(),
)
}
#[test]
fn test_add_font() {
let mut eft = ExtendedFontFamily::new();
let font = dummy_font();
eft.add_font(font.clone());
assert_eq!(
eft.fonts.first().unwrap().font.full_name(),
font.font.full_name()
);
}
#[test]
fn test_get() {
let mut eft = ExtendedFontFamily::new();
assert!(eft.get(PROPERTIES).is_none());
let font = dummy_font();
eft.fonts.push(font.clone());
assert_eq!(
eft.get(font.font.properties()).unwrap().full_name(),
font.font.full_name()
);
}
}

@ -1,278 +1,128 @@
use std::iter; use std::sync::Arc;
use cfg_if::cfg_if;
use font_kit::{properties::Properties, source::SystemSource};
use lru::LruCache; use lru::LruCache;
use rand::Rng; use skia_safe::{font::Edging, Data, Font, FontHinting, FontMgr, FontStyle, Typeface};
use skribo::{FontCollection, FontFamily};
#[cfg(any(feature = "embed-fonts", test))] use super::swash_font::SwashFont;
use super::caching_shaper::Asset;
use super::extended_font_family::*;
cfg_if! { #[derive(RustEmbed)]
if #[cfg(target_os = "windows")] { #[folder = "assets/fonts/"]
pub const SYSTEM_DEFAULT_FONT: &str = "Consolas"; pub struct Asset;
pub const SYSTEM_SYMBOL_FONT: &str = "Segoe UI Symbol";
pub const SYSTEM_EMOJI_FONT: &str = "Segoe UI Emoji";
} else if #[cfg(target_os = "linux")] {
pub const SYSTEM_DEFAULT_FONT: &str = "Noto Sans Mono";
pub const SYSTEM_SYMBOL_FONT: &str = "Noto Sans Mono";
pub const SYSTEM_EMOJI_FONT: &str = "Noto Color Emoji";
} else if #[cfg(target_os = "macos")] {
pub const SYSTEM_DEFAULT_FONT: &str = "Menlo";
pub const SYSTEM_SYMBOL_FONT: &str = "Apple Symbols";
pub const SYSTEM_EMOJI_FONT: &str = "Apple Color Emoji";
}
}
pub const EXTRA_SYMBOL_FONT: &str = "Extra Symbols.otf"; const DEFAULT_FONT: &str = "FiraCode-Regular.ttf";
pub const MISSING_GLYPH_FONT: &str = "Missing Glyphs.otf";
pub struct FontLoader { pub struct FontPair {
cache: LruCache<String, ExtendedFontFamily>, pub skia_font: Font,
source: SystemSource, pub swash_font: SwashFont,
random_font_name: Option<String>,
} }
impl FontLoader { impl FontPair {
pub fn new() -> FontLoader { fn new(mut skia_font: Font) -> Option<FontPair> {
FontLoader { skia_font.set_subpixel(true);
cache: LruCache::new(10), skia_font.set_hinting(FontHinting::Full);
source: SystemSource::new(), skia_font.set_edging(Edging::SubpixelAntiAlias);
random_font_name: None,
}
}
fn get(&mut self, font_name: &str) -> Option<ExtendedFontFamily> {
self.cache.get(&String::from(font_name)).cloned()
}
#[cfg(any(feature = "embed-fonts", test))] let (font_data, index) = skia_font.typeface().unwrap().to_font_data().unwrap();
fn load_from_asset(&mut self, font_name: &str) -> Option<ExtendedFontFamily> { let swash_font = SwashFont::from_data(font_data, index)?;
use font_kit::font::Font;
use skribo::FontRef as SkriboFont;
let mut family = ExtendedFontFamily::new();
if let Some(font) = Asset::get(font_name) Some(Self {
.and_then(|font_data| Font::from_bytes(font_data.to_vec().into(), 0).ok()) skia_font,
{ swash_font,
family.add_font(SkriboFont::new(font)); })
self.cache.put(String::from(font_name), family);
self.get(font_name)
} else {
None
}
} }
}
#[cfg(not(any(feature = "embed-fonts", test)))] impl PartialEq for FontPair {
fn load_from_asset(&self, font_name: &str) -> Option<ExtendedFontFamily> { fn eq(&self, other: &Self) -> bool {
log::warn!( self.swash_font.key == other.swash_font.key
"Tried to load {} from assets but build didn't include embed-fonts feature",
font_name
);
None
} }
}
fn load(&mut self, font_name: &str) -> Option<ExtendedFontFamily> { pub struct FontLoader {
let handle = match self.source.select_family_by_name(font_name) { font_mgr: FontMgr,
Ok(it) => it, cache: LruCache<FontKey, Arc<FontPair>>,
_ => return None, font_size: f32,
}; }
if !handle.is_empty() { #[derive(Debug, Hash, PartialEq, Eq, Clone)]
let family = ExtendedFontFamily::from(handle); pub enum FontKey {
self.cache.put(String::from(font_name), family); Default,
self.get(font_name) Name(String),
} else { Character(char),
None }
}
}
fn get_random_system_font_family(&mut self) -> Option<ExtendedFontFamily> { impl From<&str> for FontKey {
if let Some(font) = self.random_font_name.clone() { fn from(string: &str) -> FontKey {
self.get(&font) let string = string.to_string();
} else { FontKey::Name(string)
let font_names = self.source.all_families().expect("fonts exist");
let n = rand::thread_rng().gen::<usize>() % font_names.len();
let font_name = &font_names[n];
self.random_font_name = Some(font_name.clone());
self.load(&font_name)
}
} }
}
pub fn get_or_load(&mut self, font_name: &str) -> Option<ExtendedFontFamily> { impl From<&String> for FontKey {
if let Some(cached) = self.get(font_name) { fn from(string: &String) -> FontKey {
Some(cached) let string = string.to_owned();
} else if let Some(loaded) = self.load(font_name) { FontKey::Name(string)
Some(loaded)
} else {
self.load_from_asset(font_name)
}
} }
}
pub fn build_collection_by_font_name( impl From<String> for FontKey {
&mut self, fn from(string: String) -> FontKey {
fallback_list: &[String], FontKey::Name(string)
properties: Properties,
) -> FontCollection {
let mut collection = FontCollection::new();
let gui_fonts = fallback_list
.iter()
.map(|fallback_item| fallback_item.as_ref())
.chain(iter::once(SYSTEM_DEFAULT_FONT));
for font_name in gui_fonts {
if let Some(family) = self.get_or_load(font_name) {
if let Some(font) = family.get(properties) {
collection.add_family(FontFamily::new_from_font(font.clone()));
}
}
}
for font in &[
SYSTEM_SYMBOL_FONT,
SYSTEM_EMOJI_FONT,
EXTRA_SYMBOL_FONT,
MISSING_GLYPH_FONT,
] {
if let Some(family) = self.get_or_load(font) {
collection.add_family(FontFamily::from(family));
}
}
if self.cache.is_empty() {
let font_family = self.get_random_system_font_family();
collection.add_family(FontFamily::from(font_family.expect("font family loaded")));
}
collection
} }
} }
#[cfg(test)] impl From<char> for FontKey {
mod test { fn from(character: char) -> FontKey {
use font_kit::{ FontKey::Character(character)
font::Font,
properties::{Properties, Stretch, Style, Weight},
};
use skribo::FontRef as SkriboFont;
use super::*;
use crate::renderer::fonts::utils::*;
const PROPERTIES1: Properties = Properties {
weight: Weight::NORMAL,
style: Style::Normal,
stretch: Stretch::NORMAL,
};
const PROPERTIES2: Properties = Properties {
weight: Weight::BOLD,
style: Style::Normal,
stretch: Stretch::NORMAL,
};
const PROPERTIES3: Properties = Properties {
weight: Weight::NORMAL,
style: Style::Italic,
stretch: Stretch::NORMAL,
};
const PROPERTIES4: Properties = Properties {
weight: Weight::BOLD,
style: Style::Italic,
stretch: Stretch::NORMAL,
};
fn dummy_font() -> SkriboFont {
SkriboFont::new(
Asset::get(EXTRA_SYMBOL_FONT)
.and_then(|font_data| Font::from_bytes(font_data.to_vec().into(), 0).ok())
.unwrap(),
)
} }
}
#[test] impl FontLoader {
fn test_build_properties() { pub fn new(font_size: f32) -> FontLoader {
assert_eq!(build_properties(false, false), PROPERTIES1); FontLoader {
assert_eq!(build_properties(true, false), PROPERTIES2); font_mgr: FontMgr::new(),
assert_eq!(build_properties(false, true), PROPERTIES3); cache: LruCache::new(10),
assert_eq!(build_properties(true, true), PROPERTIES4); font_size,
}
} }
#[test] fn load(&mut self, font_key: FontKey) -> Option<FontPair> {
fn test_load_from_asset() { match font_key {
let mut loader = FontLoader::new(); FontKey::Default => {
let default_font_data = Asset::get(DEFAULT_FONT).unwrap();
let font_family = loader.load_from_asset(""); let data = Data::new_copy(&default_font_data);
assert!(font_family.is_none()); let typeface = Typeface::from_data(data, 0).unwrap();
FontPair::new(Font::from_typeface(typeface, self.font_size))
let font = dummy_font(); }
let mut eft = ExtendedFontFamily::new(); FontKey::Name(name) => {
eft.add_font(font.clone()); let font_style = FontStyle::normal();
let font_family = loader.load_from_asset(EXTRA_SYMBOL_FONT); let typeface = self.font_mgr.match_family_style(name, font_style)?;
let result = font_family.unwrap().fonts.first().unwrap().font.full_name(); FontPair::new(Font::from_typeface(typeface, self.font_size))
assert_eq!(&result, &eft.fonts.first().unwrap().font.full_name()); }
FontKey::Character(character) => {
assert_eq!( let font_style = FontStyle::normal();
&result, let typeface = self.font_mgr.match_family_style_character(
&loader "",
.cache font_style,
.get(&EXTRA_SYMBOL_FONT.to_string()) &[],
.unwrap() character as i32,
.fonts )?;
.first() FontPair::new(Font::from_typeface(typeface, self.font_size))
.unwrap() }
.font }
.full_name()
);
} }
#[test] pub fn get_or_load(&mut self, font_key: FontKey) -> Option<Arc<FontPair>> {
fn test_load() { if let Some(cached) = self.cache.get(&font_key) {
let mut loader = FontLoader::new(); return Some(cached.clone());
let junk_text = "uhasiudhaiudshiaushd"; }
let font_family = loader.load(junk_text);
assert!(font_family.is_none());
#[cfg(target_os = "linux")] let loaded_font = self.load(font_key.clone())?;
const SYSTEM_DEFAULT_FONT: &str = "monospace";
let font_family = loader.load(SYSTEM_DEFAULT_FONT); let font_arc = Arc::new(loaded_font);
let result = font_family.unwrap().fonts.first().unwrap().font.full_name();
assert_eq!(
&result,
&loader
.cache
.get(&SYSTEM_DEFAULT_FONT.to_string())
.unwrap()
.fonts
.first()
.unwrap()
.font
.full_name()
);
}
#[test] self.cache.put(font_key, font_arc.clone());
fn test_get_random_system_font() {
let mut loader = FontLoader::new();
let font_family = loader.get_random_system_font_family(); Some(font_arc)
let font_name = loader.random_font_name.unwrap();
let result = font_family.unwrap().fonts.first().unwrap().font.full_name();
assert_eq!(
&result,
&loader
.cache
.get(&font_name)
.unwrap()
.fonts
.first()
.unwrap()
.font
.full_name()
);
} }
} }

@ -1,29 +1,16 @@
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct FontOptions { pub struct FontOptions {
previous_guifont_setting: Option<String>, guifont_setting: Option<String>,
pub fallback_list: Vec<String>, pub fallback_list: Vec<String>,
pub size: f32, pub size: f32,
} }
impl FontOptions { impl FontOptions {
pub fn new(name: String, size: f32) -> FontOptions { pub fn parse(guifont_setting: &str, default_size: f32) -> Option<FontOptions> {
FontOptions { let mut fallback_list = None;
previous_guifont_setting: None, let mut size = default_size;
fallback_list: vec![name],
size,
}
}
pub fn update(self: &mut FontOptions, guifont_setting: &str) -> bool {
if self.previous_guifont_setting.is_some()
&& guifont_setting == self.previous_guifont_setting.as_ref().unwrap()
{
return false;
}
self.previous_guifont_setting = Some(guifont_setting.to_string());
let mut parts = guifont_setting.split(':').filter(|part| !part.is_empty()); let mut parts = guifont_setting.split(':').filter(|part| !part.is_empty());
let mut updated = false;
if let Some(parts) = parts.next() { if let Some(parts) = parts.next() {
let parsed_fallback_list: Vec<String> = parts let parsed_fallback_list: Vec<String> = parts
@ -32,23 +19,34 @@ impl FontOptions {
.map(|fallback| fallback.to_string()) .map(|fallback| fallback.to_string())
.collect(); .collect();
if !parsed_fallback_list.is_empty() && self.fallback_list != parsed_fallback_list { if !parsed_fallback_list.is_empty() {
self.fallback_list = parsed_fallback_list; fallback_list = Some(parsed_fallback_list);
updated = true;
} }
} }
for part in parts { for part in parts {
if part.starts_with('h') && part.len() > 1 { if part.starts_with('h') && part.len() > 1 {
if let Ok(size) = part[1..].parse::<f32>() { if let Ok(parsed_size) = part[1..].parse::<f32>() {
if (self.size - size).abs() > std::f32::EPSILON { size = parsed_size
self.size = size;
updated = true;
}
} }
} }
} }
updated fallback_list.map(|fallback_list| FontOptions {
guifont_setting: Some(guifont_setting.to_string()),
fallback_list,
size,
})
}
}
impl PartialEq for FontOptions {
fn eq(&self, other: &Self) -> bool {
if self.guifont_setting.is_some() && self.guifont_setting == other.guifont_setting {
return true;
}
self.fallback_list == other.fallback_list
&& (self.size - other.size).abs() < std::f32::EPSILON
} }
} }

@ -1,5 +1,4 @@
pub mod caching_shaper; pub mod caching_shaper;
mod extended_font_family; mod font_loader;
mod font_loader; mod font_options;
mod font_options; mod swash_font;
mod utils;

@ -0,0 +1,23 @@
use swash::{CacheKey, FontRef};
pub struct SwashFont {
data: Vec<u8>,
offset: u32,
pub key: CacheKey,
}
impl SwashFont {
pub fn from_data(data: Vec<u8>, index: usize) -> Option<Self> {
let font = FontRef::from_index(&data, index)?;
let (offset, key) = (font.offset, font.key);
Some(Self { data, offset, key })
}
pub fn as_ref(&self) -> FontRef {
FontRef {
data: &self.data,
offset: self.offset,
key: self.key,
}
}
}

@ -1,24 +0,0 @@
use font_kit::properties::{Properties, Stretch, Style, Weight};
use skribo::FontRef as SkriboFont;
use skulpin::skia_safe::{Data, Font as SkiaFont, Typeface};
pub fn build_skia_font_from_skribo_font(
skribo_font: &SkriboFont,
base_size: f32,
) -> Option<SkiaFont> {
let font_data = skribo_font.font.copy_font_data()?;
let skia_data = Data::new_copy(&font_data[..]);
let typeface = Typeface::from_data(skia_data, None)?;
Some(SkiaFont::from_typeface(typeface, base_size))
}
pub fn build_properties(bold: bool, italic: bool) -> Properties {
let weight = if bold { Weight::BOLD } else { Weight::NORMAL };
let style = if italic { Style::Italic } else { Style::Normal };
Properties {
weight,
style,
stretch: Stretch::NORMAL,
}
}

@ -2,9 +2,8 @@ use std::collections::HashMap;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::sync::Arc; use std::sync::Arc;
use log::{error, trace, warn}; use log::{error, trace};
use skulpin::skia_safe::{colors, dash_path_effect, BlendMode, Canvas, Color, Paint, Rect}; use skia_safe::{colors, dash_path_effect, BlendMode, Canvas, Color, Paint, Rect};
use skulpin::CoordinateSystemHelper;
pub mod animation_utils; pub mod animation_utils;
pub mod cursor_renderer; pub mod cursor_renderer;
@ -19,8 +18,9 @@ use crate::editor::{Colors, DrawCommand, Style, WindowDrawCommand};
use crate::settings::*; use crate::settings::*;
use cursor_renderer::CursorRenderer; use cursor_renderer::CursorRenderer;
#[derive(Clone, SettingGroup)] #[derive(SettingGroup)]
#[setting_prefix = "window"] #[setting_prefix = "window"]
#[derive(Clone)]
pub struct RendererSettings { pub struct RendererSettings {
position_animation_length: f32, position_animation_length: f32,
scroll_animation_length: f32, scroll_animation_length: f32,
@ -47,8 +47,8 @@ pub struct Renderer {
pub paint: Paint, pub paint: Paint,
pub shaper: CachingShaper, pub shaper: CachingShaper,
pub default_style: Arc<Style>, pub default_style: Arc<Style>,
pub font_width: f32, pub font_width: u64,
pub font_height: f32, pub font_height: u64,
pub window_regions: Vec<WindowDrawDetails>, pub window_regions: Vec<WindowDrawDetails>,
pub batched_draw_command_receiver: Receiver<Vec<DrawCommand>>, pub batched_draw_command_receiver: Receiver<Vec<DrawCommand>>,
} }
@ -64,7 +64,7 @@ impl Renderer {
let mut shaper = CachingShaper::new(); let mut shaper = CachingShaper::new();
let (font_width_raw, font_height_raw) = shaper.font_base_dimensions(); let (font_width_raw, font_height_raw) = shaper.font_base_dimensions();
let font_width = font_width_raw; let font_width = font_width_raw;
let font_height = font_height_raw.ceil(); let font_height = font_height_raw;
let default_style = Arc::new(Style::new(Colors::new( let default_style = Arc::new(Style::new(Colors::new(
Some(colors::WHITE), Some(colors::WHITE),
Some(colors::BLACK), Some(colors::BLACK),
@ -90,26 +90,21 @@ impl Renderer {
if self.shaper.update_font(guifont_setting) { if self.shaper.update_font(guifont_setting) {
let (font_width, font_height) = self.shaper.font_base_dimensions(); let (font_width, font_height) = self.shaper.font_base_dimensions();
self.font_width = font_width; self.font_width = font_width;
self.font_height = font_height.ceil(); self.font_height = font_height;
} }
} }
fn compute_text_region(&self, grid_pos: (u64, u64), cell_width: u64) -> Rect { fn compute_text_region(&self, grid_pos: (u64, u64), cell_width: u64) -> Rect {
let (grid_x, grid_y) = grid_pos; let (grid_x, grid_y) = grid_pos;
let x = grid_x as f32 * self.font_width; let x = grid_x * self.font_width;
let y = grid_y as f32 * self.font_height; let y = grid_y * self.font_height;
let width = cell_width as f32 * self.font_width as f32; let width = cell_width * self.font_width;
let height = self.font_height as f32; let height = self.font_height;
Rect::new(x, y, x + width, y + height) Rect::new(x as f32, y as f32, (x + width) as f32, (y + height) as f32)
} }
fn get_default_background(&self) -> Color { fn get_default_background(&self) -> Color {
self.default_style self.default_style.colors.background.unwrap().to_color()
.colors
.background
.clone()
.unwrap()
.to_color()
} }
fn draw_background( fn draw_background(
@ -132,15 +127,15 @@ impl Renderer {
fn draw_foreground( fn draw_foreground(
&mut self, &mut self,
canvas: &mut Canvas, canvas: &mut Canvas,
text: &str, cells: &[String],
grid_pos: (u64, u64), grid_pos: (u64, u64),
cell_width: u64, cell_width: u64,
style: &Option<Arc<Style>>, style: &Option<Arc<Style>>,
) { ) {
let (grid_x, grid_y) = grid_pos; let (grid_x, grid_y) = grid_pos;
let x = grid_x as f32 * self.font_width; let x = grid_x * self.font_width;
let y = grid_y as f32 * self.font_height; let y = grid_y * self.font_height;
let width = cell_width as f32 * self.font_width; let width = cell_width * self.font_width;
let style = style.as_ref().unwrap_or(&self.default_style); let style = style.as_ref().unwrap_or(&self.default_style);
@ -150,14 +145,9 @@ impl Renderer {
canvas.clip_rect(region, None, Some(false)); canvas.clip_rect(region, None, Some(false));
self.paint.set_blend_mode(BlendMode::Src);
let transparent = Color::from_argb(0, 0, 0, 0);
self.paint.set_color(transparent);
canvas.draw_rect(region, &self.paint);
if style.underline || style.undercurl { if style.underline || style.undercurl {
let line_position = self.shaper.underline_position(); let line_position = self.shaper.underline_position();
let stroke_width = self.shaper.options.size / 10.0; let stroke_width = self.shaper.current_size() / 10.0;
self.paint self.paint
.set_color(style.special(&self.default_style.colors).to_color()); .set_color(style.special(&self.default_style.colors).to_color());
self.paint.set_stroke_width(stroke_width); self.paint.set_stroke_width(stroke_width);
@ -172,30 +162,38 @@ impl Renderer {
} }
canvas.draw_line( canvas.draw_line(
(x, y - line_position + self.font_height), (x as f32, (y - line_position + self.font_height) as f32),
(x + width, y - line_position + self.font_height), (
(x + width) as f32,
(y - line_position + self.font_height) as f32,
),
&self.paint, &self.paint,
); );
} }
let y_adjustment = self.shaper.y_adjustment();
self.paint self.paint
.set_color(style.foreground(&self.default_style.colors).to_color()); .set_color(style.foreground(&self.default_style.colors).to_color());
let text = text.trim_end(); self.paint.set_anti_alias(false);
if !text.is_empty() {
for blob in self for blob in self
.shaper .shaper
.shape_cached(text, style.bold, style.italic) .shape_cached(cells, style.bold, style.italic)
.iter() .iter()
{ {
canvas.draw_text_blob(blob, (x, y), &self.paint); canvas.draw_text_blob(blob, (x as f32, (y + y_adjustment) as f32), &self.paint);
}
} }
if style.strikethrough { if style.strikethrough {
let line_position = region.center_y(); let line_position = region.center_y();
self.paint self.paint
.set_color(style.special(&self.default_style.colors).to_color()); .set_color(style.special(&self.default_style.colors).to_color());
canvas.draw_line((x, line_position), (x + width, line_position), &self.paint); canvas.draw_line(
(x as f32, line_position),
((x + width) as f32, line_position),
&self.paint,
);
} }
canvas.restore(); canvas.restore();
@ -207,7 +205,6 @@ impl Renderer {
draw_command: DrawCommand, draw_command: DrawCommand,
scaling: f32, scaling: f32,
) { ) {
warn!("{:?}", &draw_command);
match draw_command { match draw_command {
DrawCommand::Window { DrawCommand::Window {
grid_id, grid_id,
@ -228,10 +225,9 @@ impl Renderer {
.. ..
} = command } = command
{ {
warn!("Created window {}", grid_id);
let new_window = RenderedWindow::new( let new_window = RenderedWindow::new(
root_canvas, root_canvas,
&self, self,
grid_id, grid_id,
(grid_left as f32, grid_top as f32).into(), (grid_left as f32, grid_top as f32).into(),
width, width,
@ -259,22 +255,18 @@ impl Renderer {
} }
} }
pub fn draw_frame( #[allow(clippy::needless_collect)]
&mut self, pub fn draw_frame(&mut self, root_canvas: &mut Canvas, dt: f32, scaling: f32) -> bool {
root_canvas: &mut Canvas, trace!("Drawing Frame");
coordinate_system_helper: &CoordinateSystemHelper,
dt: f32,
scaling: f32,
) -> bool {
trace!("Rendering");
let mut font_changed = false; let mut font_changed = false;
let draw_commands: Vec<DrawCommand> = self let draw_commands: Vec<_> = self
.batched_draw_command_receiver .batched_draw_command_receiver
.try_iter() // Iterator of Vec of DrawCommand .try_iter() // Iterator of Vec of DrawCommand
.map(|batch| batch.into_iter()) // Iterator of Iterator of DrawCommand .map(|batch| batch.into_iter()) // Iterator of Iterator of DrawCommand
.flatten() // Iterator of DrawCommand .flatten() // Iterator of DrawCommand
.collect(); // Vec of DrawCommand .collect();
for draw_command in draw_commands.into_iter() { for draw_command in draw_commands.into_iter() {
if let DrawCommand::FontChanged(_) = draw_command { if let DrawCommand::FontChanged(_) = draw_command {
font_changed = true; font_changed = true;
@ -282,24 +274,18 @@ impl Renderer {
self.handle_draw_command(root_canvas, draw_command, scaling); self.handle_draw_command(root_canvas, draw_command, scaling);
} }
root_canvas.clear( root_canvas.clear(self.default_style.colors.background.unwrap().to_color());
self.default_style
.colors
.background
.clone()
.unwrap()
.to_color(),
);
root_canvas.save(); root_canvas.save();
root_canvas.reset_matrix();
root_canvas.scale((1.0 / scaling, 1.0 / scaling));
if let Some(root_window) = self.rendered_windows.get(&1) { if let Some(root_window) = self.rendered_windows.get(&1) {
let clip_rect = root_window.pixel_region(self.font_width, self.font_height); let clip_rect = root_window.pixel_region(self.font_width, self.font_height);
root_canvas.clip_rect(&clip_rect, None, Some(false)); root_canvas.clip_rect(&clip_rect, None, Some(false));
} }
coordinate_system_helper.use_logical_coordinates(root_canvas);
let default_background = self.get_default_background(); let default_background = self.get_default_background();
let font_width = self.font_width; let font_width = self.font_width;
let font_height = self.font_height; let font_height = self.font_height;
@ -312,12 +298,17 @@ impl Renderer {
.rendered_windows .rendered_windows
.values_mut() .values_mut()
.filter(|window| !window.hidden) .filter(|window| !window.hidden)
.partition(|window| !window.floating); .partition(|window| window.floating_order.is_none());
root_windows root_windows
.sort_by(|window_a, window_b| window_a.id.partial_cmp(&window_b.id).unwrap()); .sort_by(|window_a, window_b| window_a.id.partial_cmp(&window_b.id).unwrap());
floating_windows floating_windows.sort_by(|window_a, window_b| {
.sort_by(|window_a, window_b| window_a.id.partial_cmp(&window_b.id).unwrap()); window_a
.floating_order
.unwrap()
.partial_cmp(&window_b.floating_order.unwrap())
.unwrap()
});
root_windows root_windows
.into_iter() .into_iter()
@ -341,8 +332,12 @@ impl Renderer {
.collect(); .collect();
let windows = &self.rendered_windows; let windows = &self.rendered_windows;
self.cursor_renderer self.cursor_renderer.update_cursor_destination(
.update_cursor_destination(font_width, font_height, windows, &self.current_mode); font_width,
font_height,
windows,
&self.current_mode,
);
self.cursor_renderer.draw( self.cursor_renderer.draw(
&self.default_style.colors, &self.default_style.colors,

@ -1,10 +1,10 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use skulpin::skia_safe::canvas::{SaveLayerRec, SrcRectConstraint}; use skia_safe::canvas::{SaveLayerRec, SrcRectConstraint};
use skulpin::skia_safe::gpu::SurfaceOrigin; use skia_safe::gpu::SurfaceOrigin;
use skulpin::skia_safe::{ use skia_safe::{
image_filters::blur, BlendMode, Budgeted, Canvas, Color, Image, ImageInfo, Paint, Point, Rect, image_filters::blur, BlendMode, Budgeted, Canvas, Color, Image, ImageInfo, Paint, Point, Rect,
Surface, SamplingOptions, Surface, SurfaceProps, SurfacePropsFlags,
}; };
use super::animation_utils::*; use super::animation_utils::*;
@ -14,11 +14,11 @@ use crate::redraw_scheduler::REDRAW_SCHEDULER;
fn build_window_surface( fn build_window_surface(
parent_canvas: &mut Canvas, parent_canvas: &mut Canvas,
pixel_width: i32, pixel_width: u64,
pixel_height: i32, pixel_height: u64,
) -> Surface { ) -> Surface {
let dimensions = (pixel_width, pixel_height); let dimensions = (pixel_width as i32, pixel_height as i32);
let mut context = parent_canvas.gpu_context().unwrap(); let mut context = parent_canvas.recording_context().unwrap();
let budgeted = Budgeted::Yes; let budgeted = Budgeted::Yes;
let parent_image_info = parent_canvas.image_info(); let parent_image_info = parent_canvas.image_info();
let image_info = ImageInfo::new( let image_info = ImageInfo::new(
@ -28,13 +28,15 @@ fn build_window_surface(
parent_image_info.color_space(), parent_image_info.color_space(),
); );
let surface_origin = SurfaceOrigin::TopLeft; let surface_origin = SurfaceOrigin::TopLeft;
// subpixel layout (should be configurable/obtained from fontconfig)
let props = SurfaceProps::new(SurfacePropsFlags::default(), skia_safe::PixelGeometry::RGBH);
Surface::new_render_target( Surface::new_render_target(
&mut context, &mut context,
budgeted, budgeted,
&image_info, &image_info,
None, None,
surface_origin, surface_origin,
None, Some(&props),
None, None,
) )
.expect("Could not create surface") .expect("Could not create surface")
@ -47,59 +49,35 @@ fn build_window_surface_with_grid_size(
grid_height: u64, grid_height: u64,
scaling: f32, scaling: f32,
) -> Surface { ) -> Surface {
let pixel_width = (grid_width as f32 * renderer.font_width / scaling) as i32; let pixel_width = ((grid_width * renderer.font_width) as f32 / scaling) as u64;
let pixel_height = (grid_height as f32 * renderer.font_height / scaling) as i32; let pixel_height = ((grid_height * renderer.font_height) as f32 / scaling) as u64;
build_window_surface(parent_canvas, pixel_width, pixel_height) let mut surface = build_window_surface(parent_canvas, pixel_width, pixel_height);
}
fn build_background_window_surface(
parent_canvas: &mut Canvas,
renderer: &Renderer,
grid_width: u64,
grid_height: u64,
scaling: f32,
) -> Surface {
let mut surface = build_window_surface_with_grid_size(
parent_canvas,
renderer,
grid_width,
grid_height,
scaling,
);
let canvas = surface.canvas(); let canvas = surface.canvas();
canvas.clear(renderer.get_default_background()); canvas.clear(renderer.get_default_background());
surface surface
} }
pub struct SnapshotPair { pub struct LocatedSnapshot {
background: Image, image: Image,
foreground: Image, top_line: u64,
top_line: f32,
} }
pub struct SurfacePair { pub struct LocatedSurface {
background: Surface, surface: Surface,
foreground: Surface, pub top_line: u64,
pub top_line: f32,
} }
impl SurfacePair { impl LocatedSurface {
fn new( fn new(
parent_canvas: &mut Canvas, parent_canvas: &mut Canvas,
renderer: &Renderer, renderer: &Renderer,
grid_width: u64, grid_width: u64,
grid_height: u64, grid_height: u64,
top_line: f32, top_line: u64,
scaling: f32, scaling: f32,
) -> SurfacePair { ) -> LocatedSurface {
let background = build_background_window_surface( let surface = build_window_surface_with_grid_size(
parent_canvas,
renderer,
grid_width,
grid_height,
scaling,
);
let foreground = build_window_surface_with_grid_size(
parent_canvas, parent_canvas,
renderer, renderer,
grid_width, grid_width,
@ -107,31 +85,25 @@ impl SurfacePair {
scaling, scaling,
); );
SurfacePair { LocatedSurface { surface, top_line }
background,
foreground,
top_line,
}
} }
fn snapshot(&mut self) -> SnapshotPair { fn snapshot(&mut self) -> LocatedSnapshot {
let background = self.background.image_snapshot(); let image = self.surface.image_snapshot();
let foreground = self.foreground.image_snapshot(); LocatedSnapshot {
SnapshotPair { image,
background,
foreground,
top_line: self.top_line, top_line: self.top_line,
} }
} }
} }
pub struct RenderedWindow { pub struct RenderedWindow {
snapshots: VecDeque<SnapshotPair>, snapshots: VecDeque<LocatedSnapshot>,
pub current_surfaces: SurfacePair, pub current_surface: LocatedSurface,
pub id: u64, pub id: u64,
pub hidden: bool, pub hidden: bool,
pub floating: bool, pub floating_order: Option<u64>,
pub grid_width: u64, pub grid_width: u64,
pub grid_height: u64, pub grid_height: u64,
@ -150,7 +122,7 @@ pub struct RenderedWindow {
pub struct WindowDrawDetails { pub struct WindowDrawDetails {
pub id: u64, pub id: u64,
pub region: Rect, pub region: Rect,
pub floating: bool, pub floating_order: Option<u64>,
} }
impl RenderedWindow { impl RenderedWindow {
@ -163,21 +135,15 @@ impl RenderedWindow {
grid_height: u64, grid_height: u64,
scaling: f32, scaling: f32,
) -> RenderedWindow { ) -> RenderedWindow {
let current_surfaces = SurfacePair::new( let current_surface =
parent_canvas, LocatedSurface::new(parent_canvas, renderer, grid_width, grid_height, 0, scaling);
renderer,
grid_width,
grid_height,
0.0,
scaling,
);
RenderedWindow { RenderedWindow {
snapshots: VecDeque::new(), snapshots: VecDeque::new(),
current_surfaces, current_surface,
id, id,
hidden: false, hidden: false,
floating: false, floating_order: None,
grid_width, grid_width,
grid_height, grid_height,
@ -194,14 +160,14 @@ impl RenderedWindow {
} }
} }
pub fn pixel_region(&self, font_width: f32, font_height: f32) -> Rect { pub fn pixel_region(&self, font_width: u64, font_height: u64) -> Rect {
let current_pixel_position = Point::new( let current_pixel_position = Point::new(
self.grid_current_position.x * font_width, self.grid_current_position.x * font_width as f32,
self.grid_current_position.y * font_height, self.grid_current_position.y * font_height as f32,
); );
let image_width = (self.grid_width as f32 * font_width) as i32; let image_width = (self.grid_width * font_width) as i32;
let image_height = (self.grid_height as f32 * font_height) as i32; let image_height = (self.grid_height * font_height) as i32;
Rect::from_point_and_size(current_pixel_position, (image_width, image_height)) Rect::from_point_and_size(current_pixel_position, (image_width, image_height))
} }
@ -210,7 +176,7 @@ impl RenderedWindow {
let mut animating = false; let mut animating = false;
{ {
if (self.position_t - 1.0).abs() < std::f32::EPSILON { if 1.0 - self.position_t < std::f32::EPSILON {
// We are at destination, move t out of 0-1 range to stop the animation // We are at destination, move t out of 0-1 range to stop the animation
self.position_t = 2.0; self.position_t = 2.0;
} else { } else {
@ -228,7 +194,7 @@ impl RenderedWindow {
} }
{ {
if (self.scroll_t - 1.0).abs() < std::f32::EPSILON { if 1.0 - self.scroll_t < std::f32::EPSILON {
// We are at destination, move t out of 0-1 range to stop the animation // We are at destination, move t out of 0-1 range to stop the animation
self.scroll_t = 2.0; self.scroll_t = 2.0;
self.snapshots.clear(); self.snapshots.clear();
@ -253,8 +219,8 @@ impl RenderedWindow {
root_canvas: &mut Canvas, root_canvas: &mut Canvas,
settings: &RendererSettings, settings: &RendererSettings,
default_background: Color, default_background: Color,
font_width: f32, font_width: u64,
font_height: f32, font_height: u64,
dt: f32, dt: f32,
) -> WindowDrawDetails { ) -> WindowDrawDetails {
if self.update(settings, dt) { if self.update(settings, dt) {
@ -266,7 +232,7 @@ impl RenderedWindow {
root_canvas.save(); root_canvas.save();
root_canvas.clip_rect(&pixel_region, None, Some(false)); root_canvas.clip_rect(&pixel_region, None, Some(false));
if self.floating && settings.floating_blur { if self.floating_order.is_some() && settings.floating_blur {
let blur = blur((2.0, 2.0), None, None, None).unwrap(); let blur = blur((2.0, 2.0), None, None, None).unwrap();
let save_layer_rec = SaveLayerRec::default() let save_layer_rec = SaveLayerRec::default()
.backdrop(&blur) .backdrop(&blur)
@ -279,79 +245,46 @@ impl RenderedWindow {
// We want each surface to overwrite the one underneath and will use layers to ensure // We want each surface to overwrite the one underneath and will use layers to ensure
// only lower priority surfaces will get clobbered and not the underlying windows // only lower priority surfaces will get clobbered and not the underlying windows
paint.set_blend_mode(BlendMode::Src); paint.set_blend_mode(BlendMode::Src);
paint.set_anti_alias(false);
{ // Save layer so that setting the blend mode doesn't effect the blur
// Save layer so that setting the blend mode doesn't effect the blur root_canvas.save_layer(&SaveLayerRec::default());
root_canvas.save_layer(&SaveLayerRec::default()); let mut a = 255;
let mut a = 255; if self.floating_order.is_some() {
if self.floating { a = (settings.floating_opacity.min(1.0).max(0.0) * 255.0) as u8;
a = (settings.floating_opacity.min(1.0).max(0.0) * 255.0) as u8;
}
paint.set_color(default_background.with_a(a));
root_canvas.draw_rect(pixel_region, &paint);
paint.set_color(Color::from_argb(a, 255, 255, 255));
// Draw background scrolling snapshots
for snapshot_pair in self.snapshots.iter_mut().rev() {
let scroll_offset =
snapshot_pair.top_line * font_height - self.current_scroll * font_height;
let background_snapshot = &mut snapshot_pair.background;
root_canvas.draw_image_rect(
background_snapshot,
None,
pixel_region.with_offset((0.0, scroll_offset)),
&paint,
);
}
// Draw background
let scroll_offset =
self.current_surfaces.top_line * font_height - self.current_scroll * font_height;
let background_snapshot = self.current_surfaces.background.image_snapshot();
root_canvas.draw_image_rect(
background_snapshot,
None,
pixel_region.with_offset((0.0, scroll_offset)),
&paint,
);
root_canvas.restore();
} }
{ paint.set_color(default_background.with_a(a));
paint.set_color(Color::WHITE); root_canvas.draw_rect(pixel_region, &paint);
// Save layer so that text may safely overwrite images underneath
root_canvas.save_layer(&SaveLayerRec::default());
// Draw foreground scrolling snapshots
for snapshot_pair in self.snapshots.iter_mut().rev() {
let scroll_offset =
snapshot_pair.top_line * font_height - self.current_scroll * font_height;
let foreground_snapshot = &mut snapshot_pair.foreground;
root_canvas.draw_image_rect(
foreground_snapshot,
None,
pixel_region.with_offset((0.0, scroll_offset)),
&paint,
);
}
// Draw foreground paint.set_color(Color::from_argb(a, 255, 255, 255));
let scroll_offset =
self.current_surfaces.top_line * font_height - self.current_scroll * font_height; // Draw scrolling snapshots
let foreground_snapshot = self.current_surfaces.foreground.image_snapshot(); for snapshot in self.snapshots.iter_mut().rev() {
let scroll_offset = (snapshot.top_line * font_height) as f32
- (self.current_scroll * font_height as f32);
let image = &mut snapshot.image;
root_canvas.draw_image_rect( root_canvas.draw_image_rect(
foreground_snapshot, image,
None, None,
pixel_region.with_offset((0.0, scroll_offset)), pixel_region.with_offset((0.0, scroll_offset as f32)),
&paint, &paint,
); );
root_canvas.restore();
} }
// Draw current surface
let scroll_offset = (self.current_surface.top_line * font_height) as f32
- (self.current_scroll * font_height as f32);
let snapshot = self.current_surface.surface.image_snapshot();
root_canvas.draw_image_rect(
snapshot,
None,
pixel_region.with_offset((0.0, scroll_offset as f32)),
&paint,
);
root_canvas.restore();
if self.floating { if self.floating_order.is_some() {
root_canvas.restore(); root_canvas.restore();
} }
@ -360,7 +293,7 @@ impl RenderedWindow {
WindowDrawDetails { WindowDrawDetails {
id: self.id, id: self.id,
region: pixel_region, region: pixel_region,
floating: self.floating, floating_order: self.floating_order,
} }
} }
@ -376,7 +309,7 @@ impl RenderedWindow {
grid_top, grid_top,
width: grid_width, width: grid_width,
height: grid_height, height: grid_height,
floating, floating_order,
} => { } => {
let new_destination: Point = (grid_left as f32, grid_top as f32).into(); let new_destination: Point = (grid_left as f32, grid_top as f32).into();
@ -386,54 +319,36 @@ impl RenderedWindow {
{ {
self.position_t = 0.0; // Reset animation as we have a new destination. self.position_t = 0.0; // Reset animation as we have a new destination.
self.grid_start_position = self.grid_current_position; self.grid_start_position = self.grid_current_position;
self.grid_destination = new_destination;
} else { } else {
// We don't want to animate since the window is animating out of the start location, // We don't want to animate since the window is animating out of the start location,
// so we set t to 2.0 to stop animations. // so we set t to 2.0 to stop animations.
self.position_t = 2.0; self.position_t = 2.0;
self.grid_start_position = new_destination; self.grid_start_position = new_destination;
self.grid_destination = new_destination;
} }
self.grid_destination = new_destination;
} }
if grid_width != self.grid_width || grid_height != self.grid_height { if grid_width != self.grid_width || grid_height != self.grid_height {
{ let mut old_surface = self.current_surface.surface;
let mut old_background = self.current_surfaces.background; self.current_surface.surface = build_window_surface_with_grid_size(
self.current_surfaces.background = build_background_window_surface( old_surface.canvas(),
old_background.canvas(), renderer,
&renderer, grid_width,
grid_width, grid_height,
grid_height, scaling,
scaling, );
); old_surface.draw(
old_background.draw( self.current_surface.surface.canvas(),
self.current_surfaces.background.canvas(), (0.0, 0.0),
(0.0, 0.0), SamplingOptions::default(),
None, None,
); );
}
{
let mut old_foreground = self.current_surfaces.foreground;
self.current_surfaces.foreground = build_window_surface_with_grid_size(
old_foreground.canvas(),
&renderer,
grid_width,
grid_height,
scaling,
);
old_foreground.draw(
self.current_surfaces.foreground.canvas(),
(0.0, 0.0),
None,
);
}
self.grid_width = grid_width; self.grid_width = grid_width;
self.grid_height = grid_height; self.grid_height = grid_height;
} }
self.floating = floating; self.floating_order = floating_order;
if self.hidden { if self.hidden {
self.hidden = false; self.hidden = false;
@ -442,30 +357,21 @@ impl RenderedWindow {
self.grid_destination = new_destination; self.grid_destination = new_destination;
} }
} }
WindowDrawCommand::Cell { WindowDrawCommand::Cells {
text, cells,
cell_width,
window_left, window_left,
window_top, window_top,
width,
style, style,
} => { } => {
let grid_position = (window_left, window_top); let grid_position = (window_left, window_top);
{ let canvas = self.current_surface.surface.canvas();
let canvas = self.current_surfaces.background.canvas(); canvas.save();
canvas.save(); canvas.scale((1.0 / scaling, 1.0 / scaling));
canvas.scale((1.0 / scaling, 1.0 / scaling)); renderer.draw_background(canvas, grid_position, width, &style);
renderer.draw_background(canvas, grid_position, cell_width, &style); renderer.draw_foreground(canvas, &cells, grid_position, width, &style);
canvas.restore(); canvas.restore();
}
{
let canvas = self.current_surfaces.foreground.canvas();
canvas.save();
canvas.scale((1.0 / scaling, 1.0 / scaling));
renderer.draw_foreground(canvas, &text, grid_position, cell_width, &style);
canvas.restore();
}
} }
WindowDrawCommand::Scroll { WindowDrawCommand::Scroll {
top, top,
@ -476,64 +382,37 @@ impl RenderedWindow {
cols, cols,
} => { } => {
let scrolled_region = Rect::new( let scrolled_region = Rect::new(
left as f32 * renderer.font_width / scaling, (left * renderer.font_width) as f32 / scaling,
top as f32 * renderer.font_height / scaling, (top * renderer.font_height) as f32 / scaling,
right as f32 * renderer.font_width / scaling, (right * renderer.font_width) as f32 / scaling,
bot as f32 * renderer.font_height / scaling, (bot * renderer.font_height) as f32 / scaling,
); );
let mut translated_region = scrolled_region; let mut translated_region = scrolled_region;
translated_region.offset(( translated_region.offset((
-cols as f32 * renderer.font_width / scaling, (-cols * renderer.font_width as i64) as f32 / scaling,
-rows as f32 * renderer.font_height / scaling, (-rows * renderer.font_height as i64) as f32 / scaling,
)); ));
{ let snapshot = self.current_surface.surface.image_snapshot();
let background_snapshot = self.current_surfaces.background.image_snapshot(); let canvas = self.current_surface.surface.canvas();
let background_canvas = self.current_surfaces.background.canvas();
background_canvas.save();
background_canvas.clip_rect(scrolled_region, None, Some(false));
background_canvas.draw_image_rect(
background_snapshot,
Some((&scrolled_region, SrcRectConstraint::Fast)),
translated_region,
&renderer.paint,
);
background_canvas.restore();
}
{
let foreground_snapshot = self.current_surfaces.foreground.image_snapshot();
let foreground_canvas = self.current_surfaces.foreground.canvas();
foreground_canvas.save(); canvas.save();
foreground_canvas.clip_rect(scrolled_region, None, Some(false)); canvas.clip_rect(scrolled_region, None, Some(false));
foreground_canvas.draw_image_rect( canvas.draw_image_rect(
foreground_snapshot, snapshot,
Some((&scrolled_region, SrcRectConstraint::Fast)), Some((&scrolled_region, SrcRectConstraint::Fast)),
translated_region, translated_region,
&renderer.paint, &renderer.paint,
); );
foreground_canvas.restore(); canvas.restore();
}
} }
WindowDrawCommand::Clear => { WindowDrawCommand::Clear => {
self.current_surfaces.background = build_background_window_surface( self.current_surface.surface = build_window_surface_with_grid_size(
self.current_surfaces.background.canvas(), self.current_surface.surface.canvas(),
&renderer, renderer,
self.grid_width,
self.grid_height,
scaling,
);
self.current_surfaces.foreground = build_window_surface_with_grid_size(
self.current_surfaces.foreground.canvas(),
&renderer,
self.grid_width, self.grid_width,
self.grid_height, self.grid_height,
scaling, scaling,
@ -550,15 +429,15 @@ impl RenderedWindow {
} }
WindowDrawCommand::Hide => self.hidden = true, WindowDrawCommand::Hide => self.hidden = true,
WindowDrawCommand::Viewport { top_line, .. } => { WindowDrawCommand::Viewport { top_line, .. } => {
if (self.current_surfaces.top_line - top_line as f32).abs() > std::f32::EPSILON { if self.current_surface.top_line != top_line as u64 {
let new_snapshot = self.current_surfaces.snapshot(); let new_snapshot = self.current_surface.snapshot();
self.snapshots.push_back(new_snapshot); self.snapshots.push_back(new_snapshot);
if self.snapshots.len() > 5 { if self.snapshots.len() > 5 {
self.snapshots.pop_front(); self.snapshots.pop_front();
} }
self.current_surfaces.top_line = top_line as f32; self.current_surface.top_line = top_line as u64;
// Set new target viewport position and initialize animation timer // Set new target viewport position and initialize animation timer
self.start_scroll = self.current_scroll; self.start_scroll = self.current_scroll;

@ -2,11 +2,9 @@ use std::any::{Any, TypeId};
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
#[cfg(not(test))]
use flexi_logger::{Cleanup, Criterion, Duplicate, Logger, Naming};
mod from_value; mod from_value;
pub use from_value::FromValue; pub use from_value::FromValue;
use log::warn; use log::trace;
use nvim_rs::Neovim; use nvim_rs::Neovim;
use parking_lot::RwLock; use parking_lot::RwLock;
pub use rmpv::Value; pub use rmpv::Value;
@ -34,7 +32,6 @@ type ReaderFunc = fn() -> Value;
// struct except when prompted by an update event from nvim. Otherwise, the settings in Neovide and // struct except when prompted by an update event from nvim. Otherwise, the settings in Neovide and
// nvim will get out of sync. // nvim will get out of sync.
pub struct Settings { pub struct Settings {
pub neovim_arguments: Vec<String>,
settings: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>, settings: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
listeners: RwLock<HashMap<String, UpdateHandlerFunc>>, listeners: RwLock<HashMap<String, UpdateHandlerFunc>>,
readers: RwLock<HashMap<String, ReaderFunc>>, readers: RwLock<HashMap<String, ReaderFunc>>,
@ -42,58 +39,13 @@ pub struct Settings {
impl Settings { impl Settings {
fn new() -> Self { fn new() -> Self {
let mut log_to_file = false;
let neovim_arguments = std::env::args()
.filter(|arg| {
if arg == "--log" {
log_to_file = true;
false
} else {
!(arg.starts_with("--geometry=")
|| arg.starts_with("--remote-tcp=")
|| arg == "--version"
|| arg == "-v"
|| arg == "--help"
|| arg == "-h"
|| arg == "--wsl"
|| arg == "--disowned"
|| arg == "--multiGrid"
|| arg == "--maximized")
}
})
.collect::<Vec<String>>();
#[cfg(not(test))]
Settings::init_logger(log_to_file);
Self { Self {
neovim_arguments,
settings: RwLock::new(HashMap::new()), settings: RwLock::new(HashMap::new()),
listeners: RwLock::new(HashMap::new()), listeners: RwLock::new(HashMap::new()),
readers: RwLock::new(HashMap::new()), readers: RwLock::new(HashMap::new()),
} }
} }
#[cfg(not(test))]
fn init_logger(log_to_file: bool) {
if log_to_file {
Logger::with_env_or_str("neovide")
.duplicate_to_stderr(Duplicate::Error)
.log_to_file()
.rotate(
Criterion::Size(10_000_000),
Naming::Timestamps,
Cleanup::KeepLogFiles(1),
)
.start()
.expect("Could not start logger");
} else {
Logger::with_env_or_str("neovide = error")
.start()
.expect("Could not start logger");
}
}
pub fn set_setting_handlers( pub fn set_setting_handlers(
&self, &self,
property_name: &str, property_name: &str,
@ -122,7 +74,7 @@ impl Settings {
let read_lock = self.settings.read(); let read_lock = self.settings.read();
let boxed = &read_lock let boxed = &read_lock
.get(&TypeId::of::<T>()) .get(&TypeId::of::<T>())
.expect("Trying to retrieve a settings object that doesn't exist"); .expect("Trying to retrieve a settings object that doesn't exist: {:?}");
let value: &T = boxed let value: &T = boxed
.downcast_ref::<T>() .downcast_ref::<T>()
.expect("Attempted to extract a settings object of the wrong type"); .expect("Attempted to extract a settings object of the wrong type");
@ -139,7 +91,7 @@ impl Settings {
self.listeners.read().get(&name).unwrap()(value); self.listeners.read().get(&name).unwrap()(value);
} }
Err(error) => { Err(error) => {
warn!("Initial value load failed for {}: {}", name, error); trace!("Initial value load failed for {}: {}", name, error);
let setting = self.readers.read().get(&name).unwrap()(); let setting = self.readers.read().get(&name).unwrap()();
nvim.set_var(&variable_name, setting).await.ok(); nvim.set_var(&variable_name, setting).await.ok();
} }
@ -188,7 +140,10 @@ mod tests {
use tokio; use tokio;
use super::*; use super::*;
use crate::bridge::{create, create_nvim_command}; use crate::{
bridge::{create, create_nvim_command},
cmd_line::CmdLineSettings,
};
#[derive(Clone)] #[derive(Clone)]
pub struct NeovimHandler(); pub struct NeovimHandler();
@ -287,6 +242,10 @@ mod tests {
let v4: String = format!("neovide_{}", v1); let v4: String = format!("neovide_{}", v1);
let v5: String = format!("neovide_{}", v2); let v5: String = format!("neovide_{}", v2);
//create_nvim_command tries to read from CmdLineSettings.neovim_args
//TODO: this sets a static variable. Can this have side effects on other tests?
SETTINGS.set::<CmdLineSettings>(&CmdLineSettings::default());
let (nvim, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler()) let (nvim, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler())
.await .await
.unwrap_or_explained_panic("Could not locate or start the neovim process"); .unwrap_or_explained_panic("Could not locate or start the neovim process");

@ -1,8 +1,9 @@
use super::KeyboardLayout; use super::KeyboardLayout;
use crate::settings::FromValue; use crate::settings::FromValue;
#[derive(Clone, SettingGroup)] #[derive(SettingGroup)]
#[setting_prefix = "keyboard"] #[setting_prefix = "keyboard"]
#[derive(Clone)]
pub struct KeyboardSettings { pub struct KeyboardSettings {
pub layout: KeyboardLayout, pub layout: KeyboardLayout,
} }

@ -1,34 +1,28 @@
mod keyboard; mod keyboard;
mod settings; mod settings;
#[cfg_attr(feature = "sdl2", path = "sdl2/mod.rs")]
#[cfg_attr(feature = "winit", path = "winit/mod.rs")]
mod window_wrapper; mod window_wrapper;
use crate::{ use crate::{
bridge::UiCommand, bridge::UiCommand,
channel_utils::*,
cmd_line::CmdLineSettings,
editor::{DrawCommand, WindowCommand}, editor::{DrawCommand, WindowCommand},
renderer::Renderer, renderer::Renderer,
settings::SETTINGS,
INITIAL_DIMENSIONS, INITIAL_DIMENSIONS,
}; };
use crossfire::mpsc::TxUnbounded;
use skulpin::LogicalSize;
use std::sync::{atomic::AtomicBool, mpsc::Receiver, Arc}; use std::sync::{atomic::AtomicBool, mpsc::Receiver, Arc};
#[cfg(feature = "sdl2")]
pub use window_wrapper::start_loop;
#[cfg(feature = "winit")]
pub use window_wrapper::start_loop; pub use window_wrapper::start_loop;
pub use settings::*; pub use settings::*;
pub fn window_geometry() -> Result<(u64, u64), String> { pub fn window_geometry() -> Result<(u64, u64), String> {
let prefix = "--geometry="; //TODO: Maybe move this parsing into cmd_line...
SETTINGS
std::env::args() .get::<CmdLineSettings>()
.find(|arg| arg.starts_with(prefix)) .geometry
.map_or(Ok(INITIAL_DIMENSIONS), |arg| { .map_or(Ok(INITIAL_DIMENSIONS), |input| {
let input = &arg[prefix.len()..];
let invalid_parse_err = format!( let invalid_parse_err = format!(
"Invalid geometry: {}\nValid format: <width>x<height>", "Invalid geometry: {}\nValid format: <width>x<height>",
input input
@ -66,7 +60,6 @@ pub fn window_geometry_or_default() -> (u64, u64) {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn windows_fix_dpi() { fn windows_fix_dpi() {
println!("dpi fix applied");
use winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; use winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use winapi::um::winuser::SetProcessDpiAwarenessContext; use winapi::um::winuser::SetProcessDpiAwarenessContext;
unsafe { unsafe {
@ -75,14 +68,15 @@ fn windows_fix_dpi() {
} }
fn handle_new_grid_size( fn handle_new_grid_size(
new_size: LogicalSize, new_size: (u64, u64),
renderer: &Renderer, renderer: &Renderer,
ui_command_sender: &TxUnbounded<UiCommand>, ui_command_sender: &LoggingTx<UiCommand>,
) { ) {
if new_size.width > 0 && new_size.height > 0 { let (new_width, new_height) = new_size;
if new_width > 0 && new_height > 0 {
// Add 1 here to make sure resizing doesn't change the grid size on startup // Add 1 here to make sure resizing doesn't change the grid size on startup
let new_width = ((new_size.width + 1) as f32 / renderer.font_width) as u32; let new_width = ((new_width + 1) / renderer.font_width) as u32;
let new_height = ((new_size.height + 1) as f32 / renderer.font_height) as u32; let new_height = ((new_height + 1) / renderer.font_height) as u32;
ui_command_sender ui_command_sender
.send(UiCommand::Resize { .send(UiCommand::Resize {
width: new_width, width: new_width,
@ -95,16 +89,16 @@ fn handle_new_grid_size(
pub fn create_window( pub fn create_window(
batched_draw_command_receiver: Receiver<Vec<DrawCommand>>, batched_draw_command_receiver: Receiver<Vec<DrawCommand>>,
window_command_receiver: Receiver<WindowCommand>, window_command_receiver: Receiver<WindowCommand>,
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: LoggingTx<UiCommand>,
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
) { ) {
let (width, height) = window_geometry_or_default(); let (width, height) = window_geometry_or_default();
let renderer = Renderer::new(batched_draw_command_receiver); let renderer = Renderer::new(batched_draw_command_receiver);
let logical_size = LogicalSize { let logical_size = (
width: (width as f32 * renderer.font_width) as u32, (width * renderer.font_width) as u64,
height: (height as f32 * renderer.font_height + 1.0) as u32, (height * renderer.font_height + 1) as u64,
}; );
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
windows_fix_dpi(); windows_fix_dpi();

@ -1,18 +0,0 @@
mod qwerty;
use crate::{settings::SETTINGS, window::keyboard::Modifiers, WindowSettings};
use skulpin::sdl2::keyboard::Mod;
pub use qwerty::handle_qwerty_layout;
impl From<Mod> for Modifiers {
fn from(mods: Mod) -> Modifiers {
let iso_layout = SETTINGS.get::<WindowSettings>().iso_layout;
Modifiers {
shift: mods.contains(Mod::LSHIFTMOD) || mods.contains(Mod::RSHIFTMOD),
control: mods.contains(Mod::LCTRLMOD) || mods.contains(Mod::RCTRLMOD),
meta: mods.contains(Mod::LALTMOD) || (!iso_layout && mods.contains(Mod::RALTMOD)),
logo: mods.contains(Mod::LGUIMOD) || mods.contains(Mod::RGUIMOD),
}
}
}

@ -1,184 +0,0 @@
use crate::window::keyboard::{unsupported_key, Token};
use skulpin::sdl2::keyboard::Keycode::{self, *};
/// Maps winit keyboard events to Vim tokens
pub fn handle_qwerty_layout(keycode: Keycode, shift: bool) -> Option<Token<'static>> {
let special = |text| Some(Token::new(text, true, true));
let normal = |text| Some(Token::new(text, false, true));
let partial = |text| Some(Token::new(text, false, false));
match (keycode, shift) {
(Backspace, _) => special("BS"),
(Tab, _) => special("Tab"),
(Return, _) => special("Enter"),
(Escape, _) => special("Esc"),
(Space, _) => normal(" "),
(Exclaim, _) => normal("!"),
(Quotedbl, _) => normal("\""),
(Hash, _) => normal("#"),
(Dollar, _) => normal("$"),
(Percent, _) => normal("%"),
(Ampersand, _) => normal("&"),
(Quote, false) => normal("'"),
(Quote, true) => normal("\""),
(LeftParen, _) => normal("("),
(RightParen, _) => normal(")"),
(Asterisk, _) => normal("*"),
(Plus, _) => normal("+"),
(Comma, false) => normal(","),
(Comma, true) => special("lt"),
(Minus, false) => partial("-"),
(Minus, true) => partial("_"),
(Period, false) => partial("."),
(Period, true) => partial(">"),
(Slash, false) => partial("/"),
(Slash, true) => partial("?"),
(Num0, false) => partial("0"),
(Num0, true) => special(")"),
(Num1, false) => partial("1"),
(Num1, true) => special("!"),
(Num2, false) => partial("2"),
(Num2, true) => partial("@"),
(Num3, false) => partial("3"),
(Num3, true) => partial("#"),
(Num4, false) => partial("4"),
(Num4, true) => partial("$"),
(Num5, false) => partial("5"),
(Num5, true) => partial("%"),
(Num6, false) => partial("6"),
(Num6, true) => partial("^"),
(Num7, false) => partial("7"),
(Num7, true) => partial("&"),
(Num8, false) => partial("8"),
(Num8, true) => partial("*"),
(Num9, false) => partial("9"),
(Num9, true) => partial("("),
(Colon, _) => normal(":"),
(Semicolon, false) => partial(";"),
(Semicolon, true) => partial(":"),
(Less, _) => special("lt"),
(Equals, false) => partial("="),
(Equals, true) => partial("+"),
(Greater, _) => normal("gt"),
(Question, _) => normal("?"),
(At, _) => normal("@"),
(LeftBracket, false) => partial("["),
(LeftBracket, true) => partial("{"),
(Backslash, false) => partial("\\"),
(Backslash, true) => partial("|"),
(RightBracket, false) => partial("]"),
(RightBracket, true) => partial("}"),
(Caret, _) => normal("^"),
(Underscore, _) => normal("_"),
(Backquote, false) => partial("`"),
(Backquote, true) => partial("~"),
(A, _) => normal("a"),
(B, _) => normal("b"),
(C, _) => normal("c"),
(D, _) => normal("d"),
(E, _) => normal("e"),
(F, _) => normal("f"),
(G, _) => normal("g"),
(H, _) => normal("h"),
(I, _) => normal("i"),
(J, _) => normal("j"),
(K, _) => normal("k"),
(L, _) => normal("l"),
(M, _) => normal("m"),
(N, _) => normal("n"),
(O, _) => normal("o"),
(P, _) => normal("p"),
(Q, _) => normal("q"),
(R, _) => normal("r"),
(S, _) => normal("s"),
(T, _) => normal("t"),
(U, _) => normal("u"),
(V, _) => normal("v"),
(W, _) => normal("w"),
(X, _) => normal("x"),
(Y, _) => normal("y"),
(Z, _) => normal("z"),
(Delete, _) => special("Delete"),
(F1, _) => special("F1"),
(F2, _) => special("F2"),
(F3, _) => special("F3"),
(F4, _) => special("F4"),
(F5, _) => special("F5"),
(F6, _) => special("F6"),
(F7, _) => special("F7"),
(F8, _) => special("F8"),
(F9, _) => special("F9"),
(F10, _) => special("F10"),
(F11, _) => special("F11"),
(F12, _) => special("F12"),
(Insert, _) => special("Insert"),
(Home, _) => special("Home"),
(PageUp, _) => special("PageUp"),
(End, _) => special("End"),
(PageDown, _) => special("PageDown"),
(Right, _) => special("Right"),
(Left, _) => special("Left"),
(Down, _) => special("Down"),
(Up, _) => special("Up"),
(KpDivide, _) => special("/"),
(KpMultiply, _) => special("*"),
(KpMinus, _) => special("-"),
(KpPlus, _) => special("+"),
(KpEnter, _) => special("Enter"),
(Kp0, _) => normal("0"),
(Kp1, _) => normal("1"),
(Kp2, _) => normal("2"),
(Kp3, _) => normal("3"),
(Kp4, _) => normal("4"),
(Kp5, _) => normal("5"),
(Kp6, _) => normal("6"),
(Kp7, _) => normal("7"),
(Kp8, _) => normal("8"),
(Kp9, _) => normal("9"),
(KpPeriod, _) => normal("."),
(KpEquals, _) => normal("="),
(F13, _) => special("F13"),
(F14, _) => special("F14"),
(F15, _) => special("F15"),
(F16, _) => special("F16"),
(F17, _) => special("F17"),
(F18, _) => special("F18"),
(F19, _) => special("F19"),
(F20, _) => special("F20"),
(F21, _) => special("F21"),
(F22, _) => special("F22"),
(F23, _) => special("F23"),
(F24, _) => special("F24"),
(KpLeftParen, _) => normal("("),
(KpRightParen, _) => normal("("),
(KpLeftBrace, _) => normal("["),
(KpRightBrace, _) => normal("]"),
(KpTab, _) => special("TAB"),
(KpBackspace, _) => special("BS"),
(KpA, _) => normal("A"),
(KpB, _) => normal("B"),
(KpC, _) => normal("C"),
(KpD, _) => normal("D"),
(KpE, _) => normal("E"),
(KpF, _) => normal("F"),
(KpPower, _) => normal("^"),
(KpPercent, _) => normal("%"),
(KpLess, _) => special("lt"),
(KpGreater, _) => special("gt"),
(KpAmpersand, _) => normal("&"),
(KpVerticalBar, _) => normal("|"),
(KpColon, _) => normal(":"),
(KpHash, _) => normal("#"),
(KpSpace, _) => normal(" "),
(KpAt, _) => normal("@"),
(KpExclam, _) => normal("!"),
(LCtrl, _) => None,
(LShift, _) => None,
(LAlt, _) => None,
(LGui, _) => None,
(RCtrl, _) => None,
(RShift, _) => None,
(RAlt, _) => None,
(RGui, _) => None,
(keycode, _) => unsupported_key(keycode),
}
}

@ -1,525 +0,0 @@
#[macro_use]
mod layouts;
use super::{handle_new_grid_size, keyboard::neovim_keybinding_string, WindowSettings};
use crate::{
bridge::UiCommand, editor::WindowCommand, error_handling::ResultPanicExplanation,
redraw_scheduler::REDRAW_SCHEDULER, renderer::Renderer, settings::SETTINGS,
};
use crossfire::mpsc::TxUnbounded;
use image::load_from_memory_with_format;
use layouts::handle_qwerty_layout;
use skulpin::{
ash::prelude::VkResult,
sdl2::{
self,
event::{Event, WindowEvent},
keyboard::Keycode,
video::FullscreenType,
EventPump, Sdl,
},
CoordinateSystem, LogicalSize, PhysicalSize, PresentMode, Renderer as SkulpinRenderer,
RendererBuilder, Sdl2Window, Window,
};
use std::{
sync::{
atomic::{AtomicBool, Ordering},
mpsc::Receiver,
Arc,
},
thread::sleep,
time::{Duration, Instant},
};
#[derive(RustEmbed)]
#[folder = "assets/"]
struct Asset;
pub struct Sdl2WindowWrapper {
context: Sdl,
window: sdl2::video::Window,
skulpin_renderer: SkulpinRenderer,
event_pump: EventPump,
renderer: Renderer,
mouse_down: bool,
mouse_position: LogicalSize,
mouse_enabled: bool,
grid_id_under_mouse: u64,
title: String,
previous_size: LogicalSize,
fullscreen: bool,
cached_size: (u32, u32),
cached_position: (i32, i32),
ui_command_sender: TxUnbounded<UiCommand>,
window_command_receiver: Receiver<WindowCommand>,
running: Arc<AtomicBool>,
}
impl Sdl2WindowWrapper {
pub fn toggle_fullscreen(&mut self) {
if self.fullscreen {
if cfg!(target_os = "windows") {
unsafe {
let raw_handle = self.window.raw();
sdl2::sys::SDL_SetWindowResizable(raw_handle, sdl2::sys::SDL_bool::SDL_TRUE);
}
} else {
self.window.set_fullscreen(FullscreenType::Off).ok();
}
// Use cached size and position
self.window
.set_size(self.cached_size.0, self.cached_size.1)
.unwrap();
self.window.set_position(
sdl2::video::WindowPos::Positioned(self.cached_position.0),
sdl2::video::WindowPos::Positioned(self.cached_position.1),
);
} else {
self.cached_size = self.window.size();
self.cached_position = self.window.position();
if cfg!(target_os = "windows") {
let video_subsystem = self.window.subsystem();
if let Ok(rect) = self
.window
.display_index()
.and_then(|index| video_subsystem.display_bounds(index))
{
// Set window to fullscreen
unsafe {
let raw_handle = self.window.raw();
sdl2::sys::SDL_SetWindowResizable(
raw_handle,
sdl2::sys::SDL_bool::SDL_FALSE,
);
}
self.window.set_size(rect.width(), rect.height()).unwrap();
self.window.set_position(
sdl2::video::WindowPos::Positioned(rect.x()),
sdl2::video::WindowPos::Positioned(rect.y()),
);
}
} else {
self.window.set_fullscreen(FullscreenType::Desktop).ok();
}
}
self.fullscreen = !self.fullscreen;
}
pub fn synchronize_settings(&mut self) {
let transparency = { SETTINGS.get::<WindowSettings>().transparency };
if let Ok(opacity) = self.window.opacity() {
if (opacity - transparency).abs() > std::f32::EPSILON {
self.window.set_opacity(transparency).ok();
}
}
let fullscreen = { SETTINGS.get::<WindowSettings>().fullscreen };
if self.fullscreen != fullscreen {
self.toggle_fullscreen();
}
}
pub fn handle_title_changed(&mut self, new_title: String) {
self.title = new_title;
self.window
.set_title(&self.title)
.expect("Could not set title");
}
pub fn handle_quit(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
pub fn handle_keyboard_input(&mut self, keycode: Option<Keycode>, text: Option<String>) {
let modifiers = self.context.keyboard().mod_state();
if keycode.is_some() || text.is_some() {
log::trace!(
"Keyboard Input Received: keycode-{:?} modifiers-{:?} text-{:?}",
keycode,
modifiers,
text
);
}
if let Some(keybinding_string) =
neovim_keybinding_string(keycode, text, modifiers, handle_qwerty_layout)
{
self.ui_command_sender
.send(UiCommand::Keyboard(keybinding_string))
.unwrap_or_explained_panic(
"Could not send UI command from the window system to the neovim process.",
);
}
}
pub fn handle_pointer_motion(&mut self, x: i32, y: i32) {
let previous_position = self.mouse_position;
let sdl_window_wrapper = Sdl2Window::new(&self.window);
let logical_position =
PhysicalSize::new(x as u32, y as u32).to_logical(sdl_window_wrapper.scale_factor());
let mut top_window_position = (0.0, 0.0);
let mut top_grid_position = None;
for details in self.renderer.window_regions.iter() {
if logical_position.width >= details.region.left as u32
&& logical_position.width < details.region.right as u32
&& logical_position.height >= details.region.top as u32
&& logical_position.height < details.region.bottom as u32
{
top_window_position = (details.region.left, details.region.top);
top_grid_position = Some((
details.id,
LogicalSize::new(
logical_position.width - details.region.left as u32,
logical_position.height - details.region.top as u32,
),
details.floating,
));
}
}
if let Some((grid_id, grid_position, grid_floating)) = top_grid_position {
self.grid_id_under_mouse = grid_id;
self.mouse_position = LogicalSize::new(
(grid_position.width as f32 / self.renderer.font_width) as u32,
(grid_position.height as f32 / self.renderer.font_height) as u32,
);
if self.mouse_enabled && self.mouse_down && previous_position != self.mouse_position {
let (window_left, window_top) = top_window_position;
// Until https://github.com/neovim/neovim/pull/12667 is merged, we have to special
// case non floating windows. Floating windows correctly transform mouse positions
// into grid coordinates, but non floating windows do not.
let position = if grid_floating {
(self.mouse_position.width, self.mouse_position.height)
} else {
let adjusted_drag_left =
self.mouse_position.width + (window_left / self.renderer.font_width) as u32;
let adjusted_drag_top = self.mouse_position.height
+ (window_top / self.renderer.font_height) as u32;
(adjusted_drag_left, adjusted_drag_top)
};
self.ui_command_sender
.send(UiCommand::Drag {
grid_id: self.grid_id_under_mouse,
position,
})
.ok();
}
}
}
pub fn handle_pointer_down(&mut self) {
if self.mouse_enabled {
self.ui_command_sender
.send(UiCommand::MouseButton {
action: String::from("press"),
grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height),
})
.ok();
}
self.mouse_down = true;
}
pub fn handle_pointer_up(&mut self) {
if self.mouse_enabled {
self.ui_command_sender
.send(UiCommand::MouseButton {
action: String::from("release"),
grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height),
})
.ok();
}
self.mouse_down = false;
}
pub fn handle_mouse_wheel(&mut self, x: i32, y: i32) {
if !self.mouse_enabled {
return;
}
let vertical_input_type = match y {
_ if y > 0 => Some("up"),
_ if y < 0 => Some("down"),
_ => None,
};
if let Some(input_type) = vertical_input_type {
self.ui_command_sender
.send(UiCommand::Scroll {
direction: input_type.to_string(),
grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height),
})
.ok();
}
let horizontal_input_type = match y {
_ if x > 0 => Some("right"),
_ if x < 0 => Some("left"),
_ => None,
};
if let Some(input_type) = horizontal_input_type {
self.ui_command_sender
.send(UiCommand::Scroll {
direction: input_type.to_string(),
grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height),
})
.ok();
}
}
pub fn handle_focus_lost(&mut self) {
self.ui_command_sender.send(UiCommand::FocusLost).ok();
}
pub fn handle_focus_gained(&mut self) {
self.ui_command_sender.send(UiCommand::FocusGained).ok();
REDRAW_SCHEDULER.queue_next_frame();
}
fn handle_events(&mut self) {
self.synchronize_settings();
let mut keycode = None;
let mut keytext = None;
let mut ignore_text_this_frame = false;
let window_events: Vec<Event> = self.event_pump.poll_iter().collect();
for event in window_events.into_iter() {
match event {
Event::Quit { .. } => self.handle_quit(),
Event::DropFile { filename, .. } => {
self.ui_command_sender
.send(UiCommand::FileDrop(filename))
.ok();
}
Event::KeyDown {
keycode: received_keycode,
..
} => {
keycode = received_keycode;
}
Event::TextInput { text, .. } => keytext = Some(text),
Event::MouseMotion { x, y, .. } => self.handle_pointer_motion(x, y),
Event::MouseButtonDown { .. } => self.handle_pointer_down(),
Event::MouseButtonUp { .. } => self.handle_pointer_up(),
Event::MouseWheel { x, y, .. } => self.handle_mouse_wheel(x, y),
Event::Window {
win_event: WindowEvent::FocusLost,
..
} => self.handle_focus_lost(),
Event::Window {
win_event: WindowEvent::FocusGained,
..
} => {
ignore_text_this_frame = true; // Ignore any text events on the first frame when focus is regained. https://github.com/Kethku/neovide/issues/193
self.handle_focus_gained();
}
Event::Window { .. } => REDRAW_SCHEDULER.queue_next_frame(),
_ => {}
}
}
if !ignore_text_this_frame {
self.handle_keyboard_input(keycode, keytext);
}
let window_commands: Vec<WindowCommand> = self.window_command_receiver.try_iter().collect();
for window_command in window_commands.into_iter() {
match window_command {
WindowCommand::TitleChanged(new_title) => self.handle_title_changed(new_title),
WindowCommand::SetMouseEnabled(mouse_enabled) => self.mouse_enabled = mouse_enabled,
}
}
}
fn draw_frame(&mut self, dt: f32) -> VkResult<bool> {
let sdl_window_wrapper = Sdl2Window::new(&self.window);
let new_size = sdl_window_wrapper.logical_size();
if self.previous_size != new_size {
handle_new_grid_size(new_size, &self.renderer, &self.ui_command_sender);
self.previous_size = new_size;
}
let current_size = self.previous_size;
let ui_command_sender = self.ui_command_sender.clone();
if REDRAW_SCHEDULER.should_draw() || SETTINGS.get::<WindowSettings>().no_idle {
log::debug!("Render Triggered");
let scaling = sdl_window_wrapper.scale_factor();
let renderer = &mut self.renderer;
self.skulpin_renderer.draw(
&sdl_window_wrapper,
|canvas, coordinate_system_helper| {
if renderer.draw_frame(canvas, &coordinate_system_helper, dt, scaling as f32) {
handle_new_grid_size(current_size, &renderer, &ui_command_sender);
}
},
)?;
Ok(true)
} else {
Ok(false)
}
}
}
fn allow_compositing() {
// This fixes any vestiges of https://github.com/Kethku/neovide/issues/370
// which is an issue where KDE and perhaps others misbehave when compositing is forced off for
// a window, as is done by default with SDL2. Since this just sets a hint that will be ignored
// if unsupported, it is OK to leave in for all platforms.
//
// This is `sdl2_sys::SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR`
let name = "SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR";
sdl2::hint::set(name, "0");
}
fn set_icon(win: &mut sdl2::video::Window) {
let icon_data = Asset::get("nvim.ico").expect("Failed to read icon data");
let icon = load_from_memory_with_format(&icon_data, image::ImageFormat::ICO)
.expect("Failed to parse icon data");
let icon = icon.into_rgba();
let width = icon.width();
let height = icon.height();
let mut icon = icon.into_raw();
let surf = sdl2::surface::Surface::from_data(
&mut icon,
width,
height,
4 * width,
sdl2::pixels::PixelFormatEnum::RGBA32,
)
.expect("Failed to create icon surface");
win.set_icon(surf);
}
pub fn start_loop(
window_command_receiver: Receiver<WindowCommand>,
ui_command_sender: TxUnbounded<UiCommand>,
running: Arc<AtomicBool>,
logical_size: LogicalSize,
renderer: Renderer,
) {
sdl2::hint::set("SDL_MOUSE_FOCUS_CLICKTHROUGH", "1");
let context = sdl2::init().expect("Failed to initialize sdl2");
allow_compositing();
let video_subsystem = context
.video()
.expect("Failed to create sdl video subsystem");
video_subsystem.text_input().start();
let mut sdl_window = video_subsystem
.window("Neovide", logical_size.width, logical_size.height)
.position_centered()
.allow_highdpi()
.resizable()
.build()
.expect("Failed to create window");
log::info!("window created");
set_icon(&mut sdl_window);
if std::env::args().any(|arg| arg == "--maximized") {
sdl_window.maximize();
}
let skulpin_renderer = {
let sdl_window_wrapper = Sdl2Window::new(&sdl_window);
RendererBuilder::new()
.prefer_discrete_gpu()
.use_vulkan_debug_layer(false)
.present_mode_priority(vec![PresentMode::Immediate])
.coordinate_system(CoordinateSystem::Logical)
.build(&sdl_window_wrapper)
.expect("Failed to create renderer")
};
let event_pump = context
.event_pump()
.expect("Could not create sdl event pump");
let mut window_wrapper = Sdl2WindowWrapper {
context,
window: sdl_window,
skulpin_renderer,
renderer,
event_pump,
mouse_down: false,
mouse_position: LogicalSize {
width: 0,
height: 0,
},
mouse_enabled: true,
grid_id_under_mouse: 0,
title: String::from("Neovide"),
previous_size: logical_size,
fullscreen: false,
cached_size: (0, 0),
cached_position: (0, 0),
ui_command_sender,
window_command_receiver,
running: running.clone(),
};
let mut was_animating = false;
let mut previous_frame_start = Instant::now();
loop {
if !running.load(Ordering::Relaxed) {
break;
}
let frame_start = Instant::now();
let refresh_rate = { SETTINGS.get::<WindowSettings>().refresh_rate as f32 };
let dt = if was_animating {
previous_frame_start.elapsed().as_secs_f32()
} else {
1.0 / refresh_rate
};
window_wrapper.handle_events();
match window_wrapper.draw_frame(dt) {
Ok(animating) => {
was_animating = animating;
}
Err(error) => {
log::error!("Render failed: {}", error);
break;
}
}
let elapsed = frame_start.elapsed();
let expected_frame_length = Duration::from_secs_f32(1.0 / refresh_rate);
previous_frame_start = frame_start;
if elapsed < expected_frame_length {
sleep(expected_frame_length - elapsed);
}
}
std::process::exit(0);
}

@ -1,12 +1,12 @@
use crate::settings::*; use crate::{cmd_line::CmdLineSettings, settings::*};
pub use super::keyboard::KeyboardSettings; pub use super::keyboard::KeyboardSettings;
#[derive(Clone, SettingGroup)] #[derive(Clone, SettingGroup)]
pub struct WindowSettings { pub struct WindowSettings {
pub refresh_rate: u64, pub refresh_rate: u64,
pub transparency: f32,
pub no_idle: bool, pub no_idle: bool,
pub transparency: f32,
pub fullscreen: bool, pub fullscreen: bool,
pub iso_layout: bool, pub iso_layout: bool,
} }
@ -14,13 +14,14 @@ pub struct WindowSettings {
impl Default for WindowSettings { impl Default for WindowSettings {
fn default() -> Self { fn default() -> Self {
Self { Self {
refresh_rate: 60,
transparency: 1.0, transparency: 1.0,
no_idle: SETTINGS
.neovim_arguments
.contains(&String::from("--noIdle")),
fullscreen: false, fullscreen: false,
iso_layout: false, iso_layout: false,
refresh_rate: 60,
no_idle: SETTINGS
.get::<CmdLineSettings>()
.neovim_args
.contains(&String::from("--noIdle")),
} }
} }
} }

@ -1,7 +1,7 @@
mod qwerty; mod qwerty;
use crate::window::keyboard::Modifiers; use crate::window::keyboard::Modifiers;
use skulpin::winit::event::ModifiersState; use glutin::event::ModifiersState;
pub use qwerty::handle_qwerty_layout; pub use qwerty::handle_qwerty_layout;

@ -1,5 +1,5 @@
use crate::window::keyboard::{unsupported_key, Token}; use crate::window::keyboard::{unsupported_key, Token};
use skulpin::winit::event::VirtualKeyCode::{self, *}; use glutin::event::VirtualKeyCode::{self, *};
/// Maps winit keyboard events to Vim tokens /// Maps winit keyboard events to Vim tokens
pub fn handle_qwerty_layout(keycode: VirtualKeyCode, shift: bool) -> Option<Token<'static>> { pub fn handle_qwerty_layout(keycode: VirtualKeyCode, shift: bool) -> Option<Token<'static>> {
@ -23,9 +23,9 @@ pub fn handle_qwerty_layout(keycode: VirtualKeyCode, shift: bool) -> Option<Toke
(Slash, false) => partial("/"), (Slash, false) => partial("/"),
(Slash, true) => partial("?"), (Slash, true) => partial("?"),
(Key0, false) => normal("0"), (Key0, false) => normal("0"),
(Key0, true) => special(")"), (Key0, true) => partial(")"),
(Key1, false) => normal("1"), (Key1, false) => normal("1"),
(Key1, true) => special("!"), (Key1, true) => partial("!"),
(Key2, false) => partial("2"), (Key2, false) => partial("2"),
(Key2, true) => partial("@"), (Key2, true) => partial("@"),
(Key3, false) => partial("3"), (Key3, false) => partial("3"),

@ -1,28 +1,27 @@
#[macro_use] #[macro_use]
mod layouts; mod layouts;
mod renderer;
use super::{handle_new_grid_size, keyboard::neovim_keybinding_string, settings::WindowSettings}; use super::{handle_new_grid_size, keyboard::neovim_keybinding_string, settings::WindowSettings};
use crate::{ use crate::{
bridge::UiCommand, editor::WindowCommand, error_handling::ResultPanicExplanation, bridge::UiCommand, channel_utils::*, cmd_line::CmdLineSettings, editor::WindowCommand,
redraw_scheduler::REDRAW_SCHEDULER, renderer::Renderer, settings::SETTINGS, error_handling::ResultPanicExplanation, redraw_scheduler::REDRAW_SCHEDULER, renderer::Renderer,
settings::SETTINGS,
}; };
use crossfire::mpsc::TxUnbounded; use glutin::{
use image::{load_from_memory, GenericImageView, Pixel}; self,
use layouts::handle_qwerty_layout; dpi::{LogicalPosition, LogicalSize, PhysicalSize},
use skulpin::{ event::{
ash::prelude::VkResult, ElementState, Event, ModifiersState, MouseButton, MouseScrollDelta,
winit::{ VirtualKeyCode as Keycode, WindowEvent,
self,
event::{
ElementState, Event, ModifiersState, MouseButton, MouseScrollDelta,
VirtualKeyCode as Keycode, WindowEvent,
},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, Icon},
}, },
CoordinateSystem, LogicalSize, PhysicalSize, PresentMode, Renderer as SkulpinRenderer, event_loop::{ControlFlow, EventLoop},
RendererBuilder, Window, WinitWindow, window::{self, Fullscreen, Icon},
ContextBuilder, GlProfile, WindowedContext,
}; };
use image::{load_from_memory, GenericImageView, Pixel};
use layouts::handle_qwerty_layout;
use renderer::SkiaRenderer;
use std::{ use std::{
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@ -36,49 +35,47 @@ use std::{
#[folder = "assets/"] #[folder = "assets/"]
struct Asset; struct Asset;
pub struct WinitWindowWrapper { pub struct GlutinWindowWrapper {
window: winit::window::Window, windowed_context: WindowedContext<glutin::PossiblyCurrent>,
skulpin_renderer: SkulpinRenderer, skia_renderer: SkiaRenderer,
renderer: Renderer, renderer: Renderer,
mouse_down: bool, mouse_down: bool,
mouse_position: LogicalSize, mouse_position: LogicalPosition<u32>,
mouse_enabled: bool, mouse_enabled: bool,
grid_id_under_mouse: u64, grid_id_under_mouse: u64,
current_modifiers: Option<ModifiersState>, current_modifiers: Option<ModifiersState>,
title: String, title: String,
previous_size: LogicalSize, previous_size: PhysicalSize<u32>,
fullscreen: bool, fullscreen: bool,
cached_size: LogicalSize, cached_size: LogicalSize<u32>,
cached_position: LogicalSize, cached_position: LogicalPosition<u32>,
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: LoggingTx<UiCommand>,
window_command_receiver: Receiver<WindowCommand>, window_command_receiver: Receiver<WindowCommand>,
running: Arc<AtomicBool>,
} }
impl WinitWindowWrapper { impl GlutinWindowWrapper {
pub fn toggle_fullscreen(&mut self) { pub fn toggle_fullscreen(&mut self) {
let window = self.windowed_context.window();
if self.fullscreen { if self.fullscreen {
self.window.set_fullscreen(None); window.set_fullscreen(None);
// Use cached size and position // Use cached size and position
self.window.set_inner_size(winit::dpi::LogicalSize::new( window.set_inner_size(LogicalSize::new(
self.cached_size.width, self.cached_size.width,
self.cached_size.height, self.cached_size.height,
)); ));
self.window window.set_outer_position(LogicalPosition::new(
.set_outer_position(winit::dpi::LogicalPosition::new( self.cached_position.x,
self.cached_position.width, self.cached_position.y,
self.cached_position.height, ));
));
} else { } else {
let current_size = self.window.inner_size(); let current_size = window.inner_size();
self.cached_size = LogicalSize::new(current_size.width, current_size.height); self.cached_size = LogicalSize::new(current_size.width, current_size.height);
let current_position = self.window.outer_position().unwrap(); let current_position = window.outer_position().unwrap();
self.cached_position = self.cached_position =
LogicalSize::new(current_position.x as u32, current_position.y as u32); LogicalPosition::new(current_position.x as u32, current_position.y as u32);
let handle = self.window.current_monitor(); let handle = window.current_monitor();
self.window window.set_fullscreen(Some(Fullscreen::Borderless(handle)));
.set_fullscreen(Some(Fullscreen::Borderless(handle)));
} }
self.fullscreen = !self.fullscreen; self.fullscreen = !self.fullscreen;
@ -92,13 +89,30 @@ impl WinitWindowWrapper {
} }
} }
#[allow(clippy::needless_collect)]
pub fn handle_window_commands(&mut self) {
let window_commands: Vec<WindowCommand> = self.window_command_receiver.try_iter().collect();
for window_command in window_commands.into_iter() {
match window_command {
WindowCommand::TitleChanged(new_title) => self.handle_title_changed(new_title),
WindowCommand::SetMouseEnabled(mouse_enabled) => self.mouse_enabled = mouse_enabled,
}
}
}
pub fn handle_title_changed(&mut self, new_title: String) { pub fn handle_title_changed(&mut self, new_title: String) {
self.title = new_title; self.title = new_title;
self.window.set_title(&self.title); self.windowed_context.window().set_title(&self.title);
} }
pub fn handle_quit(&mut self) { pub fn handle_quit(&mut self, running: &Arc<AtomicBool>) {
self.running.store(false, Ordering::Relaxed); if SETTINGS.get::<CmdLineSettings>().remote_tcp.is_none() {
self.ui_command_sender
.send(UiCommand::Quit)
.expect("Could not send quit command to bridge");
} else {
running.store(false, Ordering::Relaxed);
}
} }
pub fn handle_keyboard_input( pub fn handle_keyboard_input(
@ -126,10 +140,15 @@ impl WinitWindowWrapper {
} }
pub fn handle_pointer_motion(&mut self, x: i32, y: i32) { pub fn handle_pointer_motion(&mut self, x: i32, y: i32) {
let size = self.windowed_context.window().inner_size();
if x < 0 || x as u32 >= size.width || y < 0 || y as u32 >= size.height {
return;
}
let previous_position = self.mouse_position; let previous_position = self.mouse_position;
let winit_window_wrapper = WinitWindow::new(&self.window);
let logical_position = let logical_position: LogicalSize<u32> = PhysicalSize::new(x as u32, y as u32)
PhysicalSize::new(x as u32, y as u32).to_logical(winit_window_wrapper.scale_factor()); .to_logical(self.windowed_context.window().scale_factor());
let mut top_window_position = (0.0, 0.0); let mut top_window_position = (0.0, 0.0);
let mut top_grid_position = None; let mut top_grid_position = None;
@ -143,20 +162,20 @@ impl WinitWindowWrapper {
top_window_position = (details.region.left, details.region.top); top_window_position = (details.region.left, details.region.top);
top_grid_position = Some(( top_grid_position = Some((
details.id, details.id,
LogicalSize::new( LogicalSize::<u32>::new(
logical_position.width - details.region.left as u32, logical_position.width - details.region.left as u32,
logical_position.height - details.region.top as u32, logical_position.height - details.region.top as u32,
), ),
details.floating, details.floating_order.is_some(),
)); ));
} }
} }
if let Some((grid_id, grid_position, grid_floating)) = top_grid_position { if let Some((grid_id, grid_position, grid_floating)) = top_grid_position {
self.grid_id_under_mouse = grid_id; self.grid_id_under_mouse = grid_id;
self.mouse_position = LogicalSize::new( self.mouse_position = LogicalPosition::new(
(grid_position.width as f32 / self.renderer.font_width) as u32, (grid_position.width as u64 / self.renderer.font_width) as u32,
(grid_position.height as f32 / self.renderer.font_height) as u32, (grid_position.height as u64 / self.renderer.font_height) as u32,
); );
if self.mouse_enabled && self.mouse_down && previous_position != self.mouse_position { if self.mouse_enabled && self.mouse_down && previous_position != self.mouse_position {
@ -166,12 +185,12 @@ impl WinitWindowWrapper {
// case non floating windows. Floating windows correctly transform mouse positions // case non floating windows. Floating windows correctly transform mouse positions
// into grid coordinates, but non floating windows do not. // into grid coordinates, but non floating windows do not.
let position = if grid_floating { let position = if grid_floating {
(self.mouse_position.width, self.mouse_position.height) (self.mouse_position.x, self.mouse_position.y)
} else { } else {
let adjusted_drag_left = let adjusted_drag_left = self.mouse_position.x
self.mouse_position.width + (window_left / self.renderer.font_width) as u32; + (window_left / self.renderer.font_width as f32) as u32;
let adjusted_drag_top = self.mouse_position.height let adjusted_drag_top = self.mouse_position.y
+ (window_top / self.renderer.font_height) as u32; + (window_top / self.renderer.font_height as f32) as u32;
(adjusted_drag_left, adjusted_drag_top) (adjusted_drag_left, adjusted_drag_top)
}; };
@ -191,7 +210,7 @@ impl WinitWindowWrapper {
.send(UiCommand::MouseButton { .send(UiCommand::MouseButton {
action: String::from("press"), action: String::from("press"),
grid_id: self.grid_id_under_mouse, grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height), position: (self.mouse_position.x, self.mouse_position.y),
}) })
.ok(); .ok();
} }
@ -204,21 +223,21 @@ impl WinitWindowWrapper {
.send(UiCommand::MouseButton { .send(UiCommand::MouseButton {
action: String::from("release"), action: String::from("release"),
grid_id: self.grid_id_under_mouse, grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height), position: (self.mouse_position.x, self.mouse_position.y),
}) })
.ok(); .ok();
} }
self.mouse_down = false; self.mouse_down = false;
} }
pub fn handle_mouse_wheel(&mut self, x: i32, y: i32) { pub fn handle_mouse_wheel(&mut self, x: f32, y: f32) {
if !self.mouse_enabled { if !self.mouse_enabled {
return; return;
} }
let vertical_input_type = match y { let vertical_input_type = match y {
_ if y > 0 => Some("up"), _ if y > 0.0 => Some("up"),
_ if y < 0 => Some("down"), _ if y < 0.0 => Some("down"),
_ => None, _ => None,
}; };
@ -227,14 +246,14 @@ impl WinitWindowWrapper {
.send(UiCommand::Scroll { .send(UiCommand::Scroll {
direction: input_type.to_string(), direction: input_type.to_string(),
grid_id: self.grid_id_under_mouse, grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height), position: (self.mouse_position.x, self.mouse_position.y),
}) })
.ok(); .ok();
} }
let horizontal_input_type = match y { let horizontal_input_type = match y {
_ if x > 0 => Some("right"), _ if x > 0.0 => Some("right"),
_ if x < 0 => Some("left"), _ if x < 0.0 => Some("left"),
_ => None, _ => None,
}; };
@ -243,7 +262,7 @@ impl WinitWindowWrapper {
.send(UiCommand::Scroll { .send(UiCommand::Scroll {
direction: input_type.to_string(), direction: input_type.to_string(),
grid_id: self.grid_id_under_mouse, grid_id: self.grid_id_under_mouse,
position: (self.mouse_position.width, self.mouse_position.height), position: (self.mouse_position.x, self.mouse_position.y),
}) })
.ok(); .ok();
} }
@ -258,19 +277,19 @@ impl WinitWindowWrapper {
REDRAW_SCHEDULER.queue_next_frame(); REDRAW_SCHEDULER.queue_next_frame();
} }
pub fn handle_event(&mut self, event: Event<()>) { pub fn handle_event(&mut self, event: Event<()>, running: &Arc<AtomicBool>) {
let mut keycode = None; let mut keycode = None;
let mut ignore_text_this_frame = false; let mut ignore_text_this_frame = false;
match event { match event {
Event::LoopDestroyed => { Event::LoopDestroyed => {
self.handle_quit(); self.handle_quit(running);
} }
Event::WindowEvent { Event::WindowEvent {
event: WindowEvent::CloseRequested, event: WindowEvent::CloseRequested,
.. ..
} => { } => {
self.handle_quit(); self.handle_quit(running);
} }
Event::WindowEvent { Event::WindowEvent {
event: WindowEvent::DroppedFile(path), event: WindowEvent::DroppedFile(path),
@ -307,8 +326,15 @@ impl WinitWindowWrapper {
.. ..
}, },
.. ..
} => self.handle_mouse_wheel(x as i32, y as i32), } => self.handle_mouse_wheel(x as f32, y as f32),
Event::WindowEvent {
event:
WindowEvent::MouseWheel {
delta: MouseScrollDelta::PixelDelta(logical_position),
..
},
..
} => self.handle_mouse_wheel(0.0, logical_position.y as f32),
Event::WindowEvent { Event::WindowEvent {
event: event:
WindowEvent::MouseInput { WindowEvent::MouseInput {
@ -344,12 +370,18 @@ impl WinitWindowWrapper {
} }
} }
pub fn draw_frame(&mut self, dt: f32) -> VkResult<bool> { pub fn draw_frame(&mut self, dt: f32) {
let winit_window_wrapper = WinitWindow::new(&self.window); let window = self.windowed_context.window();
let new_size = winit_window_wrapper.logical_size(); let new_size = window.inner_size();
if self.previous_size != new_size { if self.previous_size != new_size {
handle_new_grid_size(new_size, &self.renderer, &self.ui_command_sender);
self.previous_size = new_size; self.previous_size = new_size;
let new_size: LogicalSize<u32> = new_size.to_logical(window.scale_factor());
handle_new_grid_size(
(new_size.width as u64, new_size.height as u64),
&self.renderer,
&self.ui_command_sender,
);
self.skia_renderer.resize(&self.windowed_context);
} }
let current_size = self.previous_size; let current_size = self.previous_size;
@ -358,29 +390,33 @@ impl WinitWindowWrapper {
if REDRAW_SCHEDULER.should_draw() || SETTINGS.get::<WindowSettings>().no_idle { if REDRAW_SCHEDULER.should_draw() || SETTINGS.get::<WindowSettings>().no_idle {
log::debug!("Render Triggered"); log::debug!("Render Triggered");
let scaling = winit_window_wrapper.scale_factor(); let scaling = 1.0 / self.windowed_context.window().scale_factor();
let renderer = &mut self.renderer; let renderer = &mut self.renderer;
self.skulpin_renderer.draw(
&winit_window_wrapper, {
|canvas, coordinate_system_helper| { let canvas = self.skia_renderer.canvas();
if renderer.draw_frame(canvas, &coordinate_system_helper, dt, scaling as f32) {
handle_new_grid_size(current_size, &renderer, &ui_command_sender); if renderer.draw_frame(canvas, dt, scaling as f32) {
} handle_new_grid_size(
}, (current_size.width as u64, current_size.height as u64),
)?; renderer,
&ui_command_sender,
Ok(true) );
} else { }
Ok(false) }
self.skia_renderer.gr_context.flush(None);
self.windowed_context.swap_buffers().unwrap();
} }
} }
} }
pub fn start_loop( pub fn start_loop(
window_command_receiver: Receiver<WindowCommand>, window_command_receiver: Receiver<WindowCommand>,
ui_command_sender: TxUnbounded<UiCommand>, ui_command_sender: LoggingTx<UiCommand>,
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
logical_size: LogicalSize, logical_size: (u64, u64),
renderer: Renderer, renderer: Renderer,
) { ) {
let icon = { let icon = {
@ -396,102 +432,71 @@ pub fn start_loop(
log::info!("icon created"); log::info!("icon created");
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
let winit_window = winit::window::WindowBuilder::new() let (width, height) = logical_size;
let logical_size: LogicalSize<u32> = (width as u32, height as u32).into();
let winit_window_builder = window::WindowBuilder::new()
.with_title("Neovide") .with_title("Neovide")
.with_inner_size(winit::dpi::LogicalSize::new( .with_inner_size(logical_size)
logical_size.width,
logical_size.height,
))
.with_window_icon(Some(icon)) .with_window_icon(Some(icon))
.with_maximized(std::env::args().any(|arg| arg == "--maximized")) .with_maximized(SETTINGS.get::<CmdLineSettings>().maximized)
.build(&event_loop) .with_decorations(!SETTINGS.get::<CmdLineSettings>().frameless);
.expect("Failed to create window");
let windowed_context = ContextBuilder::new()
.with_pixel_format(24, 8)
.with_stencil_buffer(8)
.with_gl_profile(GlProfile::Core)
.with_vsync(false)
.with_srgb(false)
.build_windowed(winit_window_builder, &event_loop)
.unwrap();
let windowed_context = unsafe { windowed_context.make_current().unwrap() };
let previous_size = logical_size.to_physical(windowed_context.window().scale_factor());
log::info!("window created"); log::info!("window created");
let skulpin_renderer = { let skia_renderer = SkiaRenderer::new(&windowed_context);
let winit_window_wrapper = WinitWindow::new(&winit_window);
RendererBuilder::new()
.prefer_integrated_gpu()
.use_vulkan_debug_layer(false)
.present_mode_priority(vec![PresentMode::Immediate])
.coordinate_system(CoordinateSystem::Logical)
.build(&winit_window_wrapper)
.expect("Failed to create renderer")
};
let mut window_wrapper = WinitWindowWrapper { let mut window_wrapper = GlutinWindowWrapper {
window: winit_window, windowed_context,
skulpin_renderer, skia_renderer,
renderer, renderer,
mouse_down: false, mouse_down: false,
mouse_position: LogicalSize { mouse_position: LogicalPosition::new(0, 0),
width: 0,
height: 0,
},
mouse_enabled: true, mouse_enabled: true,
grid_id_under_mouse: 0, grid_id_under_mouse: 0,
current_modifiers: None, current_modifiers: None,
title: String::from("Neovide"), title: String::from("Neovide"),
previous_size: logical_size, previous_size,
fullscreen: false, fullscreen: false,
cached_size: LogicalSize::new(0, 0), cached_size: LogicalSize::new(0, 0),
cached_position: LogicalSize::new(0, 0), cached_position: LogicalPosition::new(0, 0),
ui_command_sender, ui_command_sender,
window_command_receiver, window_command_receiver,
running: running.clone(),
}; };
let mut was_animating = false; let mut previous_frame_start = Instant::now();
let previous_frame_start = Instant::now();
event_loop.run(move |e, _window_target, control_flow| { event_loop.run(move |e, _window_target, control_flow| {
if !running.load(Ordering::Relaxed) { if !running.load(Ordering::Relaxed) {
*control_flow = ControlFlow::Exit; std::process::exit(0);
return;
} }
let frame_start = Instant::now(); let frame_start = Instant::now();
let refresh_rate = { SETTINGS.get::<WindowSettings>().refresh_rate as f32 }; window_wrapper.handle_window_commands();
let dt = if was_animating {
previous_frame_start.elapsed().as_secs_f32()
} else {
1.0 / refresh_rate
};
window_wrapper.synchronize_settings(); window_wrapper.synchronize_settings();
window_wrapper.handle_event(e, &running);
window_wrapper.handle_event(e); let refresh_rate = { SETTINGS.get::<WindowSettings>().refresh_rate as f32 };
let expected_frame_length_seconds = 1.0 / refresh_rate;
let window_commands: Vec<WindowCommand> = let frame_duration = Duration::from_secs_f32(expected_frame_length_seconds);
window_wrapper.window_command_receiver.try_iter().collect();
for window_command in window_commands.into_iter() {
match window_command {
WindowCommand::TitleChanged(new_title) => {
window_wrapper.handle_title_changed(new_title)
}
WindowCommand::SetMouseEnabled(mouse_enabled) => {
window_wrapper.mouse_enabled = mouse_enabled
}
}
}
match window_wrapper.draw_frame(dt) { if frame_start - previous_frame_start > frame_duration {
Ok(animating) => { let dt = previous_frame_start.elapsed().as_secs_f32();
was_animating = animating; window_wrapper.draw_frame(dt);
} previous_frame_start = frame_start;
Err(error) => {
log::error!("Render failed: {}", error);
window_wrapper.running.store(false, Ordering::Relaxed);
return;
}
} }
let elapsed = frame_start.elapsed(); *control_flow = ControlFlow::WaitUntil(previous_frame_start + frame_duration)
let frame_length = Duration::from_secs_f32(1.0 / refresh_rate);
if elapsed < frame_length {
*control_flow = ControlFlow::WaitUntil(Instant::now() + frame_length);
}
}); });
} }

@ -0,0 +1,88 @@
use skia_safe::gpu::gl::FramebufferInfo;
use skia_safe::gpu::{BackendRenderTarget, DirectContext, SurfaceOrigin};
use skia_safe::{Canvas, ColorType, Surface};
use std::convert::TryInto;
use gl::types::*;
type WindowedContext = glutin::ContextWrapper<glutin::PossiblyCurrent, glutin::window::Window>;
fn create_surface(
windowed_context: &WindowedContext,
gr_context: &mut DirectContext,
fb_info: FramebufferInfo,
) -> Surface {
let pixel_format = windowed_context.get_pixel_format();
let size = windowed_context.window().inner_size();
let size = (
size.width.try_into().expect("Could not convert width"),
size.height.try_into().expect("Could not convert height"),
);
let backend_render_target = BackendRenderTarget::new_gl(
size,
pixel_format
.multisampling
.map(|s| s.try_into().expect("Could not convert multisampling")),
pixel_format
.stencil_bits
.try_into()
.expect("Could not convert stencil"),
fb_info,
);
windowed_context.resize(size.into());
Surface::from_backend_render_target(
gr_context,
&backend_render_target,
SurfaceOrigin::BottomLeft,
ColorType::RGBA8888,
None,
None,
)
.expect("Could not create skia surface")
}
pub struct SkiaRenderer {
pub gr_context: DirectContext,
fb_info: FramebufferInfo,
surface: Surface,
}
impl SkiaRenderer {
pub fn new(windowed_context: &WindowedContext) -> SkiaRenderer {
gl::load_with(|s| windowed_context.get_proc_address(s));
let interface = skia_safe::gpu::gl::Interface::new_load_with(|name| {
if name == "eglGetCurrentDisplay" {
return std::ptr::null();
}
windowed_context.get_proc_address(name)
})
.expect("Could not create interface");
let mut gr_context = skia_safe::gpu::DirectContext::new_gl(Some(interface), None)
.expect("Could not create direct context");
let fb_info = {
let mut fboid: GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
FramebufferInfo {
fboid: fboid.try_into().expect("Could not create frame buffer id"),
format: skia_safe::gpu::gl::Format::RGBA8.into(),
}
};
let surface = create_surface(windowed_context, &mut gr_context, fb_info);
SkiaRenderer {
gr_context,
fb_info,
surface,
}
}
pub fn canvas(&mut self) -> &mut Canvas {
self.surface.canvas()
}
pub fn resize(&mut self, windowed_context: &WindowedContext) {
self.surface = create_surface(windowed_context, &mut self.gr_context, self.fb_info);
}
}
Loading…
Cancel
Save