feat: add animated progress bar with nvim_echo event support (#3279)

This commit is contained in:
Alexsander Falcucci 2025-11-09 13:20:34 +01:00 committed by GitHub
parent 71abe21672
commit faf1d8ad37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 223 additions and 4 deletions

View file

@ -166,4 +166,25 @@ vim.api.nvim_create_user_command("NeovideConfig", function()
end
end, {})
local function progress_bar(data)
-- Wrap inside pcall to avoid errors if Neovide disconnects.
pcall(rpcnotify, "neovide.progress_bar", data)
end
pcall(vim.api.nvim_create_autocmd, 'Progress', {
group = vim.api.nvim_create_augroup('NeovideProgressBar', { clear = true }),
desc = 'Forward progress events to Neovide',
callback = function(ev)
if ev.data and ev.data.status == 'running' then
progress_bar({
percent = ev.data.percent or 0,
title = ev.data.title or "",
message = ev.data.message or "",
})
else
progress_bar({ percent = 100 })
end
end,
})
_G["neovide"] = M

View file

@ -9,7 +9,10 @@ use rmpv::Value;
use skia_safe::Color4f;
use strum::AsRefStr;
use crate::editor::{Colors, CursorMode, CursorShape, Style, UnderlineStyle};
use crate::{
editor::{Colors, CursorMode, CursorShape, Style, UnderlineStyle},
window::UserEvent,
};
#[derive(Clone, Debug)]
pub enum ParseError {
@ -1063,3 +1066,14 @@ pub fn parse_redraw_event(event_value: Value) -> Result<Vec<RedrawEvent>> {
Ok(parsed_events)
}
pub fn parse_progress_bar_event(value: Option<&Value>) -> Option<UserEvent> {
let map = value.filter(|v| matches!(v, Value::Map(_)))?.as_map()?;
let percent = map
.iter()
.find(|(key, _)| key.as_str() == Some("percent"))
.and_then(|(_, value)| value.as_f64())
.unwrap_or(0.0) as f32;
Some(UserEvent::ShowProgressBar { percent })
}

View file

@ -11,7 +11,7 @@ use crate::{
bridge::{
clipboard::{get_clipboard_contents, set_clipboard_contents},
events::parse_redraw_event,
send_ui, NeovimWriter, ParallelCommand, RedrawEvent,
parse_progress_bar_event, send_ui, NeovimWriter, ParallelCommand, RedrawEvent,
},
error_handling::ResultPanicExplanation,
running_tracker::RunningTracker,
@ -134,6 +134,18 @@ impl Handler for NeovimHandler {
let _ = self.sender.send(RedrawEvent::NeovideSetRedraw(value));
}
}
"neovide.progress_bar" => {
parse_progress_bar_event(arguments.first())
.map(|event| {
let _ = self.proxy.lock().unwrap().send_event(event);
})
.unwrap_or_else(|| {
log::info!(
"Failed to parse neovide.progress_bar event data: {:?}",
arguments
);
});
}
_ => {}
}
}

View file

@ -225,7 +225,6 @@ impl ParallelCommand {
ParallelCommand::DisplayAvailableFonts(fonts) => display_available_fonts(nvim, fonts)
.await
.context("DisplayAvailableFonts failed"),
ParallelCommand::ShowError { lines } => {
// nvim.err_write(&message).await.ok();
// NOTE: https://github.com/neovim/neovim/issues/5067

View file

@ -63,7 +63,9 @@ use backtrace::Backtrace;
use bridge::NeovimRuntime;
use cmd_line::CmdLineSettings;
use error_handling::handle_startup_errors;
use renderer::{cursor_renderer::CursorSettings, RendererSettings};
use renderer::{
cursor_renderer::CursorSettings, progress_bar::ProgressBarSettings, RendererSettings,
};
use running_tracker::RunningTracker;
use window::{
create_event_loop, determine_window_size, UpdateLoop, UserEvent, WindowSettings, WindowSize,
@ -215,6 +217,7 @@ fn setup(
settings.register::<WindowSettings>();
settings.register::<RendererSettings>();
settings.register::<CursorSettings>();
settings.register::<ProgressBarSettings>();
let config = Config::init();
Config::watch_config_file(config.clone(), proxy.clone());

View file

@ -5,6 +5,7 @@ pub mod fonts;
pub mod grid_renderer;
pub mod opengl;
pub mod profiler;
pub mod progress_bar;
mod rendered_layer;
mod rendered_window;
mod vsync;
@ -23,6 +24,7 @@ use std::{
use itertools::Itertools;
use log::error;
use progress_bar::{ProgressBar, ProgressBarSettings};
use skia_safe::Canvas;
use winit::{
@ -153,6 +155,8 @@ pub struct Renderer {
pub grid_renderer: GridRenderer,
current_mode: EditorMode,
pub progress_bar: ProgressBar,
rendered_windows: HashMap<u64, RenderedWindow>,
pub window_regions: Vec<WindowDrawDetails>,
@ -186,6 +190,8 @@ impl Renderer {
let profiler = profiler::Profiler::new(12.0, settings.clone());
let progress_bar = ProgressBar::new();
Renderer {
rendered_windows,
cursor_renderer,
@ -193,6 +199,7 @@ impl Renderer {
current_mode,
window_regions,
profiler,
progress_bar,
os_scale_factor,
user_scale_factor,
settings,
@ -327,8 +334,18 @@ impl Renderer {
self.profiler.draw(root_canvas, dt);
let grid_size = self.get_grid_size();
root_canvas.restore();
let progress_bar_settings = self.settings.get::<ProgressBarSettings>();
self.progress_bar.draw(
&progress_bar_settings,
root_canvas,
&self.grid_renderer,
grid_size,
);
#[cfg(feature = "profiling")]
plot_skia_cache();
}
@ -368,6 +385,10 @@ impl Renderer {
.cursor_renderer
.animate(&self.current_mode, &self.grid_renderer, dt);
let progress_bar_settings = self.settings.get::<ProgressBarSettings>();
self.progress_bar.animate(&progress_bar_settings, dt);
animating |= self.progress_bar.is_animating();
animating
}

View file

@ -0,0 +1,114 @@
use std::time::Instant;
use crate::{renderer::GridRenderer, settings::ParseFromValue};
use neovide_derive::SettingGroup;
use skia_safe::{Canvas, Color4f, Paint, Rect};
#[derive(Clone, SettingGroup)]
#[setting_prefix = "progress_bar"]
pub struct ProgressBarSettings {
pub enabled: bool,
pub height: f32,
pub animation_speed: f32,
pub hide_delay: f32,
}
impl Default for ProgressBarSettings {
fn default() -> Self {
Self {
enabled: true,
height: 3.0,
animation_speed: 100.0,
hide_delay: 0.5,
}
}
}
enum ProgressBarState {
Idle,
Animating,
Completing { completion_time: Instant },
}
pub struct ProgressBar {
current_percent: f32,
target_percent: f32,
state: ProgressBarState,
}
impl ProgressBar {
pub fn new() -> Self {
Self {
current_percent: 0.0,
target_percent: 0.0,
state: ProgressBarState::Idle,
}
}
pub fn is_animating(&self) -> bool {
!matches!(self.state, ProgressBarState::Idle)
}
pub fn start(&mut self, percent: f32) {
self.target_percent = percent.clamp(0.0, 100.0);
if self.target_percent < self.current_percent {
self.current_percent = self.target_percent;
}
self.state = ProgressBarState::Animating;
}
pub fn animate(&mut self, settings: &ProgressBarSettings, dt: f32) {
match &self.state {
ProgressBarState::Idle => {}
ProgressBarState::Animating => {
if self.current_percent < self.target_percent {
self.current_percent += settings.animation_speed * dt;
// here we clamp to the target to prevent overshooting.
self.current_percent = self.current_percent.min(self.target_percent);
}
if self.current_percent >= 100.0 {
self.state = ProgressBarState::Completing {
completion_time: Instant::now(),
};
}
}
ProgressBarState::Completing { completion_time } => {
if completion_time.elapsed().as_secs_f32() > settings.hide_delay {
self.state = ProgressBarState::Idle;
// Reset percents for next time
self.current_percent = 0.0;
self.target_percent = 0.0;
}
}
}
}
pub fn draw(
&self,
settings: &ProgressBarSettings,
canvas: &Canvas,
grid_renderer: &GridRenderer,
grid_size: crate::units::GridSize<u32>,
) {
if !self.is_animating() || !settings.enabled {
return;
}
let width = grid_size.width as f32 * grid_renderer.grid_scale.width();
let height = settings.height;
let x = 0.0;
let y = 0.0;
let foreground_color = grid_renderer
.default_style
.colors
.foreground
.unwrap()
.to_color();
let mut paint = Paint::new(Color4f::from(foreground_color), None);
paint.set_anti_alias(true);
let rect = Rect::from_xywh(x, y, width * (self.current_percent / 100.0), height);
canvas.draw_rect(rect, &paint);
}
}

View file

@ -189,6 +189,7 @@ pub enum SettingsChanged {
Window(crate::window::WindowSettingsChanged),
Cursor(crate::renderer::cursor_renderer::CursorSettingsChanged),
Renderer(crate::renderer::RendererSettingsChanged),
ProgressBar(crate::renderer::progress_bar::ProgressBarSettingsChanged),
#[cfg(test)]
Test(tests::TestSettingsChanged),
}

View file

@ -96,6 +96,9 @@ pub enum UserEvent {
#[allow(dead_code)]
RedrawRequested,
NeovimExited,
ShowProgressBar {
percent: f32,
},
}
impl From<Vec<DrawCommand>> for UserEvent {

View file

@ -432,6 +432,9 @@ impl WinitWindowWrapper {
UserEvent::ConfigsChanged(config) => {
self.handle_config_changed(*config);
}
UserEvent::ShowProgressBar { percent, .. } => {
self.renderer.progress_bar.start(percent);
}
_ => {}
}
}

View file

@ -447,6 +447,34 @@ When scrolling more than one screen at a time, only this many lines at the end o
will be animated. Set it to 0 to snap to the final position without any animation, or to something
big like 9999 to always scroll the whole screen, much like Neovide <= 0.10.4 did.
#### Progress Bar
VimScript:
```vim
let g:neovide_progress_bar_enabled = v:true
let g:neovide_progress_bar_height = 5.0
let g:neovide_progress_bar_animation_speed = 200.0
let g:neovide_progress_bar_hide_delay = 0.2
```
Lua:
```lua
vim.g.neovide_progress_bar_enabled = true
vim.g.neovide_progress_bar_height = 5.0
vim.g.neovide_progress_bar_animation_speed = 200.0
vim.g.neovide_progress_bar_hide_delay = 0.2
```
**Unreleased yet.**
- `g:neovide_progress_bar_enabled` sets whether the progress bar is enabled.
- `g:neovide_progress_bar_height` sets the height of the progress bar in pixels.
- `g:neovide_progress_bar_animation_speed` sets the speed of the progress bar animation.
- `g:neovide_progress_bar_hide_delay` sets the delay in seconds before the progress bar is
hidden after reaching 100%.
#### Hiding the mouse when typing
VimScript: