* 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 4 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
github: Kethku
ko_fi: keithsimmons
# These are supported funding model platforms
github: Kethku
ko_fi: keithsimmons

@ -10,7 +10,21 @@ jobs:
steps:
- 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: |
cargo fmt --all -- --check
@ -27,7 +41,13 @@ jobs:
NEOVIM_BIN: "C:/tools/neovim/Neovim/bin/nvim.exe"
RUST_BACKTRACE: full
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
run: |
@ -45,17 +65,26 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Vulkan SDK
run: brew install apenngrace/vulkan/vulkan-sdk
- name: Install Nightly Toolchain
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: |
rustup component add rustfmt --toolchain stable-x86_64-apple-darwin
cargo fmt --all -- --check
- name: Lint with Clippy
run: |
rustup component add clippy --toolchain stable-x86_64-apple-darwin
cargo clippy --all -- -D warnings
- name: Uninstall Conflicting LLVM
@ -67,8 +96,16 @@ jobs:
brew install neovim
- name: Test
env:
RUST_BACKTRACE: full
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
run: |
@ -76,18 +113,6 @@ jobs:
cargo install cargo-bundle
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
run: |
hdiutil create Neovide-uncompressed.dmg -volname "Neovide" -srcfolder target/release/bundle/osx
@ -105,18 +130,25 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Vulkan SDK
run: |
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-get update -y --ignore-missing
sudo apt-get install -y vulkan-sdk
- 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: Install dependencies
- name: Install Dependencies
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: |
cargo fmt --all -- --check
@ -129,8 +161,16 @@ jobs:
sudo apt-get install -y neovim
- name: Test
env:
RUST_BACKTRACE: full
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
run: |
@ -140,3 +180,62 @@ jobs:
with:
name: neovide-linux
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.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(Mac/Linux) Debug",
"type": "lldb",
"request": "launch",
"program": "${workspaceRoot}/target/debug/neovide",
"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
},
]
}
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/target/debug/neovide.exe",
"args": [],
},
]
}

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"
[workspace]
members = [
"neovide-derive"
]
members = ["neovide-derive"]
[features]
default = ["sdl2"]
default = []
embed-fonts = []
sdl2 = ["skulpin/skulpin_sdl2"]
winit = ["skulpin/skulpin_winit", "skulpin/winit-23"]
[dependencies]
neovide-derive = { path = "neovide-derive" }
euclid = "0.20.7"
font-kit = "0.10.0"
skribo = { git = "https://github.com/linebender/skribo" }
lru = "0.4.3"
skulpin = "0.11.0"
derive-new = "0.5"
rmpv = "0.4.4"
rust-embed = { version = "5.2.0", features = ["debug-embed"] }
image = "0.22.3"
nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = [ "use_tokio" ] }
tokio = { version = "0.2.9", features = [ "blocking", "process", "time", "tcp" ] }
nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = ["use_tokio"] }
tokio = { version = "0.2.9", features = ["blocking", "process", "time", "tcp"] }
async-trait = "0.1.18"
crossfire = "0.1"
lazy_static = "1.4.0"
unicode-segmentation = "1.6.0"
log = "0.4.8"
flexi_logger = { version = "0.14.6", default-features = false }
anyhow = "1.0.26"
parking_lot="0.10.0"
flexi_logger = { version = "0.17.1", default-features = false }
parking_lot = "0.10.0"
cfg-if = "0.1.10"
which = "4"
dirs = "2"
rand = "0.7"
skia-safe = "0.32.1"
pin-project = "0.4.27"
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]
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]
winapi = { version = "0.3.9", features = ["winuser"] }
[target.'cfg(windows)'.build-dependencies]
winres = "0.1.11"
sdl2-sys = { version = "0.34.4", default-features = false, features = ["bundled", "static-link"] }
[target.'cfg(macos)'.build-dependencies]
sdl2-sys = { version = "0.34.4", default-features = false, features = ["bundled", "static-link"] }
[target.'cfg(linux)'.dependencies.skia-safe]
features = ["gl", "egl"]
version = "0.39.1"
[target.'cfg(not(linux))'.dependencies.skia-safe]
features = ["gl"]
version = "0.39.1"
[profile.release]
debug = true
@ -74,9 +69,9 @@ incremental = true
name = "Neovide"
identifier = "com.kethku.neovide"
icon = ["assets/nvim.ico"]
version = "0.6.0"
version = "0.7.0"
resources = []
copyright = "Copyright (c) keith 2020. All rights reserved."
copyright = "Copyright (c) Keith 2021. All rights reserved."
category = "Productivity"
short_description = "A simple GUI for Neovim."
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)
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.
This is a simple graphical user interface for [Neovim](https://github.com/neovim/neovim) (an aggressively refactored and updated
Vim editor). Where possible there are some graphical improvements, but functionally it should act like the terminal UI.
![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
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
@ -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
```
Finally, if you would like to leave the neovim server running, close the neovide application window instead of issuing a `:q` command.
### Some Nonsense ;)
```vim
@ -110,11 +112,25 @@ necessary. On Windows this should be enabled by default if you have a relatively
### 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.
### 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/>
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/>
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.
4. Build and install Neovide:
```sh
git clone https://github.com/Kethku/neovide
cd neovide
cargo build --release
```
5. Copy `./target/release/neovide` to a known location and enjoy.
3. `git clone https://github.com/Kethku/neovide`
4. `cd neovide`
5. `cargo build --release`
6. Copy `./target/release/neovide` to a known location and enjoy.
### Linux
@ -165,6 +175,20 @@ cd neovide-git
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
```sh
@ -186,40 +210,31 @@ makepkg -si
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)
```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
2. Install Rust
```sh
curl --proto '=https' --tlsv1.2 -sSf "https://sh.rustup.rs" | sh
```
4. Clone the repository
3. Clone the repository
```sh
git clone "https://github.com/Kethku/neovide"
```
5. Build
4. Build
```sh
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
- 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
- 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 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::error;
use std::fmt;
use std::fmt::Debug;
use rmpv::Value;
use skulpin::skia_safe::Color4f;
use skia_safe::Color4f;
use crate::editor::{Colors, CursorMode, CursorShape, Style};
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub enum ParseError {
InvalidArray(Value),
InvalidMap(Value),
InvalidString(Value),
InvalidU64(Value),
InvalidI64(Value),
InvalidF64(Value),
InvalidBool(Value),
InvalidWindowAnchor(Value),
InvalidFormat,
Array(Value),
Map(Value),
String(Value),
U64(Value),
I64(Value),
F64(Value),
Bool(Value),
WindowAnchor(Value),
Format(String),
}
type Result<T> = std::result::Result<T, ParseError>;
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseError::InvalidArray(value) => write!(f, "invalid array format {}", value),
ParseError::InvalidMap(value) => write!(f, "invalid map format {}", value),
ParseError::InvalidString(value) => write!(f, "invalid string format {}", value),
ParseError::InvalidU64(value) => write!(f, "invalid u64 format {}", value),
ParseError::InvalidI64(value) => write!(f, "invalid i64 format {}", value),
ParseError::InvalidF64(value) => write!(f, "invalid f64 format {}", value),
ParseError::InvalidBool(value) => write!(f, "invalid bool format {}", value),
ParseError::InvalidWindowAnchor(value) => {
ParseError::Array(value) => write!(f, "invalid array format {}", value),
ParseError::Map(value) => write!(f, "invalid map format {}", value),
ParseError::String(value) => write!(f, "invalid string format {}", value),
ParseError::U64(value) => write!(f, "invalid u64 format {}", value),
ParseError::I64(value) => write!(f, "invalid i64 format {}", value),
ParseError::F64(value) => write!(f, "invalid f64 format {}", value),
ParseError::Bool(value) => write!(f, "invalid bool format {}", value),
ParseError::WindowAnchor(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 text: String,
pub highlight_id: Option<u64>,
@ -54,7 +57,7 @@ pub struct GridLineCell {
pub type StyledContent = Vec<(u64, String)>;
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum MessageKind {
Unknown,
Confirm,
@ -91,7 +94,7 @@ impl MessageKind {
}
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum GuiOption {
ArabicShape(bool),
AmbiWidth(String),
@ -106,7 +109,7 @@ pub enum GuiOption {
Unknown(String, Value),
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum WindowAnchor {
NorthWest,
NorthEast,
@ -114,7 +117,7 @@ pub enum WindowAnchor {
SouthEast,
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub enum EditorMode {
// The set of modes reported will change in new versions of Nvim, for
// instance more sub-modes and temporary states might be represented as
@ -128,7 +131,7 @@ pub enum EditorMode {
Unknown(String),
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum RedrawEvent {
SetTitle {
title: String,
@ -200,7 +203,7 @@ pub enum RedrawEvent {
anchor_row: f64,
anchor_column: f64,
focusable: bool,
z_index: i64,
sort_order: Option<u64>,
},
WindowExternalPosition {
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();
if values.len() != arr_ref.len() {
Err(ParseError::InvalidFormat)
Err(ParseError::Format(format!("{:?}", values)))
} else {
for (i, val) in values.into_iter().enumerate() {
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>> {
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)>> {
map_value.try_into().map_err(ParseError::InvalidMap)
map_value.try_into().map_err(ParseError::Map)
}
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> {
u64_value.try_into().map_err(ParseError::InvalidU64)
u64_value.try_into().map_err(ParseError::U64)
}
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> {
f64_value.try_into().map_err(ParseError::InvalidF64)
f64_value.try_into().map_err(ParseError::F64)
}
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> {
@ -564,7 +567,7 @@ fn parse_grid_line_cell(grid_line_cell: Value) -> Result<GridLineCell> {
let text_value = cell_contents
.first_mut()
.map(|v| take_value(v))
.ok_or(ParseError::InvalidFormat)?;
.ok_or_else(|| ParseError::Format(format!("{:?}", cell_contents)))?;
let highlight_id = cell_contents
.get_mut(1)
@ -677,33 +680,59 @@ fn parse_window_anchor(value: Value) -> Result<WindowAnchor> {
"NE" => Ok(WindowAnchor::NorthEast),
"SW" => Ok(WindowAnchor::SouthWest),
"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> {
let values = [
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)?;
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)?,
z_index: parse_i64(z_index)?,
})
if win_float_pos_arguments.len() == 8 {
let values = [
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, sort_order] =
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: 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> {
@ -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 event_name = event_contents
.next()
.ok_or(ParseError::InvalidFormat)
.ok_or_else(|| ParseError::Format(format!("{:?}", event_contents)))
.and_then(parse_string)?;
let events = event_contents;
@ -960,7 +989,7 @@ pub fn parse_channel_stream_type(channel_stream_value: Value) -> Result<ChannelS
"stderr" => Ok(ChannelStreamType::Stderr),
"socket" => Ok(ChannelStreamType::Socket),
"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),
"terminal" => Ok(ChannelMode::Terminal),
"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),
"host" => Ok(ClientType::Host),
"plugin" => Ok(ClientType::Plugin),
_ => Err(ParseError::InvalidFormat),
client_type => Err(ParseError::Format(format!("{:?}", client_type))),
}
}

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

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

@ -14,6 +14,7 @@ use crate::windows_utils::{
#[derive(Debug, Clone)]
pub enum UiCommand {
Quit,
Resize {
width: u32,
height: u32,
@ -45,6 +46,9 @@ pub enum UiCommand {
impl UiCommand {
pub async fn execute(self, nvim: &Neovim<TxWrapper>) {
match self {
UiCommand::Quit => {
nvim.command("qa!").await.ok();
}
UiCommand::Resize { width, height } => nvim
.ui_try_resize(width.max(10) as i64, height.max(3) as i64)
.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::sync::Arc;
use skulpin::skia_safe::Color4f;
use skia_safe::Color4f;
use super::style::{Colors, Style};
@ -70,10 +70,9 @@ impl Cursor {
style
.colors
.foreground
.clone()
.unwrap_or_else(|| default_colors.background.clone().unwrap())
.unwrap_or_else(|| default_colors.background.unwrap())
} else {
default_colors.background.clone().unwrap()
default_colors.background.unwrap()
}
}
@ -82,10 +81,9 @@ impl Cursor {
style
.colors
.background
.clone()
.unwrap_or_else(|| default_colors.foreground.clone().unwrap())
.unwrap_or_else(|| default_colors.foreground.unwrap())
} else {
default_colors.foreground.clone().unwrap()
default_colors.foreground.unwrap()
}
}
@ -159,18 +157,18 @@ mod tests {
assert_eq!(
cursor.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap()
DEFAULT_COLORS.background.unwrap()
);
cursor.style = style.clone();
assert_eq!(
cursor.foreground(&DEFAULT_COLORS),
COLORS.foreground.clone().unwrap()
COLORS.foreground.unwrap()
);
cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
assert_eq!(
cursor.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap()
DEFAULT_COLORS.background.unwrap()
);
}
@ -181,18 +179,18 @@ mod tests {
assert_eq!(
cursor.background(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap()
DEFAULT_COLORS.foreground.unwrap()
);
cursor.style = style.clone();
assert_eq!(
cursor.background(&DEFAULT_COLORS),
COLORS.background.clone().unwrap()
COLORS.background.unwrap()
);
cursor.style = Some(Arc::new(Style::new(NONE_COLORS)));
assert_eq!(
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 super::DrawCommand;
use crate::channel_utils::*;
pub struct DrawCommandBatcher {
window_draw_command_sender: Sender<DrawCommand>,
window_draw_command_receiver: Receiver<DrawCommand>,
batched_draw_command_sender: Sender<Vec<DrawCommand>>,
batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>,
}
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();
DrawCommandBatcher {

@ -2,7 +2,14 @@ use std::sync::Arc;
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 width: u64,
@ -16,7 +23,7 @@ impl CharacterGrid {
let (width, height) = size;
let cell_count = (width * height) as usize;
CharacterGrid {
characters: vec![None; cell_count],
characters: vec![default_cell!(); cell_count],
width,
height,
}
@ -24,8 +31,7 @@ impl CharacterGrid {
pub fn resize(&mut self, width: u64, height: u64) {
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 y in 0..self.height.min(height) {
@ -41,7 +47,7 @@ impl CharacterGrid {
}
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> {
@ -61,12 +67,10 @@ impl CharacterGrid {
.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
.resize_with((self.width * self.height) as usize, || {
value.as_ref().cloned()
});
.resize_with((self.width * self.height) as usize, || value.clone());
}
pub fn row(&self, row_index: u64) -> Option<&[GridCell]> {
@ -123,25 +127,28 @@ mod tests {
}
#[test]
fn test_new() {
fn new_constructsGrid() {
let context = Context::new();
// RUN FUNCTION
let character_grid = CharacterGrid::new(context.size);
assert_eq!(character_grid.width, context.size.0);
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]
fn test_get_cell() {
fn getCell_returnsExpectedCell() {
let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size);
character_grid.characters[context.index] = Some((
character_grid.characters[context.index] = (
"foo".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))),
));
);
let result = (
"foo".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))),
@ -149,24 +156,20 @@ mod tests {
// RUN FUNCTION
assert_eq!(
character_grid
.get_cell(context.x, context.y)
.unwrap()
.as_ref()
.unwrap(),
character_grid.get_cell(context.x, context.y).unwrap(),
&result
);
}
#[test]
fn test_get_cell_mut() {
fn getCellMut_modifiersGridProperly() {
let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size);
character_grid.characters[context.index] = Some((
character_grid.characters[context.index] = (
"foo".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))),
));
);
let result = (
"bar".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))),
@ -174,32 +177,28 @@ mod tests {
// RUN FUNCTION
let cell = character_grid.get_cell_mut(context.x, context.y).unwrap();
*cell = Some((
*cell = (
"bar".to_string(),
Some(Arc::new(Style::new(context.none_colors.clone()))),
));
);
assert_eq!(
character_grid
.get_cell_mut(context.x, context.y)
.unwrap()
.as_ref()
.unwrap(),
character_grid.get_cell_mut(context.x, context.y).unwrap(),
&result
);
}
#[test]
fn test_set_characters_all() {
fn setAllCharacters_setsAllCellsToGivenCharacter() {
let context = Context::new();
let grid_cell = Some((
let grid_cell = (
"foo".to_string(),
Some(Arc::new(Style::new(context.none_colors))),
));
);
let mut character_grid = CharacterGrid::new(context.size);
// RUN FUNCTION
character_grid.set_characters_all(grid_cell.clone());
character_grid.set_all_characters(grid_cell.clone());
assert_eq!(
character_grid.characters,
vec![grid_cell.clone(); context.area]
@ -207,14 +206,14 @@ mod tests {
}
#[test]
fn test_clear() {
fn clear_emptiesBuffer() {
let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size);
let grid_cell = Some((
let grid_cell = (
"foo".to_string(),
Some(Arc::new(Style::new(context.none_colors))),
));
);
character_grid.characters = vec![grid_cell.clone(); context.area];
// RUN FUNCTION
@ -222,11 +221,14 @@ mod tests {
assert_eq!(character_grid.width, context.size.0);
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]
fn test_resize() {
fn resize_clearsAndResizesGrid() {
let context = Context::new();
let mut character_grid = CharacterGrid::new(context.size);
let (width, height) = (
@ -234,10 +236,10 @@ mod tests {
(thread_rng().gen::<u64>() % 500) + 1,
);
let grid_cell = Some((
let grid_cell = (
"foo".to_string(),
Some(Arc::new(Style::new(context.none_colors))),
));
);
character_grid.characters = vec![grid_cell.clone(); context.area];
// RUN FUNCTION
@ -255,7 +257,7 @@ mod tests {
for x in original_width..width {
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::fmt;
use std::sync::mpsc::Sender;
use std::sync::Arc;
use std::thread;
use crossfire::mpsc::RxUnbounded;
use log::{error, trace, warn};
use log::{error, trace};
use crate::bridge::{EditorMode, GuiOption, RedrawEvent, WindowAnchor};
use crate::channel_utils::*;
use crate::redraw_scheduler::REDRAW_SCHEDULER;
pub use cursor::{Cursor, CursorMode, CursorShape};
pub use draw_command_batcher::DrawCommandBatcher;
@ -21,11 +21,13 @@ pub use grid::CharacterGrid;
pub use style::{Colors, Style};
pub use window::*;
#[derive(Clone)]
pub struct AnchorInfo {
pub anchor_grid_id: u64,
pub anchor_type: WindowAnchor,
pub anchor_left: f64,
pub anchor_top: f64,
pub sort_order: u64,
}
impl WindowAnchor {
@ -57,6 +59,7 @@ pub enum DrawCommand {
ModeChanged(EditorMode),
}
#[derive(Debug)]
pub enum WindowCommand {
TitleChanged(String),
SetMouseEnabled(bool),
@ -83,13 +86,13 @@ pub struct Editor {
pub defined_styles: HashMap<u64, Arc<Style>>,
pub mode_list: Vec<CursorMode>,
pub draw_command_batcher: Arc<DrawCommandBatcher>,
pub window_command_sender: Sender<WindowCommand>,
pub window_command_sender: LoggingSender<WindowCommand>,
}
impl Editor {
pub fn new(
batched_draw_command_sender: Sender<Vec<DrawCommand>>,
window_command_sender: Sender<WindowCommand>,
batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>,
window_command_sender: LoggingSender<WindowCommand>,
) -> Editor {
Editor {
windows: HashMap::new(),
@ -208,8 +211,16 @@ impl Editor {
anchor_grid,
anchor_column: anchor_left,
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 } => {
let window = self.windows.get(&grid);
if let Some(window) = window {
@ -240,7 +251,7 @@ impl Editor {
}
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) {
window.resize(width, height);
} else {
@ -265,7 +276,6 @@ impl Editor {
width: u64,
height: u64,
) {
warn!("position {}", grid);
if let Some(window) = self.windows.get_mut(&grid) {
window.position(width, height, None, start_left as f64, start_top as f64);
window.show();
@ -290,8 +300,8 @@ impl Editor {
anchor_type: WindowAnchor,
anchor_left: f64,
anchor_top: f64,
sort_order: Option<u64>,
) {
warn!("floating position {}", grid);
let parent_position = self.get_window_top_left(anchor_grid);
if let Some(window) = self.windows.get_mut(&grid) {
let width = window.get_width();
@ -312,6 +322,7 @@ impl Editor {
anchor_type,
anchor_left,
anchor_top,
sort_order: sort_order.unwrap_or(grid),
}),
modified_left,
modified_top,
@ -323,7 +334,6 @@ impl Editor {
}
fn set_message_position(&mut self, grid: u64, grid_top: u64) {
warn!("message position {}", grid);
let parent_width = self
.windows
.get(&1)
@ -335,6 +345,7 @@ impl Editor {
anchor_type: WindowAnchor::NorthWest,
anchor_left: 0.0,
anchor_top: grid_top as f64,
sort_order: std::u64::MAX,
};
if let Some(window) = self.windows.get_mut(&grid) {
@ -422,15 +433,15 @@ impl Editor {
if let Some(window) = self.windows.get_mut(&grid) {
window.update_viewport(top_line, bottom_line);
} else {
warn!("viewport event received before window initialized");
trace!("viewport event received before window initialized");
}
}
}
pub fn start_editor(
redraw_event_receiver: RxUnbounded<RedrawEvent>,
batched_draw_command_sender: Sender<Vec<DrawCommand>>,
window_command_sender: Sender<WindowCommand>,
batched_draw_command_sender: LoggingSender<Vec<DrawCommand>>,
window_command_sender: LoggingSender<WindowCommand>,
) {
thread::spawn(move || {
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)]
pub struct Colors {
@ -31,13 +31,11 @@ impl Style {
if self.reverse {
self.colors
.background
.clone()
.unwrap_or_else(|| default_colors.background.clone().unwrap())
.unwrap_or_else(|| default_colors.background.unwrap())
} else {
self.colors
.foreground
.clone()
.unwrap_or_else(|| default_colors.foreground.clone().unwrap())
.unwrap_or_else(|| default_colors.foreground.unwrap())
}
}
@ -45,21 +43,18 @@ impl Style {
if self.reverse {
self.colors
.foreground
.clone()
.unwrap_or_else(|| default_colors.foreground.clone().unwrap())
.unwrap_or_else(|| default_colors.foreground.unwrap())
} else {
self.colors
.background
.clone()
.unwrap_or_else(|| default_colors.background.clone().unwrap())
.unwrap_or_else(|| default_colors.background.unwrap())
}
}
pub fn special(&self, default_colors: &Colors) -> Color4f {
self.colors
.special
.clone()
.unwrap_or_else(|| default_colors.special.clone().unwrap())
.unwrap_or_else(|| default_colors.special.unwrap())
}
}
@ -85,12 +80,12 @@ mod tests {
assert_eq!(
style.foreground(&DEFAULT_COLORS),
COLORS.foreground.clone().unwrap()
COLORS.foreground.unwrap()
);
style.colors.foreground = None;
assert_eq!(
style.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap()
DEFAULT_COLORS.foreground.unwrap()
);
}
@ -101,12 +96,12 @@ mod tests {
assert_eq!(
style.foreground(&DEFAULT_COLORS),
COLORS.background.clone().unwrap()
COLORS.background.unwrap()
);
style.colors.background = None;
assert_eq!(
style.foreground(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap()
DEFAULT_COLORS.background.unwrap()
);
}
@ -116,12 +111,12 @@ mod tests {
assert_eq!(
style.background(&DEFAULT_COLORS),
COLORS.background.clone().unwrap()
COLORS.background.unwrap()
);
style.colors.background = None;
assert_eq!(
style.background(&DEFAULT_COLORS),
DEFAULT_COLORS.background.clone().unwrap()
DEFAULT_COLORS.background.unwrap()
);
}
@ -132,12 +127,12 @@ mod tests {
assert_eq!(
style.background(&DEFAULT_COLORS),
COLORS.foreground.clone().unwrap()
COLORS.foreground.unwrap()
);
style.colors.foreground = None;
assert_eq!(
style.background(&DEFAULT_COLORS),
DEFAULT_COLORS.foreground.clone().unwrap()
DEFAULT_COLORS.foreground.unwrap()
);
}
@ -145,14 +140,11 @@ mod tests {
fn test_special() {
let mut style = Style::new(COLORS);
assert_eq!(
style.special(&DEFAULT_COLORS),
COLORS.special.clone().unwrap()
);
assert_eq!(style.special(&DEFAULT_COLORS), COLORS.special.unwrap());
style.colors.special = None;
assert_eq!(
style.special(&DEFAULT_COLORS),
DEFAULT_COLORS.special.clone().unwrap()
DEFAULT_COLORS.special.unwrap()
);
}
}

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use log::warn;
@ -10,20 +9,20 @@ use super::style::Style;
use super::{AnchorInfo, DrawCommand, DrawCommandBatcher};
use crate::bridge::GridLineCell;
#[derive(new, Clone)]
#[derive(new, Clone, Debug)]
pub enum WindowDrawCommand {
Position {
grid_left: f64,
grid_top: f64,
width: u64,
height: u64,
floating: bool,
floating_order: Option<u64>,
},
Cell {
text: String,
cell_width: u64,
Cells {
cells: Vec<String>,
window_left: u64,
window_top: u64,
width: u64,
style: Option<Arc<Style>>,
},
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 {
grid_id: u64,
grid: CharacterGrid,
@ -123,18 +92,18 @@ impl Window {
grid_top: self.grid_top,
width: self.grid.width,
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) {
let character = match self.grid.get_cell(window_left, window_top) {
Some(Some((character, _))) => character.clone(),
Some((character, _)) => character.clone(),
_ => ' '.to_string(),
};
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,
};
@ -199,80 +168,58 @@ impl Window {
// Insert the contents of the cell into the grid.
if text.is_empty() {
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;
} else {
for (i, character) in text.graphemes(true).enumerate() {
if let Some(cell) = self.grid.get_cell_mut(i as u64 + *column_pos, row_index) {
*cell = Some((character.to_string(), style.clone()));
for character in text.graphemes(true) {
if let Some(cell) = self.grid.get_cell_mut(*column_pos, row_index) {
*cell = (character.to_string(), style.clone());
}
*column_pos += 1;
}
*column_pos += text.graphemes(true).count() as u64;
}
*previous_style = 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
// line in order to ensure that ligatures before the beginning of the grid cell are also
// updated.
fn send_draw_command(
&self,
row_index: u64,
line_start: u64,
current_start: u64,
) -> Option<u64> {
// change or double width character.
fn send_draw_command(&self, row_index: u64, start: u64) -> Option<u64> {
let row = self.grid.row(row_index).unwrap();
let (_, style) = &row[current_start as usize].as_ref()?;
let mut draw_command_start_index = current_start;
if current_start == line_start {
// Locate contiguous same styled cells before the inserted cells.
// This way any ligatures are correctly rerendered.
// This could be sped up if we knew what characters were a part of a ligature, but in the
// current system we do not.
for possible_start_index in (0..current_start).rev() {
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;
}
}
let (_, style) = &row[start as usize];
let mut cells = Vec::new();
let mut width = 0;
for possible_end_index in start..self.grid.width {
let (character, possible_end_style) = &row[possible_end_index as usize];
// Style doesn't match. Draw what we've got
if style != possible_end_style {
break;
}
}
let mut draw_command_end_index = current_start;
for possible_end_index in draw_command_start_index..self.grid.width {
if let Some((_, possible_end_style)) = &row[possible_end_index as usize] {
if style == possible_end_style {
draw_command_end_index = possible_end_index;
continue;
}
width += 1;
// The previous character is double width, so send this as its own draw command
if character.is_empty() {
break;
}
break;
}
// Build up the actual text to be rendered including the contiguously styled bits.
let mut text = String::new();
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);
// Add the grid cell to the cells to render
cells.push(character.clone());
}
// Send a window draw command to the current window.
self.send_command(WindowDrawCommand::Cell {
text,
cell_width: draw_command_end_index - draw_command_start_index + 1,
window_left: draw_command_start_index,
self.send_command(WindowDrawCommand::Cells {
cells,
window_left: start,
window_top: row_index,
width,
style: style.clone(),
});
Some(draw_command_end_index + 1)
Some(start + width)
}
pub fn draw_grid_line(
@ -295,9 +242,11 @@ impl Window {
);
}
let mut current_start = column_start;
while current_start < column_pos {
if let Some(next_start) = self.send_draw_command(row, column_start, current_start) {
// Redraw the participating line by calling send_draw_command starting at 0
// until current_start is greater than the grid width
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;
} else {
break;
@ -369,7 +318,7 @@ impl Window {
for row in 0..self.grid.height {
let mut current_start = 0;
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;
} else {
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(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]
extern crate neovide_derive;
#[macro_use]
extern crate clap;
mod bridge;
mod channel_utils;
mod cmd_line;
mod editor;
mod error_handling;
mod redraw_scheduler;
@ -24,11 +35,15 @@ use std::sync::{atomic::AtomicBool, mpsc::channel, Arc};
use crossfire::mpsc::unbounded_future;
use bridge::start_bridge;
#[cfg(not(test))]
use cmd_line::CmdLineSettings;
use editor::start_editor;
use renderer::{cursor_renderer::CursorSettings, RendererSettings};
#[cfg(not(test))]
use settings::SETTINGS;
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);
fn main() {
@ -101,28 +116,21 @@ fn main() {
// 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.
if std::env::args().any(|arg| arg == "--version" || arg == "-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;
}
cmd_line::handle_command_line_arguments(); //Will exit if -h or -v
if let Err(err) = window_geometry() {
eprintln!("{}", err);
return;
}
#[cfg(not(test))]
init_logger();
#[cfg(target_os = "macos")]
{
// incase of app bundle, we can just pass --disowned option straight away to bypass this check
#[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() {
assert!(std::process::Command::new(curr_exe)
.args(std::env::args().skip(1))
@ -156,33 +164,72 @@ fn main() {
KeyboardSettings::register();
WindowSettings::register();
redraw_scheduler::RedrawSettings::register();
RendererSettings::register();
CursorSettings::register();
let running = Arc::new(AtomicBool::new(true));
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 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 logging_ui_command_sender = LoggingTx::attach(ui_command_sender, "ui_command".to_owned());
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
let _bridge = start_bridge(
ui_command_sender.clone(),
logging_ui_command_sender.clone(),
ui_command_receiver,
redraw_event_sender,
logging_redraw_event_sender,
running.clone(),
);
start_editor(
redraw_event_receiver,
batched_draw_command_sender,
window_command_sender,
logging_batched_draw_command_sender,
logging_window_command_sender,
);
create_window(
batched_draw_command_receiver,
window_command_receiver,
ui_command_sender,
logging_ui_command_sender,
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::time::Instant;
use log::trace;
use crate::settings::*;
use crate::{cmd_line::CmdLineSettings, settings::*};
lazy_static! {
pub static ref REDRAW_SCHEDULER: RedrawScheduler = RedrawScheduler::new();
@ -19,7 +19,8 @@ impl Default for RedrawSettings {
fn default() -> Self {
Self {
extra_buffer_frames: if SETTINGS
.neovim_arguments
.get::<CmdLineSettings>()
.neovim_args
.contains(&"--extraBufferFrames".to_string())
{
60
@ -31,15 +32,15 @@ impl Default for RedrawSettings {
}
pub struct RedrawScheduler {
frames_queued: AtomicU16,
scheduled_frame: Mutex<Option<Instant>>,
frame_queued: AtomicBool,
}
impl RedrawScheduler {
pub fn new() -> RedrawScheduler {
RedrawScheduler {
frames_queued: AtomicU16::new(1),
scheduled_frame: Mutex::new(None),
frame_queued: AtomicBool::new(true),
}
}
@ -58,18 +59,12 @@ impl RedrawScheduler {
pub fn queue_next_frame(&self) {
trace!("Next frame queued");
let buffer_frames = SETTINGS.get::<RedrawSettings>().extra_buffer_frames;
self.frames_queued
.store(buffer_frames as u16, Ordering::Relaxed);
self.frame_queued.store(true, Ordering::Relaxed);
}
pub fn should_draw(&self) -> bool {
let frames_queued = self.frames_queued.load(Ordering::Relaxed);
if frames_queued > 0 {
self.frames_queued
.store(frames_queued - 1, Ordering::Relaxed);
if self.frame_queued.load(Ordering::Relaxed) {
self.frame_queued.store(false, Ordering::Relaxed);
true
} else {
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)]
pub fn ease_linear(t: f32) -> f32 {

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

@ -1,5 +1,5 @@
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 crate::editor::{Colors, Cursor};
@ -11,7 +11,7 @@ pub trait CursorVfx {
&mut self,
settings: &CursorSettings,
current_cursor_destination: Point,
font_size: (f32, f32),
font_size: (u64, u64),
dt: f32,
) -> bool;
fn restart(&mut self, position: Point);
@ -21,7 +21,7 @@ pub trait CursorVfx {
canvas: &mut Canvas,
cursor: &Cursor,
colors: &Colors,
font_size: (f32, f32),
font_size: (u64, u64),
);
}
@ -111,7 +111,7 @@ impl CursorVfx for PointHighlight {
&mut self,
_settings: &CursorSettings,
_current_cursor_destination: Point,
_font_size: (f32, f32),
_font_size: (u64, u64),
dt: f32,
) -> bool {
self.t = (self.t + dt * 5.0).min(1.0); // TODO - speed config
@ -129,23 +129,23 @@ impl CursorVfx for PointHighlight {
canvas: &mut Canvas,
cursor: &Cursor,
colors: &Colors,
font_size: (f32, f32),
font_size: (u64, u64),
) {
if (self.t - 1.0).abs() < std::f32::EPSILON {
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);
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 color = Color::from_argb(alpha, base_color.r(), base_color.g(), base_color.b());
paint.set_color(color);
let size = 3.0 * font_size.1;
let radius = self.t * size;
let size = 3 * font_size.1;
let radius = self.t * size as f32;
let hr = radius * 0.5;
let rect = Rect::from_xywh(
self.center_position.x - hr,
@ -160,12 +160,12 @@ impl CursorVfx for PointHighlight {
}
HighlightMode::Ripple => {
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);
}
HighlightMode::Wireframe => {
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);
}
}
@ -218,7 +218,7 @@ impl CursorVfx for ParticleTrail {
&mut self,
settings: &CursorSettings,
current_cursor_dest: Point,
font_size: (f32, f32),
font_size: (u64, u64),
dt: f32,
) -> bool {
// Update lifetimes and remove dead particles
@ -246,7 +246,7 @@ impl CursorVfx for ParticleTrail {
let travel_distance = travel.length();
// 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
* 0.01) as usize;
@ -259,7 +259,7 @@ impl CursorVfx for ParticleTrail {
TrailMode::Railgun => {
let phase = t / std::f32::consts::PI
* 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
}
TrailMode::Torpedo => {
@ -280,7 +280,9 @@ impl CursorVfx for ParticleTrail {
let pos = match self.trail_mode {
TrailMode::Railgun => prev_p + travel * t,
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,
cursor: &Cursor,
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 {
TrailMode::Torpedo | TrailMode::Railgun => {
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);
self.particles.iter().for_each(|particle| {
let l = particle.lifetime / settings.vfx_particle_lifetime;
let alpha = (l * settings.vfx_opacity) as u8;
let lifetime = particle.lifetime / settings.vfx_particle_lifetime;
let alpha = (lifetime * settings.vfx_opacity) as u8;
let color = Color::from_argb(alpha, base_color.r(), base_color.g(), base_color.b());
paint.set_color(color);
let radius = match self.trail_mode {
TrailMode::Torpedo | TrailMode::Railgun => font_size.0 * 0.5 * l,
TrailMode::PixieDust => font_size.0 * 0.2,
TrailMode::Torpedo | TrailMode::Railgun => font_size.0 as f32 * 0.5 * lifetime,
TrailMode::PixieDust => font_size.0 as f32 * 0.2,
};
let hr = radius * 0.5;

@ -4,7 +4,7 @@ mod cursor_vfx;
use std::collections::HashMap;
// use neovide_derive::SettingGroup;
use skulpin::skia_safe::{Canvas, Paint, Path, Point};
use skia_safe::{Canvas, Paint, Path, Point};
use super::RenderedWindow;
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)];
#[derive(Clone, SettingGroup)]
#[derive(SettingGroup)]
#[setting_prefix = "cursor"]
#[derive(Clone)]
pub struct CursorSettings {
antialiasing: bool,
animation_length: f32,
@ -42,7 +43,8 @@ impl Default for CursorSettings {
fn default() -> Self {
CursorSettings {
antialiasing: true,
animation_length: 0.13,
animation_length: 0.06,
distance_length_adjust: true,
animate_in_insert_mode: true,
animate_command_line: true,
trail_size: 0.7,
@ -146,7 +148,8 @@ impl Corner {
(1.0 - settings.trail_size).max(0.0).min(1.0),
-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(
@ -223,32 +226,30 @@ impl CursorRenderer {
pub fn update_cursor_destination(
&mut self,
font_width: f32,
font_height: f32,
font_width: u64,
font_height: u64,
windows: &HashMap<u64, RenderedWindow>,
current_mode: &EditorMode,
) {
let (cursor_grid_x, cursor_grid_y) = self.cursor.grid_position;
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 mut grid_y = cursor_grid_y as f32 + window.grid_current_position.y
- (window.current_scroll - window.current_surfaces.top_line);
// 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
// grid position.
grid_y = grid_y
.max(window.grid_current_position.y)
.min(window.grid_current_position.y + window.grid_height as f32 - 1.0);
self.destination = (grid_x * font_width, grid_y * font_height).into();
}
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
- (window.current_scroll - window.current_surface.top_line as f32);
// 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
// grid position.
grid_y = grid_y
.max(window.grid_current_position.y)
.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();
} else {
self.destination = (
cursor_grid_x as f32 * font_width,
cursor_grid_y as f32 * font_height,
(cursor_grid_x * font_width) as f32,
(cursor_grid_y * font_height) as f32,
)
.into();
}
@ -257,7 +258,7 @@ impl CursorRenderer {
pub fn draw(
&mut self,
default_colors: &Colors,
font_size: (f32, f32),
font_size: (u64, u64),
current_mode: &EditorMode,
shaper: &mut CachingShaper,
canvas: &mut Canvas,
@ -272,21 +273,22 @@ impl CursorRenderer {
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);
let character = self.cursor.character.clone();
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,
};
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 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 new_cursor = Some(self.cursor.shape.clone());
@ -309,13 +311,15 @@ impl CursorRenderer {
if !center_destination.is_zero() {
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(
&settings,
font_dimensions,
center_destination,
dt,
!settings.animate_in_insert_mode && in_insert_mode
|| !settings.animate_command_line && !changed_to_from_cmdline,
immediate_movement,
);
animating |= corner_animating;
@ -338,7 +342,7 @@ impl CursorRenderer {
if self.cursor.enabled && render {
// 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
// corners.
@ -353,15 +357,20 @@ impl CursorRenderer {
canvas.draw_path(&path, &paint);
// 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.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() {
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();
@ -371,7 +380,7 @@ impl CursorRenderer {
&settings,
canvas,
&self.cursor,
&default_colors,
default_colors,
(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 lru::LruCache;
use skribo::{FontCollection, FontRef as SkriboFont, LayoutSession, TextStyle};
use skulpin::skia_safe::{Font as SkiaFont, TextBlob, TextBlobBuilder};
use skia_safe::{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_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;
#[derive(new, Clone, Hash, PartialEq, Eq, Debug)]
struct ShapeKey {
pub text: String,
pub cells: Vec<String>,
pub bold: 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 options: FontOptions,
font_set: FontSet,
pub options: Option<FontOptions>,
font_loader: FontLoader,
font_cache: LruCache<String, SkiaFont>,
blob_cache: LruCache<ShapeKey, Vec<TextBlob>>,
shape_context: ShapeContext,
}
impl 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 {
options,
font_set,
font_loader: loader,
font_cache: LruCache::new(10),
options: None,
font_loader: FontLoader::new(DEFAULT_FONT_SIZE),
blob_cache: LruCache::new(10000),
shape_context: ShapeContext::new(),
}
}
fn get_skia_font(&mut self, skribo_font: &SkriboFont) -> Option<&SkiaFont> {
let font_name = skribo_font.font.postscript_name()?;
if !self.font_cache.contains(&font_name) {
let font = build_skia_font_from_skribo_font(skribo_font, self.options.size)?;
self.font_cache.put(font_name.clone(), font);
fn current_font_pair(&mut self) -> Arc<FontPair> {
let font_key = self
.options
.as_ref()
.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 {
self.font_set
.normal
.itemize("a")
.next()
.expect("Cannot get font metrics")
.1
.font
.metrics()
pub fn font_base_dimensions(&mut self) -> (u64, u64) {
let metrics = self.metrics();
let font_height = (metrics.ascent + metrics.descent + metrics.leading).ceil() as u64;
let font_width = metrics.average_width as u64;
(font_width, font_height)
}
pub fn shape(&mut self, text: &str, bold: bool, italic: bool) -> Vec<TextBlob> {
let style = TextStyle {
size: self.options.size,
};
let session = LayoutSession::create(text, &style, &self.font_set.get(bold, italic));
pub fn underline_position(&mut self) -> u64 {
self.metrics().underline_offset as u64
}
pub fn y_adjustment(&mut self) -> u64 {
let metrics = self.metrics();
let ascent = metrics.ascent * self.options.size / metrics.units_per_em as f32;
let mut blobs = Vec::new();
(metrics.ascent + metrics.leading) as u64
}
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 skribo_font = layout_run.font();
let mut results = Vec::new();
'cluster: while parser.next(&mut cluster) {
let mut font_fallback_keys = Vec::new();
if let Some(skia_font) = self.get_skia_font(&skribo_font) {
let mut blob_builder = TextBlobBuilder::new();
let count = layout_run.glyphs().count();
let (glyphs, positions) =
blob_builder.alloc_run_pos_h(&skia_font, count, ascent, None);
// Add guifont fallback list
if let Some(options) = &self.options {
font_fallback_keys.extend(
options
.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() {
glyphs[i] = glyph.glyph_id as u16;
positions[i] = glyph.offset.x();
let mut best = None;
// Use the cluster.map function to select a viable font from the fallback list
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 {
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> {
let key = ShapeKey::new(text.to_string(), bold, italic);
pub fn shape(&mut self, cells: &[String]) -> Vec<TextBlob> {
let current_size = self.current_size();
let (glyph_width, _) = self.font_base_dimensions();
if !self.blob_cache.contains(&key) {
let blobs = self.shape(text, bold, italic);
self.blob_cache.put(key.clone(), blobs);
}
let mut resulting_blobs = Vec::new();
self.blob_cache.get(&key).unwrap()
}
let text = cells.concat();
trace!("Shaping text: {}", text);
pub fn update_font(&mut self, guifont_setting: &str) -> bool {
let updated = self.options.update(guifont_setting);
if updated {
trace!("Font changed: {:?}", self.options);
self.font_set = FontSet::new(&self.options.fallback_list, &mut self.font_loader);
self.font_cache.clear();
self.blob_cache.clear();
}
updated
}
for (cluster_group, font_pair) in self.build_clusters(&text) {
let mut shaper = self
.shape_context
.builder(font_pair.swash_font.as_ref())
.size(current_size)
.build();
pub fn font_base_dimensions(&mut self) -> (f32, f32) {
let metrics = self.metrics();
let font_height =
(metrics.ascent - metrics.descent) * self.options.size / metrics.units_per_em as f32;
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 charmap = font_pair.swash_font.as_ref().charmap();
for mut cluster in cluster_group {
cluster.map(|ch| charmap.map(ch));
shaper.add_cluster(&cluster);
}
let (font_width, _) = amounts.into_iter().max_by_key(|(_, count)| *count).unwrap();
let font_width = font_width.parse::<f32>().unwrap();
let mut glyph_data = Vec::new();
(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 {
let metrics = self.metrics();
-metrics.underline_position * self.options.size / metrics.units_per_em as f32
pub fn shape_cached(&mut self, cells: &[String], bold: bool, italic: bool) -> &Vec<TextBlob> {
let key = ShapeKey::new(cells.to_vec(), bold, italic);
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 rand::Rng;
use skribo::{FontCollection, FontFamily};
use skia_safe::{font::Edging, Data, Font, FontHinting, FontMgr, FontStyle, Typeface};
#[cfg(any(feature = "embed-fonts", test))]
use super::caching_shaper::Asset;
use super::extended_font_family::*;
use super::swash_font::SwashFont;
cfg_if! {
if #[cfg(target_os = "windows")] {
pub const SYSTEM_DEFAULT_FONT: &str = "Consolas";
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";
}
}
#[derive(RustEmbed)]
#[folder = "assets/fonts/"]
pub struct Asset;
pub const EXTRA_SYMBOL_FONT: &str = "Extra Symbols.otf";
pub const MISSING_GLYPH_FONT: &str = "Missing Glyphs.otf";
const DEFAULT_FONT: &str = "FiraCode-Regular.ttf";
pub struct FontLoader {
cache: LruCache<String, ExtendedFontFamily>,
source: SystemSource,
random_font_name: Option<String>,
pub struct FontPair {
pub skia_font: Font,
pub swash_font: SwashFont,
}
impl FontLoader {
pub fn new() -> FontLoader {
FontLoader {
cache: LruCache::new(10),
source: SystemSource::new(),
random_font_name: None,
}
}
fn get(&mut self, font_name: &str) -> Option<ExtendedFontFamily> {
self.cache.get(&String::from(font_name)).cloned()
}
impl FontPair {
fn new(mut skia_font: Font) -> Option<FontPair> {
skia_font.set_subpixel(true);
skia_font.set_hinting(FontHinting::Full);
skia_font.set_edging(Edging::SubpixelAntiAlias);
#[cfg(any(feature = "embed-fonts", test))]
fn load_from_asset(&mut self, font_name: &str) -> Option<ExtendedFontFamily> {
use font_kit::font::Font;
use skribo::FontRef as SkriboFont;
let mut family = ExtendedFontFamily::new();
let (font_data, index) = skia_font.typeface().unwrap().to_font_data().unwrap();
let swash_font = SwashFont::from_data(font_data, index)?;
if let Some(font) = Asset::get(font_name)
.and_then(|font_data| Font::from_bytes(font_data.to_vec().into(), 0).ok())
{
family.add_font(SkriboFont::new(font));
self.cache.put(String::from(font_name), family);
self.get(font_name)
} else {
None
}
Some(Self {
skia_font,
swash_font,
})
}
}
#[cfg(not(any(feature = "embed-fonts", test)))]
fn load_from_asset(&self, font_name: &str) -> Option<ExtendedFontFamily> {
log::warn!(
"Tried to load {} from assets but build didn't include embed-fonts feature",
font_name
);
None
impl PartialEq for FontPair {
fn eq(&self, other: &Self) -> bool {
self.swash_font.key == other.swash_font.key
}
}
fn load(&mut self, font_name: &str) -> Option<ExtendedFontFamily> {
let handle = match self.source.select_family_by_name(font_name) {
Ok(it) => it,
_ => return None,
};
pub struct FontLoader {
font_mgr: FontMgr,
cache: LruCache<FontKey, Arc<FontPair>>,
font_size: f32,
}
if !handle.is_empty() {
let family = ExtendedFontFamily::from(handle);
self.cache.put(String::from(font_name), family);
self.get(font_name)
} else {
None
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
pub enum FontKey {
Default,
Name(String),
Character(char),
}
fn get_random_system_font_family(&mut self) -> Option<ExtendedFontFamily> {
if let Some(font) = self.random_font_name.clone() {
self.get(&font)
} else {
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)
}
impl From<&str> for FontKey {
fn from(string: &str) -> FontKey {
let string = string.to_string();
FontKey::Name(string)
}
}
pub fn get_or_load(&mut self, font_name: &str) -> Option<ExtendedFontFamily> {
if let Some(cached) = self.get(font_name) {
Some(cached)
} else if let Some(loaded) = self.load(font_name) {
Some(loaded)
} else {
self.load_from_asset(font_name)
}
impl From<&String> for FontKey {
fn from(string: &String) -> FontKey {
let string = string.to_owned();
FontKey::Name(string)
}
}
pub fn build_collection_by_font_name(
&mut self,
fallback_list: &[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
impl From<String> for FontKey {
fn from(string: String) -> FontKey {
FontKey::Name(string)
}
}
#[cfg(test)]
mod test {
use font_kit::{
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(),
)
impl From<char> for FontKey {
fn from(character: char) -> FontKey {
FontKey::Character(character)
}
}
#[test]
fn test_build_properties() {
assert_eq!(build_properties(false, false), PROPERTIES1);
assert_eq!(build_properties(true, false), PROPERTIES2);
assert_eq!(build_properties(false, true), PROPERTIES3);
assert_eq!(build_properties(true, true), PROPERTIES4);
impl FontLoader {
pub fn new(font_size: f32) -> FontLoader {
FontLoader {
font_mgr: FontMgr::new(),
cache: LruCache::new(10),
font_size,
}
}
#[test]
fn test_load_from_asset() {
let mut loader = FontLoader::new();
let font_family = loader.load_from_asset("");
assert!(font_family.is_none());
let font = dummy_font();
let mut eft = ExtendedFontFamily::new();
eft.add_font(font.clone());
let font_family = loader.load_from_asset(EXTRA_SYMBOL_FONT);
let result = font_family.unwrap().fonts.first().unwrap().font.full_name();
assert_eq!(&result, &eft.fonts.first().unwrap().font.full_name());
assert_eq!(
&result,
&loader
.cache
.get(&EXTRA_SYMBOL_FONT.to_string())
.unwrap()
.fonts
.first()
.unwrap()
.font
.full_name()
);
fn load(&mut self, font_key: FontKey) -> Option<FontPair> {
match font_key {
FontKey::Default => {
let default_font_data = Asset::get(DEFAULT_FONT).unwrap();
let data = Data::new_copy(&default_font_data);
let typeface = Typeface::from_data(data, 0).unwrap();
FontPair::new(Font::from_typeface(typeface, self.font_size))
}
FontKey::Name(name) => {
let font_style = FontStyle::normal();
let typeface = self.font_mgr.match_family_style(name, font_style)?;
FontPair::new(Font::from_typeface(typeface, self.font_size))
}
FontKey::Character(character) => {
let font_style = FontStyle::normal();
let typeface = self.font_mgr.match_family_style_character(
"",
font_style,
&[],
character as i32,
)?;
FontPair::new(Font::from_typeface(typeface, self.font_size))
}
}
}
#[test]
fn test_load() {
let mut loader = FontLoader::new();
let junk_text = "uhasiudhaiudshiaushd";
let font_family = loader.load(junk_text);
assert!(font_family.is_none());
pub fn get_or_load(&mut self, font_key: FontKey) -> Option<Arc<FontPair>> {
if let Some(cached) = self.cache.get(&font_key) {
return Some(cached.clone());
}
#[cfg(target_os = "linux")]
const SYSTEM_DEFAULT_FONT: &str = "monospace";
let loaded_font = self.load(font_key.clone())?;
let font_family = loader.load(SYSTEM_DEFAULT_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()
);
}
let font_arc = Arc::new(loaded_font);
#[test]
fn test_get_random_system_font() {
let mut loader = FontLoader::new();
self.cache.put(font_key, font_arc.clone());
let font_family = loader.get_random_system_font_family();
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()
);
Some(font_arc)
}
}

@ -1,29 +1,16 @@
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, Debug)]
pub struct FontOptions {
previous_guifont_setting: Option<String>,
guifont_setting: Option<String>,
pub fallback_list: Vec<String>,
pub size: f32,
}
impl FontOptions {
pub fn new(name: String, size: f32) -> FontOptions {
FontOptions {
previous_guifont_setting: None,
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());
pub fn parse(guifont_setting: &str, default_size: f32) -> Option<FontOptions> {
let mut fallback_list = None;
let mut size = default_size;
let mut parts = guifont_setting.split(':').filter(|part| !part.is_empty());
let mut updated = false;
if let Some(parts) = parts.next() {
let parsed_fallback_list: Vec<String> = parts
@ -32,23 +19,34 @@ impl FontOptions {
.map(|fallback| fallback.to_string())
.collect();
if !parsed_fallback_list.is_empty() && self.fallback_list != parsed_fallback_list {
self.fallback_list = parsed_fallback_list;
updated = true;
if !parsed_fallback_list.is_empty() {
fallback_list = Some(parsed_fallback_list);
}
}
for part in parts {
if part.starts_with('h') && part.len() > 1 {
if let Ok(size) = part[1..].parse::<f32>() {
if (self.size - size).abs() > std::f32::EPSILON {
self.size = size;
updated = true;
}
if let Ok(parsed_size) = part[1..].parse::<f32>() {
size = parsed_size
}
}
}
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;
mod extended_font_family;
mod font_loader;
mod font_options;
mod utils;
pub mod caching_shaper;
mod font_loader;
mod font_options;
mod swash_font;

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

@ -1,10 +1,10 @@
use std::collections::VecDeque;
use skulpin::skia_safe::canvas::{SaveLayerRec, SrcRectConstraint};
use skulpin::skia_safe::gpu::SurfaceOrigin;
use skulpin::skia_safe::{
use skia_safe::canvas::{SaveLayerRec, SrcRectConstraint};
use skia_safe::gpu::SurfaceOrigin;
use skia_safe::{
image_filters::blur, BlendMode, Budgeted, Canvas, Color, Image, ImageInfo, Paint, Point, Rect,
Surface,
SamplingOptions, Surface, SurfaceProps, SurfacePropsFlags,
};
use super::animation_utils::*;
@ -14,11 +14,11 @@ use crate::redraw_scheduler::REDRAW_SCHEDULER;
fn build_window_surface(
parent_canvas: &mut Canvas,
pixel_width: i32,
pixel_height: i32,
pixel_width: u64,
pixel_height: u64,
) -> Surface {
let dimensions = (pixel_width, pixel_height);
let mut context = parent_canvas.gpu_context().unwrap();
let dimensions = (pixel_width as i32, pixel_height as i32);
let mut context = parent_canvas.recording_context().unwrap();
let budgeted = Budgeted::Yes;
let parent_image_info = parent_canvas.image_info();
let image_info = ImageInfo::new(
@ -28,13 +28,15 @@ fn build_window_surface(
parent_image_info.color_space(),
);
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(
&mut context,
budgeted,
&image_info,
None,
surface_origin,
None,
Some(&props),
None,
)
.expect("Could not create surface")
@ -47,59 +49,35 @@ fn build_window_surface_with_grid_size(
grid_height: u64,
scaling: f32,
) -> Surface {
let pixel_width = (grid_width as f32 * renderer.font_width / scaling) as i32;
let pixel_height = (grid_height as f32 * renderer.font_height / scaling) as i32;
build_window_surface(parent_canvas, pixel_width, pixel_height)
}
let pixel_width = ((grid_width * renderer.font_width) as f32 / scaling) as u64;
let pixel_height = ((grid_height * renderer.font_height) as f32 / scaling) as u64;
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();
canvas.clear(renderer.get_default_background());
surface
}
pub struct SnapshotPair {
background: Image,
foreground: Image,
top_line: f32,
pub struct LocatedSnapshot {
image: Image,
top_line: u64,
}
pub struct SurfacePair {
background: Surface,
foreground: Surface,
pub top_line: f32,
pub struct LocatedSurface {
surface: Surface,
pub top_line: u64,
}
impl SurfacePair {
impl LocatedSurface {
fn new(
parent_canvas: &mut Canvas,
renderer: &Renderer,
grid_width: u64,
grid_height: u64,
top_line: f32,
top_line: u64,
scaling: f32,
) -> SurfacePair {
let background = build_background_window_surface(
parent_canvas,
renderer,
grid_width,
grid_height,
scaling,
);
let foreground = build_window_surface_with_grid_size(
) -> LocatedSurface {
let surface = build_window_surface_with_grid_size(
parent_canvas,
renderer,
grid_width,
@ -107,31 +85,25 @@ impl SurfacePair {
scaling,
);
SurfacePair {
background,
foreground,
top_line,
}
LocatedSurface { surface, top_line }
}
fn snapshot(&mut self) -> SnapshotPair {
let background = self.background.image_snapshot();
let foreground = self.foreground.image_snapshot();
SnapshotPair {
background,
foreground,
fn snapshot(&mut self) -> LocatedSnapshot {
let image = self.surface.image_snapshot();
LocatedSnapshot {
image,
top_line: self.top_line,
}
}
}
pub struct RenderedWindow {
snapshots: VecDeque<SnapshotPair>,
pub current_surfaces: SurfacePair,
snapshots: VecDeque<LocatedSnapshot>,
pub current_surface: LocatedSurface,
pub id: u64,
pub hidden: bool,
pub floating: bool,
pub floating_order: Option<u64>,
pub grid_width: u64,
pub grid_height: u64,
@ -150,7 +122,7 @@ pub struct RenderedWindow {
pub struct WindowDrawDetails {
pub id: u64,
pub region: Rect,
pub floating: bool,
pub floating_order: Option<u64>,
}
impl RenderedWindow {
@ -163,21 +135,15 @@ impl RenderedWindow {
grid_height: u64,
scaling: f32,
) -> RenderedWindow {
let current_surfaces = SurfacePair::new(
parent_canvas,
renderer,
grid_width,
grid_height,
0.0,
scaling,
);
let current_surface =
LocatedSurface::new(parent_canvas, renderer, grid_width, grid_height, 0, scaling);
RenderedWindow {
snapshots: VecDeque::new(),
current_surfaces,
current_surface,
id,
hidden: false,
floating: false,
floating_order: None,
grid_width,
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(
self.grid_current_position.x * font_width,
self.grid_current_position.y * font_height,
self.grid_current_position.x * font_width as f32,
self.grid_current_position.y * font_height as f32,
);
let image_width = (self.grid_width as f32 * font_width) as i32;
let image_height = (self.grid_height as f32 * font_height) as i32;
let image_width = (self.grid_width * font_width) as i32;
let image_height = (self.grid_height * font_height) as i32;
Rect::from_point_and_size(current_pixel_position, (image_width, image_height))
}
@ -210,7 +176,7 @@ impl RenderedWindow {
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
self.position_t = 2.0;
} 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
self.scroll_t = 2.0;
self.snapshots.clear();
@ -253,8 +219,8 @@ impl RenderedWindow {
root_canvas: &mut Canvas,
settings: &RendererSettings,
default_background: Color,
font_width: f32,
font_height: f32,
font_width: u64,
font_height: u64,
dt: f32,
) -> WindowDrawDetails {
if self.update(settings, dt) {
@ -266,7 +232,7 @@ impl RenderedWindow {
root_canvas.save();
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 save_layer_rec = SaveLayerRec::default()
.backdrop(&blur)
@ -279,79 +245,46 @@ impl RenderedWindow {
// 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
paint.set_blend_mode(BlendMode::Src);
paint.set_anti_alias(false);
{
// Save layer so that setting the blend mode doesn't effect the blur
root_canvas.save_layer(&SaveLayerRec::default());
let mut a = 255;
if self.floating {
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();
// Save layer so that setting the blend mode doesn't effect the blur
root_canvas.save_layer(&SaveLayerRec::default());
let mut a = 255;
if self.floating_order.is_some() {
a = (settings.floating_opacity.min(1.0).max(0.0) * 255.0) as u8;
}
{
paint.set_color(Color::WHITE);
// 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,
);
}
paint.set_color(default_background.with_a(a));
root_canvas.draw_rect(pixel_region, &paint);
// Draw foreground
let scroll_offset =
self.current_surfaces.top_line * font_height - self.current_scroll * font_height;
let foreground_snapshot = self.current_surfaces.foreground.image_snapshot();
paint.set_color(Color::from_argb(a, 255, 255, 255));
// Draw scrolling snapshots
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(
foreground_snapshot,
image,
None,
pixel_region.with_offset((0.0, scroll_offset)),
pixel_region.with_offset((0.0, scroll_offset as f32)),
&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();
}
@ -360,7 +293,7 @@ impl RenderedWindow {
WindowDrawDetails {
id: self.id,
region: pixel_region,
floating: self.floating,
floating_order: self.floating_order,
}
}
@ -376,7 +309,7 @@ impl RenderedWindow {
grid_top,
width: grid_width,
height: grid_height,
floating,
floating_order,
} => {
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.grid_start_position = self.grid_current_position;
self.grid_destination = new_destination;
} else {
// 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.
self.position_t = 2.0;
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 {
{
let mut old_background = self.current_surfaces.background;
self.current_surfaces.background = build_background_window_surface(
old_background.canvas(),
&renderer,
grid_width,
grid_height,
scaling,
);
old_background.draw(
self.current_surfaces.background.canvas(),
(0.0, 0.0),
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,
);
}
let mut old_surface = self.current_surface.surface;
self.current_surface.surface = build_window_surface_with_grid_size(
old_surface.canvas(),
renderer,
grid_width,
grid_height,
scaling,
);
old_surface.draw(
self.current_surface.surface.canvas(),
(0.0, 0.0),
SamplingOptions::default(),
None,
);
self.grid_width = grid_width;
self.grid_height = grid_height;
}
self.floating = floating;
self.floating_order = floating_order;
if self.hidden {
self.hidden = false;
@ -442,30 +357,21 @@ impl RenderedWindow {
self.grid_destination = new_destination;
}
}
WindowDrawCommand::Cell {
text,
cell_width,
WindowDrawCommand::Cells {
cells,
window_left,
window_top,
width,
style,
} => {
let grid_position = (window_left, window_top);
{
let canvas = self.current_surfaces.background.canvas();
canvas.save();
canvas.scale((1.0 / scaling, 1.0 / scaling));
renderer.draw_background(canvas, grid_position, cell_width, &style);
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();
}
let canvas = self.current_surface.surface.canvas();
canvas.save();
canvas.scale((1.0 / scaling, 1.0 / scaling));
renderer.draw_background(canvas, grid_position, width, &style);
renderer.draw_foreground(canvas, &cells, grid_position, width, &style);
canvas.restore();
}
WindowDrawCommand::Scroll {
top,
@ -476,64 +382,37 @@ impl RenderedWindow {
cols,
} => {
let scrolled_region = Rect::new(
left as f32 * renderer.font_width / scaling,
top as f32 * renderer.font_height / scaling,
right as f32 * renderer.font_width / scaling,
bot as f32 * renderer.font_height / scaling,
(left * renderer.font_width) as f32 / scaling,
(top * renderer.font_height) as f32 / scaling,
(right * renderer.font_width) as f32 / scaling,
(bot * renderer.font_height) as f32 / scaling,
);
let mut translated_region = scrolled_region;
translated_region.offset((
-cols as f32 * renderer.font_width / scaling,
-rows as f32 * renderer.font_height / scaling,
(-cols * renderer.font_width as i64) as f32 / scaling,
(-rows * renderer.font_height as i64) as f32 / scaling,
));
{
let background_snapshot = self.current_surfaces.background.image_snapshot();
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();
let snapshot = self.current_surface.surface.image_snapshot();
let canvas = self.current_surface.surface.canvas();
foreground_canvas.save();
foreground_canvas.clip_rect(scrolled_region, None, Some(false));
canvas.save();
canvas.clip_rect(scrolled_region, None, Some(false));
foreground_canvas.draw_image_rect(
foreground_snapshot,
Some((&scrolled_region, SrcRectConstraint::Fast)),
translated_region,
&renderer.paint,
);
canvas.draw_image_rect(
snapshot,
Some((&scrolled_region, SrcRectConstraint::Fast)),
translated_region,
&renderer.paint,
);
foreground_canvas.restore();
}
canvas.restore();
}
WindowDrawCommand::Clear => {
self.current_surfaces.background = build_background_window_surface(
self.current_surfaces.background.canvas(),
&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.current_surface.surface = build_window_surface_with_grid_size(
self.current_surface.surface.canvas(),
renderer,
self.grid_width,
self.grid_height,
scaling,
@ -550,15 +429,15 @@ impl RenderedWindow {
}
WindowDrawCommand::Hide => self.hidden = true,
WindowDrawCommand::Viewport { top_line, .. } => {
if (self.current_surfaces.top_line - top_line as f32).abs() > std::f32::EPSILON {
let new_snapshot = self.current_surfaces.snapshot();
if self.current_surface.top_line != top_line as u64 {
let new_snapshot = self.current_surface.snapshot();
self.snapshots.push_back(new_snapshot);
if self.snapshots.len() > 5 {
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
self.start_scroll = self.current_scroll;

@ -2,11 +2,9 @@ use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::convert::TryInto;
#[cfg(not(test))]
use flexi_logger::{Cleanup, Criterion, Duplicate, Logger, Naming};
mod from_value;
pub use from_value::FromValue;
use log::warn;
use log::trace;
use nvim_rs::Neovim;
use parking_lot::RwLock;
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
// nvim will get out of sync.
pub struct Settings {
pub neovim_arguments: Vec<String>,
settings: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
listeners: RwLock<HashMap<String, UpdateHandlerFunc>>,
readers: RwLock<HashMap<String, ReaderFunc>>,
@ -42,58 +39,13 @@ pub struct Settings {
impl Settings {
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 {
neovim_arguments,
settings: RwLock::new(HashMap::new()),
listeners: 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(
&self,
property_name: &str,
@ -122,7 +74,7 @@ impl Settings {
let read_lock = self.settings.read();
let boxed = &read_lock
.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
.downcast_ref::<T>()
.expect("Attempted to extract a settings object of the wrong type");
@ -139,7 +91,7 @@ impl Settings {
self.listeners.read().get(&name).unwrap()(value);
}
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()();
nvim.set_var(&variable_name, setting).await.ok();
}
@ -188,7 +140,10 @@ mod tests {
use tokio;
use super::*;
use crate::bridge::{create, create_nvim_command};
use crate::{
bridge::{create, create_nvim_command},
cmd_line::CmdLineSettings,
};
#[derive(Clone)]
pub struct NeovimHandler();
@ -287,6 +242,10 @@ mod tests {
let v4: String = format!("neovide_{}", v1);
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())
.await
.unwrap_or_explained_panic("Could not locate or start the neovim process");

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

@ -1,34 +1,28 @@
mod keyboard;
mod settings;
#[cfg_attr(feature = "sdl2", path = "sdl2/mod.rs")]
#[cfg_attr(feature = "winit", path = "winit/mod.rs")]
mod window_wrapper;
use crate::{
bridge::UiCommand,
channel_utils::*,
cmd_line::CmdLineSettings,
editor::{DrawCommand, WindowCommand},
renderer::Renderer,
settings::SETTINGS,
INITIAL_DIMENSIONS,
};
use crossfire::mpsc::TxUnbounded;
use skulpin::LogicalSize;
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 settings::*;
pub fn window_geometry() -> Result<(u64, u64), String> {
let prefix = "--geometry=";
std::env::args()
.find(|arg| arg.starts_with(prefix))
.map_or(Ok(INITIAL_DIMENSIONS), |arg| {
let input = &arg[prefix.len()..];
//TODO: Maybe move this parsing into cmd_line...
SETTINGS
.get::<CmdLineSettings>()
.geometry
.map_or(Ok(INITIAL_DIMENSIONS), |input| {
let invalid_parse_err = format!(
"Invalid geometry: {}\nValid format: <width>x<height>",
input
@ -66,7 +60,6 @@ pub fn window_geometry_or_default() -> (u64, u64) {
#[cfg(target_os = "windows")]
fn windows_fix_dpi() {
println!("dpi fix applied");
use winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use winapi::um::winuser::SetProcessDpiAwarenessContext;
unsafe {
@ -75,14 +68,15 @@ fn windows_fix_dpi() {
}
fn handle_new_grid_size(
new_size: LogicalSize,
new_size: (u64, u64),
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
let new_width = ((new_size.width + 1) as f32 / renderer.font_width) as u32;
let new_height = ((new_size.height + 1) as f32 / renderer.font_height) as u32;
let new_width = ((new_width + 1) / renderer.font_width) as u32;
let new_height = ((new_height + 1) / renderer.font_height) as u32;
ui_command_sender
.send(UiCommand::Resize {
width: new_width,
@ -95,16 +89,16 @@ fn handle_new_grid_size(
pub fn create_window(
batched_draw_command_receiver: Receiver<Vec<DrawCommand>>,
window_command_receiver: Receiver<WindowCommand>,
ui_command_sender: TxUnbounded<UiCommand>,
ui_command_sender: LoggingTx<UiCommand>,
running: Arc<AtomicBool>,
) {
let (width, height) = window_geometry_or_default();
let renderer = Renderer::new(batched_draw_command_receiver);
let logical_size = LogicalSize {
width: (width as f32 * renderer.font_width) as u32,
height: (height as f32 * renderer.font_height + 1.0) as u32,
};
let logical_size = (
(width * renderer.font_width) as u64,
(height * renderer.font_height + 1) as u64,
);
#[cfg(target_os = "windows")]
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;
#[derive(Clone, SettingGroup)]
pub struct WindowSettings {
pub refresh_rate: u64,
pub transparency: f32,
pub no_idle: bool,
pub transparency: f32,
pub fullscreen: bool,
pub iso_layout: bool,
}
@ -14,13 +14,14 @@ pub struct WindowSettings {
impl Default for WindowSettings {
fn default() -> Self {
Self {
refresh_rate: 60,
transparency: 1.0,
no_idle: SETTINGS
.neovim_arguments
.contains(&String::from("--noIdle")),
fullscreen: false,
iso_layout: false,
refresh_rate: 60,
no_idle: SETTINGS
.get::<CmdLineSettings>()
.neovim_args
.contains(&String::from("--noIdle")),
}
}
}

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

@ -1,5 +1,5 @@
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
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, true) => partial("?"),
(Key0, false) => normal("0"),
(Key0, true) => special(")"),
(Key0, true) => partial(")"),
(Key1, false) => normal("1"),
(Key1, true) => special("!"),
(Key1, true) => partial("!"),
(Key2, false) => partial("2"),
(Key2, true) => partial("@"),
(Key3, false) => partial("3"),

@ -1,28 +1,27 @@
#[macro_use]
mod layouts;
mod renderer;
use super::{handle_new_grid_size, keyboard::neovim_keybinding_string, settings::WindowSettings};
use crate::{
bridge::UiCommand, editor::WindowCommand, error_handling::ResultPanicExplanation,
redraw_scheduler::REDRAW_SCHEDULER, renderer::Renderer, settings::SETTINGS,
bridge::UiCommand, channel_utils::*, cmd_line::CmdLineSettings, editor::WindowCommand,
error_handling::ResultPanicExplanation, redraw_scheduler::REDRAW_SCHEDULER, renderer::Renderer,
settings::SETTINGS,
};
use crossfire::mpsc::TxUnbounded;
use image::{load_from_memory, GenericImageView, Pixel};
use layouts::handle_qwerty_layout;
use skulpin::{
ash::prelude::VkResult,
winit::{
self,
event::{
ElementState, Event, ModifiersState, MouseButton, MouseScrollDelta,
VirtualKeyCode as Keycode, WindowEvent,
},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, Icon},
use glutin::{
self,
dpi::{LogicalPosition, LogicalSize, PhysicalSize},
event::{
ElementState, Event, ModifiersState, MouseButton, MouseScrollDelta,
VirtualKeyCode as Keycode, WindowEvent,
},
CoordinateSystem, LogicalSize, PhysicalSize, PresentMode, Renderer as SkulpinRenderer,
RendererBuilder, Window, WinitWindow,
event_loop::{ControlFlow, EventLoop},
window::{self, Fullscreen, Icon},
ContextBuilder, GlProfile, WindowedContext,
};
use image::{load_from_memory, GenericImageView, Pixel};
use layouts::handle_qwerty_layout;
use renderer::SkiaRenderer;
use std::{
sync::{
atomic::{AtomicBool, Ordering},
@ -36,49 +35,47 @@ use std::{
#[folder = "assets/"]
struct Asset;
pub struct WinitWindowWrapper {
window: winit::window::Window,
skulpin_renderer: SkulpinRenderer,
pub struct GlutinWindowWrapper {
windowed_context: WindowedContext<glutin::PossiblyCurrent>,
skia_renderer: SkiaRenderer,
renderer: Renderer,
mouse_down: bool,
mouse_position: LogicalSize,
mouse_position: LogicalPosition<u32>,
mouse_enabled: bool,
grid_id_under_mouse: u64,
current_modifiers: Option<ModifiersState>,
title: String,
previous_size: LogicalSize,
previous_size: PhysicalSize<u32>,
fullscreen: bool,
cached_size: LogicalSize,
cached_position: LogicalSize,
ui_command_sender: TxUnbounded<UiCommand>,
cached_size: LogicalSize<u32>,
cached_position: LogicalPosition<u32>,
ui_command_sender: LoggingTx<UiCommand>,
window_command_receiver: Receiver<WindowCommand>,
running: Arc<AtomicBool>,
}
impl WinitWindowWrapper {
impl GlutinWindowWrapper {
pub fn toggle_fullscreen(&mut self) {
let window = self.windowed_context.window();
if self.fullscreen {
self.window.set_fullscreen(None);
window.set_fullscreen(None);
// 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.height,
));
self.window
.set_outer_position(winit::dpi::LogicalPosition::new(
self.cached_position.width,
self.cached_position.height,
));
window.set_outer_position(LogicalPosition::new(
self.cached_position.x,
self.cached_position.y,
));
} 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);
let current_position = self.window.outer_position().unwrap();
let current_position = window.outer_position().unwrap();
self.cached_position =
LogicalSize::new(current_position.x as u32, current_position.y as u32);
let handle = self.window.current_monitor();
self.window
.set_fullscreen(Some(Fullscreen::Borderless(handle)));
LogicalPosition::new(current_position.x as u32, current_position.y as u32);
let handle = window.current_monitor();
window.set_fullscreen(Some(Fullscreen::Borderless(handle)));
}
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) {
self.title = new_title;
self.window.set_title(&self.title);
self.windowed_context.window().set_title(&self.title);
}
pub fn handle_quit(&mut self) {
self.running.store(false, Ordering::Relaxed);
pub fn handle_quit(&mut self, running: &Arc<AtomicBool>) {
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(
@ -126,10 +140,15 @@ impl WinitWindowWrapper {
}
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 winit_window_wrapper = WinitWindow::new(&self.window);
let logical_position =
PhysicalSize::new(x as u32, y as u32).to_logical(winit_window_wrapper.scale_factor());
let logical_position: LogicalSize<u32> = PhysicalSize::new(x as u32, y as u32)
.to_logical(self.windowed_context.window().scale_factor());
let mut top_window_position = (0.0, 0.0);
let mut top_grid_position = None;
@ -143,20 +162,20 @@ impl WinitWindowWrapper {
top_window_position = (details.region.left, details.region.top);
top_grid_position = Some((
details.id,
LogicalSize::new(
LogicalSize::<u32>::new(
logical_position.width - details.region.left 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 {
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,
self.mouse_position = LogicalPosition::new(
(grid_position.width as u64 / self.renderer.font_width) 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 {
@ -166,12 +185,12 @@ impl WinitWindowWrapper {
// 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)
(self.mouse_position.x, self.mouse_position.y)
} 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;
let adjusted_drag_left = self.mouse_position.x
+ (window_left / self.renderer.font_width as f32) as u32;
let adjusted_drag_top = self.mouse_position.y
+ (window_top / self.renderer.font_height as f32) as u32;
(adjusted_drag_left, adjusted_drag_top)
};
@ -191,7 +210,7 @@ impl WinitWindowWrapper {
.send(UiCommand::MouseButton {
action: String::from("press"),
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();
}
@ -204,21 +223,21 @@ impl WinitWindowWrapper {
.send(UiCommand::MouseButton {
action: String::from("release"),
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();
}
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 {
return;
}
let vertical_input_type = match y {
_ if y > 0 => Some("up"),
_ if y < 0 => Some("down"),
_ if y > 0.0 => Some("up"),
_ if y < 0.0 => Some("down"),
_ => None,
};
@ -227,14 +246,14 @@ impl WinitWindowWrapper {
.send(UiCommand::Scroll {
direction: input_type.to_string(),
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();
}
let horizontal_input_type = match y {
_ if x > 0 => Some("right"),
_ if x < 0 => Some("left"),
_ if x > 0.0 => Some("right"),
_ if x < 0.0 => Some("left"),
_ => None,
};
@ -243,7 +262,7 @@ impl WinitWindowWrapper {
.send(UiCommand::Scroll {
direction: input_type.to_string(),
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();
}
@ -258,19 +277,19 @@ impl WinitWindowWrapper {
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 ignore_text_this_frame = false;
match event {
Event::LoopDestroyed => {
self.handle_quit();
self.handle_quit(running);
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
self.handle_quit();
self.handle_quit(running);
}
Event::WindowEvent {
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::MouseInput {
@ -344,12 +370,18 @@ impl WinitWindowWrapper {
}
}
pub fn draw_frame(&mut self, dt: f32) -> VkResult<bool> {
let winit_window_wrapper = WinitWindow::new(&self.window);
let new_size = winit_window_wrapper.logical_size();
pub fn draw_frame(&mut self, dt: f32) {
let window = self.windowed_context.window();
let new_size = window.inner_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 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;
@ -358,29 +390,33 @@ impl WinitWindowWrapper {
if REDRAW_SCHEDULER.should_draw() || SETTINGS.get::<WindowSettings>().no_idle {
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;
self.skulpin_renderer.draw(
&winit_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)
{
let canvas = self.skia_renderer.canvas();
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,
);
}
}
self.skia_renderer.gr_context.flush(None);
self.windowed_context.swap_buffers().unwrap();
}
}
}
pub fn start_loop(
window_command_receiver: Receiver<WindowCommand>,
ui_command_sender: TxUnbounded<UiCommand>,
ui_command_sender: LoggingTx<UiCommand>,
running: Arc<AtomicBool>,
logical_size: LogicalSize,
logical_size: (u64, u64),
renderer: Renderer,
) {
let icon = {
@ -396,102 +432,71 @@ pub fn start_loop(
log::info!("icon created");
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_inner_size(winit::dpi::LogicalSize::new(
logical_size.width,
logical_size.height,
))
.with_inner_size(logical_size)
.with_window_icon(Some(icon))
.with_maximized(std::env::args().any(|arg| arg == "--maximized"))
.build(&event_loop)
.expect("Failed to create window");
.with_maximized(SETTINGS.get::<CmdLineSettings>().maximized)
.with_decorations(!SETTINGS.get::<CmdLineSettings>().frameless);
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");
let skulpin_renderer = {
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 skia_renderer = SkiaRenderer::new(&windowed_context);
let mut window_wrapper = WinitWindowWrapper {
window: winit_window,
skulpin_renderer,
let mut window_wrapper = GlutinWindowWrapper {
windowed_context,
skia_renderer,
renderer,
mouse_down: false,
mouse_position: LogicalSize {
width: 0,
height: 0,
},
mouse_position: LogicalPosition::new(0, 0),
mouse_enabled: true,
grid_id_under_mouse: 0,
current_modifiers: None,
title: String::from("Neovide"),
previous_size: logical_size,
previous_size,
fullscreen: false,
cached_size: LogicalSize::new(0, 0),
cached_position: LogicalSize::new(0, 0),
cached_position: LogicalPosition::new(0, 0),
ui_command_sender,
window_command_receiver,
running: running.clone(),
};
let mut was_animating = false;
let previous_frame_start = Instant::now();
let mut previous_frame_start = Instant::now();
event_loop.run(move |e, _window_target, control_flow| {
if !running.load(Ordering::Relaxed) {
*control_flow = ControlFlow::Exit;
return;
std::process::exit(0);
}
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_window_commands();
window_wrapper.synchronize_settings();
window_wrapper.handle_event(e, &running);
window_wrapper.handle_event(e);
let window_commands: Vec<WindowCommand> =
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
}
}
}
let refresh_rate = { SETTINGS.get::<WindowSettings>().refresh_rate as f32 };
let expected_frame_length_seconds = 1.0 / refresh_rate;
let frame_duration = Duration::from_secs_f32(expected_frame_length_seconds);
match window_wrapper.draw_frame(dt) {
Ok(animating) => {
was_animating = animating;
}
Err(error) => {
log::error!("Render failed: {}", error);
window_wrapper.running.store(false, Ordering::Relaxed);
return;
}
if frame_start - previous_frame_start > frame_duration {
let dt = previous_frame_start.elapsed().as_secs_f32();
window_wrapper.draw_frame(dt);
previous_frame_start = frame_start;
}
let elapsed = frame_start.elapsed();
let frame_length = Duration::from_secs_f32(1.0 / refresh_rate);
if elapsed < frame_length {
*control_flow = ControlFlow::WaitUntil(Instant::now() + frame_length);
}
*control_flow = ControlFlow::WaitUntil(previous_frame_start + frame_duration)
});
}

@ -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