mirror of https://github.com/sgoudham/neovide.git
Opengl (#655)
* 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
parent
c6a68915a2
commit
c7694569bd
@ -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 }}
|
File diff suppressed because it is too large
Load Diff
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.
|
Binary file not shown.
@ -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
|
@ -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,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,
|
||||
pub struct CachingShaper {
|
||||
pub options: Option<FontOptions>,
|
||||
font_loader: FontLoader,
|
||||
blob_cache: LruCache<ShapeKey, Vec<TextBlob>>,
|
||||
shape_context: ShapeContext,
|
||||
}
|
||||
|
||||
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)),
|
||||
impl CachingShaper {
|
||||
pub fn new() -> CachingShaper {
|
||||
CachingShaper {
|
||||
options: None,
|
||||
font_loader: FontLoader::new(DEFAULT_FONT_SIZE),
|
||||
blob_cache: LruCache::new(10000),
|
||||
shape_context: ShapeContext::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, bold: bool, italic: bool) -> &FontCollection {
|
||||
match (bold, italic) {
|
||||
(true, _) => &self.bold,
|
||||
(false, false) => &self.normal,
|
||||
(false, true) => &self.italic,
|
||||
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 struct CachingShaper {
|
||||
pub options: FontOptions,
|
||||
font_set: FontSet,
|
||||
font_loader: FontLoader,
|
||||
font_cache: LruCache<String, SkiaFont>,
|
||||
blob_cache: LruCache<ShapeKey, Vec<TextBlob>>,
|
||||
}
|
||||
pub fn update_font(&mut self, guifont_setting: &str) -> bool {
|
||||
let new_options = FontOptions::parse(guifont_setting, DEFAULT_FONT_SIZE);
|
||||
|
||||
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);
|
||||
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;
|
||||
|
||||
CachingShaper {
|
||||
options,
|
||||
font_set,
|
||||
font_loader: loader,
|
||||
font_cache: LruCache::new(10),
|
||||
blob_cache: LruCache::new(10000),
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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 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();
|
||||
|
||||
shaper.metrics()
|
||||
}
|
||||
|
||||
self.font_cache.get(&font_name)
|
||||
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)
|
||||
}
|
||||
|
||||
fn metrics(&self) -> Metrics {
|
||||
self.font_set
|
||||
.normal
|
||||
.itemize("a")
|
||||
.next()
|
||||
.expect("Cannot get font metrics")
|
||||
.1
|
||||
.font
|
||||
.metrics()
|
||||
pub fn underline_position(&mut self) -> u64 {
|
||||
self.metrics().underline_offset as u64
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
for layout_run in session.iter_all() {
|
||||
let skribo_font = layout_run.font();
|
||||
fn build_clusters(&mut self, text: &str) -> Vec<(Vec<CharCluster>, Arc<FontPair>)> {
|
||||
let mut cluster = CharCluster::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);
|
||||
// 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 (i, glyph) in layout_run.glyphs().enumerate() {
|
||||
glyphs[i] = glyph.glyph_id as u16;
|
||||
positions[i] = glyph.offset.x();
|
||||
let mut results = Vec::new();
|
||||
'cluster: while parser.next(&mut cluster) {
|
||||
let mut font_fallback_keys = Vec::new();
|
||||
|
||||
// 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());
|
||||
|
||||
blobs.push(blob_builder.make().unwrap());
|
||||
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!("Could not load skribo font");
|
||||
warn!("No valid font for {:?}", cluster.chars());
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
blobs
|
||||
// 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 {
|
||||
current_group = vec![cluster];
|
||||
current_font_option = Some(font);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape_cached(&mut self, text: &str, bold: bool, italic: bool) -> &Vec<TextBlob> {
|
||||
let key = ShapeKey::new(text.to_string(), bold, italic);
|
||||
if !current_group.is_empty() {
|
||||
grouped_results.push((current_group, current_font_option.unwrap()));
|
||||
}
|
||||
|
||||
if !self.blob_cache.contains(&key) {
|
||||
let blobs = self.shape(text, bold, italic);
|
||||
self.blob_cache.put(key.clone(), blobs);
|
||||
grouped_results
|
||||
}
|
||||
|
||||
self.blob_cache.get(&key).unwrap()
|
||||
pub fn shape(&mut self, cells: &[String]) -> Vec<TextBlob> {
|
||||
let current_size = self.current_size();
|
||||
let (glyph_width, _) = self.font_base_dimensions();
|
||||
|
||||
let mut resulting_blobs = Vec::new();
|
||||
|
||||
let text = cells.concat();
|
||||
trace!("Shaping text: {}", text);
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
let mut glyph_data = Vec::new();
|
||||
|
||||
shaper.shape_with(|glyph_cluster| {
|
||||
for glyph in glyph_cluster.glyphs {
|
||||
glyph_data.push((glyph.id, glyph.data as u64 * glyph_width));
|
||||
}
|
||||
updated
|
||||
});
|
||||
|
||||
if glyph_data.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
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 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 mut amounts = HashMap::new();
|
||||
let blob = blob_builder.make();
|
||||
resulting_blobs.push(blob.expect("Could not create textblob"));
|
||||
}
|
||||
|
||||
for advance in glyph_advances.iter() {
|
||||
amounts
|
||||
.entry(advance.to_string())
|
||||
.and_modify(|e| *e += 1)
|
||||
.or_insert(1);
|
||||
resulting_blobs
|
||||
}
|
||||
|
||||
let (font_width, _) = amounts.into_iter().max_by_key(|(_, count)| *count).unwrap();
|
||||
let font_width = font_width.parse::<f32>().unwrap();
|
||||
pub fn shape_cached(&mut self, cells: &[String], bold: bool, italic: bool) -> &Vec<TextBlob> {
|
||||
let key = ShapeKey::new(cells.to_vec(), bold, italic);
|
||||
|
||||
(font_width, font_height)
|
||||
if !self.blob_cache.contains(&key) {
|
||||
let blobs = self.shape(cells);
|
||||
self.blob_cache.put(key.clone(), blobs);
|
||||
}
|
||||
|
||||
pub fn underline_position(&mut self) -> f32 {
|
||||
let metrics = self.metrics();
|
||||
-metrics.underline_position * self.options.size / metrics.units_per_em as f32
|
||||
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()));
|
||||
}
|
||||
impl From<String> for FontKey {
|
||||
fn from(string: String) -> FontKey {
|
||||
FontKey::Name(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for FontKey {
|
||||
fn from(character: char) -> FontKey {
|
||||
FontKey::Character(character)
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
impl FontLoader {
|
||||
pub fn new(font_size: f32) -> FontLoader {
|
||||
FontLoader {
|
||||
font_mgr: FontMgr::new(),
|
||||
cache: LruCache::new(10),
|
||||
font_size,
|
||||
}
|
||||
}
|
||||
|
||||
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")));
|
||||
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))
|
||||
}
|
||||
collection
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
#[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(),
|
||||
)
|
||||
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_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);
|
||||
}
|
||||
|
||||
#[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()
|
||||
);
|
||||
}
|
||||
|
||||
#[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,5 +1,4 @@
|
||||
pub mod caching_shaper;
|
||||
mod extended_font_family;
|
||||
mod font_loader;
|
||||
mod font_options;
|
||||
mod utils;
|
||||
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,
|
||||
}
|
||||
}
|
@ -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,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;
|
||||
|
@ -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…
Reference in New Issue