use std::sync::{Arc, Mutex};
use skia_safe::{
Color4f, FontMgr, Paint, Point, Rect, Size,
canvas::{Canvas, SaveLayerRec},
colors::{BLACK, WHITE},
textlayout::{
FontCollection, Paragraph, ParagraphBuilder, ParagraphStyle, TextHeightBehavior, TextIndex,
TextStyle,
},
};
use strum::IntoEnumIterator;
use strum::{EnumCount, EnumIter};
use winit::{
application::ApplicationHandler,
dpi::PhysicalSize,
event::{ElementState, KeyEvent, Modifiers, MouseScrollDelta, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop},
keyboard::{Key, NamedKey},
window::Window,
};
use crate::{
clipboard::{Clipboard, ClipboardHandle},
cmd_line::{CmdLineSettings, SRGB_DEFAULT},
renderer::{SkiaRenderer, WindowConfig, build_window_config, create_skia_renderer},
settings::Settings,
window::load_icon,
};
use super::EventPayload;
const TEXT_COLOR: Color4f = WHITE;
const BACKGROUND_COLOR: Color4f = BLACK;
const FONT_SIZE: f32 = 12.0 * 96.0 / 72.0;
const PADDING: f32 = 10.0;
const MAX_LINES: i32 = 9999;
const MIN_SIZE: PhysicalSize<u32> = PhysicalSize::new(500, 500);
const DEFAULT_SIZE: PhysicalSize<u32> = PhysicalSize::new(800, 600);
pub fn show_error_window(
message: &str,
event_loop: EventLoop<EventPayload>,
settings: Arc<Settings>,
clipboard: Arc<Mutex<Clipboard>>,
) {
let mut error_window = ErrorWindow::new(message, settings, clipboard);
event_loop.run_app(&mut error_window).ok();
}
#[derive(Debug)]
enum Scroll {
None,
Line(i32),
Page(f32),
Start,
End,
}
#[derive(EnumCount, EnumIter)]
enum PossibleScrollDirection {
None,
Up,
Down,
Both,
}
struct Paragraphs {
message: Paragraph,
help_messages: [Paragraph; PossibleScrollDirection::COUNT],
}
pub struct State {
skia_renderer: Box<dyn SkiaRenderer>,
font_collection: FontCollection,
size: PhysicalSize<u32>,
scale_factor: f64,
paragraphs: Paragraphs,
scroll: Scroll,
current_position: TextIndex,
modifiers: Modifiers,
mouse_scroll_accumulator: f32,
clipboard: ClipboardHandle,
pub should_close: bool,
}
struct ErrorWindow<'a> {
state: Option<State>,
message: &'a str,
settings: Arc<Settings>,
clipboard: Option<Arc<Mutex<Clipboard>>>,
}
impl<'a> ErrorWindow<'a> {
fn new(message: &'a str, settings: Arc<Settings>, clipboard: Arc<Mutex<Clipboard>>) -> Self {
Self { state: None, message, settings, clipboard: Some(clipboard) }
}
}
impl ApplicationHandler<EventPayload> for ErrorWindow<'_> {
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: winit::window::WindowId,
event: WindowEvent,
) {
let state = self.state.as_mut().unwrap();
state.handle_window_event(event, self.message);
if state.should_close {
event_loop.exit();
}
}
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_none() {
if let Some(clipboard) = self.clipboard.as_ref() {
let window_config = create_error_window(event_loop, &self.settings);
self.state = Some(State::new(
self.message,
window_config,
self.settings.clone(),
ClipboardHandle::new(clipboard),
));
}
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
self.clipboard.take();
}
}
impl State {
pub fn new(
message: &str,
window_config: WindowConfig,
settings: Arc<Settings>,
clipboard: ClipboardHandle,
) -> Self {
let message = message.trim_end();
let font_manager = FontMgr::new();
let mut font_collection = FontCollection::new();
font_collection.set_default_font_manager(Some(font_manager), None);
let srgb = SRGB_DEFAULT == "1";
let vsync = true;
let skia_renderer = create_skia_renderer(&window_config, srgb, vsync, settings);
window_config.window.set_visible(true);
let scale_factor = window_config.window.scale_factor();
let size = window_config.window.inner_size();
let paragraphs = create_paragraphs(message, scale_factor as f32, &font_collection);
Self {
skia_renderer,
font_collection,
size,
scale_factor,
paragraphs,
scroll: Scroll::None,
current_position: 0,
modifiers: Modifiers::default(),
mouse_scroll_accumulator: 0.0,
clipboard,
should_close: false,
}
}
pub fn window_id(&self) -> winit::window::WindowId {
self.skia_renderer.window().id()
}
pub fn handle_window_event(&mut self, event: WindowEvent, message: &str) {
match event {
WindowEvent::CloseRequested => {
self.should_close = true;
}
WindowEvent::RedrawRequested => {
self.render();
}
WindowEvent::Resized(size) => {
self.size = size;
self.skia_renderer.resize();
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.scale_factor = scale_factor;
self.paragraphs =
create_paragraphs(message, scale_factor as f32, &self.font_collection);
}
WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => {
if self.handle_keyboard_input(event, message) {
self.skia_renderer.window().request_redraw();
}
}
WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(_, y), .. } => {
self.mouse_scroll_accumulator += y * 3.0;
self.handle_mouse_scroll();
}
WindowEvent::MouseWheel { delta: MouseScrollDelta::PixelDelta(delta), .. } => {
if let Some(line_metrics) = self.paragraphs.message.get_line_metrics_at(0) {
let line_height = line_metrics.height;
self.mouse_scroll_accumulator += (delta.y / line_height) as f32;
self.handle_mouse_scroll();
}
}
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers,
_ => {}
}
}
pub fn render(&mut self) {
let (message_rect, help_message_rect) = self.layout();
let (offset, possible_scroll_direction) =
self.handle_scrolling(message_rect.height() as f64);
let canvas = self.skia_renderer.canvas();
canvas.save();
render_main_message(&self.paragraphs.message, canvas, &message_rect, offset);
render_help_message(
&self.paragraphs.help_messages[possible_scroll_direction as usize],
canvas,
&help_message_rect,
);
canvas.restore();
self.skia_renderer.flush();
self.skia_renderer.swap_buffers();
}
fn handle_keyboard_input(&mut self, event: KeyEvent, message: &str) -> bool {
if event.state != ElementState::Pressed {
return false;
}
let handled = if self.modifiers.state().control_key() {
match &event.logical_key {
Key::Character(c) => match c.as_str() {
"e" => self.scroll_line(1),
"y" => self.scroll_line(-1),
"n" => self.scroll_line(1),
_ => false,
},
_ => false,
}
} else {
match &event.logical_key {
Key::Character(c) => match c.as_str() {
"j" => self.scroll_line(1),
"g" => {
self.scroll = Scroll::Start;
true
}
"G" => {
self.scroll = Scroll::End;
true
}
"q" => {
self.should_close = true;
true
}
"y" => {
if let Some(handle) = self.clipboard.upgrade() {
if let Ok(mut clipboard) = handle.lock() {
let _ = clipboard.set_contents(message.to_string(), "+");
}
}
true
}
_ => false,
},
Key::Named(named_key) => match named_key {
NamedKey::ArrowDown => self.scroll_line(1),
NamedKey::ArrowUp => self.scroll_line(-1),
NamedKey::Space => self.scroll_page(1.0),
NamedKey::Enter => self.scroll_line(1),
NamedKey::Home => {
self.scroll = Scroll::Start;
true
}
NamedKey::End => {
self.scroll = Scroll::End;
true
}
NamedKey::Escape => {
self.should_close = true;
true
}
_ => false,
},
_ => false,
}
};
if handled {
return true;
}
match event.logical_key {
Key::Character(c) => match c.as_str() {
"k" => self.scroll_line(-1),
"d" => self.scroll_page(0.5),
"u" => self.scroll_page(-0.5),
"f" => self.scroll_page(1.0),
"b" => self.scroll_page(-1.0),
_ => false,
},
_ => false,
}
}
fn handle_mouse_scroll(&mut self) {
let tolerance: f32 = 1.0 / 1000000.0;
let lines = (self.mouse_scroll_accumulator
+ self.mouse_scroll_accumulator.signum() * tolerance)
.trunc() as i32;
if lines != 0 {
self.scroll_line(-lines);
self.mouse_scroll_accumulator -= lines as f32;
if self.mouse_scroll_accumulator.abs() < tolerance {
self.mouse_scroll_accumulator = 0.0
}
self.skia_renderer.window().request_redraw();
}
}
fn scroll_line(&mut self, count: i32) -> bool {
self.scroll = match self.scroll {
Scroll::Line(prev_count) => Scroll::Line(prev_count + count),
_ => Scroll::Line(count),
};
true
}
fn scroll_page(&mut self, amount: f32) -> bool {
self.scroll = match self.scroll {
Scroll::Page(prev_amount) => Scroll::Page(prev_amount + amount),
_ => Scroll::Page(amount),
};
true
}
fn handle_scrolling(&mut self, allowed_height: f64) -> (f64, PossibleScrollDirection) {
let metrics = self.paragraphs.message.get_line_metrics();
let mut current_line =
metrics.partition_point(|v| v.start_index <= self.current_position) - 1;
let lines_to_scroll = match self.scroll {
Scroll::Line(lines) => lines,
Scroll::Page(amount) => {
let mut height = 0.0;
let count = metrics[current_line..]
.iter()
.take_while(|line| {
height += line.height;
height <= allowed_height
})
.count() as f32;
(count * amount).round() as i32
}
Scroll::Start => -MAX_LINES,
Scroll::End => MAX_LINES,
Scroll::None => 0,
};
current_line =
(current_line as i32 + lines_to_scroll).clamp(0, metrics.len() as i32 - 1) as usize;
let mut current_line_metrics = &metrics[current_line];
self.scroll = Scroll::None;
let mut offset = current_line_metrics.baseline - current_line_metrics.ascent;
let last_line_metrix = metrics.last().unwrap();
let last_line_pos = last_line_metrix.baseline + last_line_metrix.descent;
while current_line > 0
&& (allowed_height - current_line_metrics.height) > last_line_pos - offset
{
current_line -= 1;
current_line_metrics = &metrics[current_line];
offset = current_line_metrics.baseline - current_line_metrics.ascent;
}
self.current_position = current_line_metrics.start_index;
let can_scroll_up = current_line > 0;
let can_scroll_down = last_line_pos - offset > allowed_height;
let possible_scroll_direction = match (can_scroll_up, can_scroll_down) {
(true, true) => PossibleScrollDirection::Both,
(true, false) => PossibleScrollDirection::Up,
(false, true) => PossibleScrollDirection::Down,
(false, false) => PossibleScrollDirection::None,
};
(current_line_metrics.baseline - current_line_metrics.ascent, possible_scroll_direction)
}
fn layout(&mut self) -> (Rect, Rect) {
let window_size = Size::new(self.size.width as f32, self.size.height as f32);
let message_width = window_size.width - 2.0 * PADDING;
self.paragraphs.message.layout(message_width);
for p in &mut self.paragraphs.help_messages {
p.layout(message_width);
}
let help_message_height =
self.paragraphs.help_messages.iter().map(|p| p.height()).reduce(f32::max).unwrap()
+ PADDING;
let message_rect = Rect::from_point_and_size(
Point::new(PADDING, PADDING),
Size::new(
window_size.width - 2.0 * PADDING,
window_size.height - 2.0 * PADDING - help_message_height,
),
);
let help_message_rect = Rect::from_point_and_size(
Point::new(0.0, message_rect.bottom + PADDING),
Size::new(window_size.width, help_message_height),
);
(message_rect, help_message_rect)
}
}
fn render_main_message(message: &Paragraph, canvas: &Canvas, rect: &Rect, offset: f64) {
canvas.clear(BACKGROUND_COLOR);
let save_layer_rec = SaveLayerRec::default().bounds(rect);
canvas.save_layer(&save_layer_rec);
message.paint(canvas, Point::new(PADDING, PADDING - offset as f32));
canvas.restore();
}
fn render_help_message(message: &Paragraph, canvas: &Canvas, help_message_rect: &Rect) {
let help_message_text_point =
Point::new(help_message_rect.left + PADDING, help_message_rect.top);
canvas.draw_rect(help_message_rect, &Paint::new(TEXT_COLOR, None));
message.paint(canvas, help_message_text_point);
}
fn create_paragraphs(
message: &str,
scale_factor: f32,
font_collection: &FontCollection,
) -> Paragraphs {
let mut normal_text = TextStyle::new();
normal_text.set_font_families(&["monospace"]);
normal_text.set_foreground_paint(&Paint::new(TEXT_COLOR, None));
normal_text.set_font_size(FONT_SIZE * scale_factor);
let mut inverted_text = normal_text.clone();
inverted_text.set_foreground_paint(&Paint::new(BACKGROUND_COLOR, None));
let mut paragraph_style = ParagraphStyle::new();
paragraph_style.set_text_style(&normal_text);
paragraph_style.set_max_lines(MAX_LINES as usize);
paragraph_style.set_text_height_behavior(TextHeightBehavior::DisableAll);
let create_message = |message: &str, style| {
let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection);
paragraph_builder.push_style(style);
paragraph_builder.add_text(message);
paragraph_builder.pop();
paragraph_builder.build()
};
let message_line = "quit (q), copy (y)";
let help_messages = PossibleScrollDirection::iter()
.map(|dir| match dir {
PossibleScrollDirection::None => message_line.to_owned(),
PossibleScrollDirection::Down => message_line.to_owned() + " ↓",
PossibleScrollDirection::Up => message_line.to_owned() + " ↑",
PossibleScrollDirection::Both => message_line.to_owned() + " ↑↓",
})
.map(|msg| create_message(&msg, &inverted_text))
.collect::<Vec<Paragraph>>()
.try_into()
.unwrap();
Paragraphs { message: create_message(message, &normal_text), help_messages }
}
pub fn create_error_window(event_loop: &ActiveEventLoop, settings: &Settings) -> WindowConfig {
let cmd_line_settings = settings.get::<CmdLineSettings>();
let icon = load_icon(cmd_line_settings.icon.as_ref());
let window_attributes = Window::default_attributes()
.with_title("Neovide")
.with_window_icon(Some(icon))
.with_transparent(false)
.with_visible(true)
.with_decorations(true)
.with_inner_size(DEFAULT_SIZE)
.with_min_inner_size(MIN_SIZE);
build_window_config(window_attributes, event_loop, settings)
}