pub mod routes;
mod window;
use crate::event::EventProxy;
use crate::router::window::{configure_window, create_window_builder};
use crate::screen::{Screen, ScreenWindowProperties};
use assistant::Assistant;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use rio_backend::clipboard::Clipboard;
use rio_backend::config::Config as RioConfig;
use rio_backend::error::{RioError, RioErrorLevel, RioErrorType};
use rio_window::event_loop::ActiveEventLoop;
use rio_window::keyboard::{Key, NamedKey};
#[cfg(not(any(target_os = "macos", windows)))]
use rio_window::platform::startup_notify::{
self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify,
};
use rio_window::window::{Window, WindowId};
use routes::{assistant, RoutePath};
use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
const RIO_TITLE: &str = "â–²";
pub struct Route<'a> {
pub assistant: assistant::Assistant,
pub path: RoutePath,
pub window: RouteWindow<'a>,
}
impl Route<'_> {
#[inline]
pub fn new(
assistant: assistant::Assistant,
path: RoutePath,
window: RouteWindow,
) -> Route {
Route {
assistant,
path,
window,
}
}
}
impl Route<'_> {
#[inline]
pub fn request_redraw(&mut self) {
self.window.winit_window.request_redraw();
}
#[inline]
pub fn schedule_redraw(
&mut self,
scheduler: &mut crate::scheduler::Scheduler,
route_id: usize,
) {
#[cfg(target_os = "macos")]
{
let _ = (scheduler, route_id); self.request_redraw();
}
#[cfg(not(target_os = "macos"))]
{
use crate::event::{EventPayload, RioEvent, RioEventType};
use crate::scheduler::{TimerId, Topic};
let timer_id = TimerId::new(Topic::Render, route_id);
let event = EventPayload::new(
RioEventType::Rio(RioEvent::Render),
self.window.winit_window.id(),
);
if !scheduler.scheduled(timer_id) {
scheduler.schedule(event, self.window.vblank_interval, false, timer_id);
}
}
}
#[inline]
pub fn begin_render(&mut self) {
self.window.render_timestamp = Instant::now();
}
#[inline]
pub fn update_config(
&mut self,
config: &RioConfig,
db: &rio_backend::sugarloaf::font::FontLibrary,
should_update_font: bool,
) {
self.window
.screen
.update_config(config, db, should_update_font);
}
#[inline]
#[allow(unused_variables)]
pub fn set_window_subtitle(&mut self, subtitle: &str) {
#[cfg(target_os = "macos")]
self.window.winit_window.set_subtitle(subtitle);
}
#[inline]
pub fn set_window_title(&mut self, title: &str) {
self.window.winit_window.set_title(title);
}
#[inline]
pub fn report_error(&mut self, error: &RioError) {
if error.report == RioErrorType::ConfigurationNotFound {
self.path = RoutePath::Welcome;
return;
}
self.assistant.set(error.to_owned());
self.window
.screen
.renderer
.assistant
.set_error(error.to_owned());
}
#[inline]
pub fn clear_errors(&mut self) {
self.assistant.clear();
self.window.screen.renderer.assistant.clear();
self.path = RoutePath::Terminal;
}
#[inline]
pub fn confirm_quit(&mut self) {
self.path = RoutePath::ConfirmQuit;
}
#[inline]
pub fn quit(&mut self) {
std::process::exit(0);
}
#[inline]
pub fn has_key_wait(&mut self, key_event: &rio_window::event::KeyEvent) -> bool {
use rio_window::event::ElementState;
if let Some(ref mut island) = self.window.screen.renderer.island {
if island.is_color_picker_open() {
let consumed = island.handle_rename_input(key_event);
if consumed {
self.window.screen.render();
return true;
}
}
}
if self.window.screen.renderer.command_palette.is_enabled() {
if key_event.state == ElementState::Pressed {
match &key_event.logical_key {
Key::Named(NamedKey::Escape) => {
self.window
.screen
.renderer
.command_palette
.set_enabled(false);
self.window.screen.render();
}
Key::Named(NamedKey::ArrowUp) => {
self.window
.screen
.renderer
.command_palette
.move_selection_up();
self.window.screen.render();
}
Key::Named(NamedKey::ArrowDown) => {
self.window
.screen
.renderer
.command_palette
.move_selection_down();
self.window.screen.render();
}
Key::Named(NamedKey::Tab) => {
self.window
.screen
.renderer
.command_palette
.move_selection_down();
self.window.screen.render();
}
Key::Named(NamedKey::Enter) => {
if let Some(action) = self
.window
.screen
.renderer
.command_palette
.get_selected_action()
{
self.window
.screen
.renderer
.command_palette
.set_enabled(false);
self.window.screen.execute_palette_action(action);
}
self.window.screen.render();
}
Key::Named(NamedKey::Backspace) => {
let current_query =
self.window.screen.renderer.command_palette.query.clone();
if !current_query.is_empty() {
let mut chars = current_query.chars().collect::<Vec<_>>();
chars.pop();
self.window
.screen
.renderer
.command_palette
.set_query(chars.into_iter().collect());
self.window.screen.render();
}
}
_ => {
if let Some(text) = key_event.text.as_ref() {
let text_str = text.as_str();
if !text_str.is_empty()
&& text_str.chars().all(|c| !c.is_control())
{
let current_query = self
.window
.screen
.renderer
.command_palette
.query
.clone();
self.window
.screen
.renderer
.command_palette
.set_query(format!("{}{}", current_query, text_str));
self.window.screen.render();
}
}
}
}
}
return true; }
if self.path == RoutePath::Terminal {
return false;
}
let is_enter = key_event.logical_key == Key::Named(NamedKey::Enter);
if self.window.screen.renderer.assistant.is_active() {
if is_enter {
self.assistant.clear();
self.window.screen.renderer.assistant.clear();
self.window.screen.render();
}
return true;
}
if self.path == RoutePath::ConfirmQuit {
if key_event.state == rio_window::event::ElementState::Pressed {
match &key_event.logical_key {
Key::Character(c) if c.as_str() == "n" || c.as_str() == "N" => {
self.path = RoutePath::Terminal;
}
Key::Named(NamedKey::Escape) => {
self.path = RoutePath::Terminal;
}
Key::Character(c) if c.as_str() == "y" || c.as_str() == "Y" => {
self.quit();
return true;
}
_ => {}
}
}
return true;
}
if self.path == RoutePath::Welcome && is_enter {
rio_backend::config::create_config_file(None);
self.path = RoutePath::Terminal;
}
false
}
}
pub struct Router<'a> {
pub routes: FxHashMap<WindowId, Route<'a>>,
propagated_report: Option<RioError>,
pub font_library: Box<rio_backend::sugarloaf::font::FontLibrary>,
pub config_route: Option<WindowId>,
pub clipboard: Rc<RefCell<Clipboard>>,
current_tab_id: u64,
}
impl Router<'_> {
pub fn new<'b>(
fonts: rio_backend::sugarloaf::font::SugarloafFonts,
clipboard: Clipboard,
) -> Router<'b> {
let (font_library, fonts_not_found) =
rio_backend::sugarloaf::font::FontLibrary::new(fonts);
let mut propagated_report = None;
if let Some(err) = fonts_not_found {
propagated_report = Some(RioError {
report: RioErrorType::FontsNotFound(err.fonts_not_found),
level: RioErrorLevel::Warning,
});
}
let clipboard = Rc::new(RefCell::new(clipboard));
Router {
routes: FxHashMap::default(),
propagated_report,
config_route: None,
font_library: Box::new(font_library),
clipboard,
current_tab_id: 0,
}
}
#[inline]
pub fn propagate_error_to_next_route(&mut self, error: RioError) {
self.propagated_report = Some(error);
}
#[inline]
pub fn update_titles(&mut self) {
for route in self.routes.values_mut() {
if route.window.is_focused {
route.window.screen.context_manager.update_titles();
}
}
}
#[inline]
pub fn get_focused_route(&self) -> Option<WindowId> {
self.routes
.iter()
.find_map(|(key, val)| {
if val.window.winit_window.has_focus() {
Some(key)
} else {
None
}
})
.copied()
}
pub fn open_config_window(
&mut self,
event_loop: &ActiveEventLoop,
event_proxy: EventProxy,
config: &RioConfig,
) {
if let Some(route_id) = self.config_route {
if let Some(route) = self.routes.get(&route_id) {
route.window.winit_window.focus_window();
return;
}
}
let current_config: RioConfig = config.clone();
let editor = config.editor.clone();
let mut args = editor.args;
args.push(
rio_backend::config::config_file_path()
.display()
.to_string(),
);
let new_config = RioConfig {
shell: rio_backend::config::Shell {
program: editor.program,
args,
},
..current_config
};
let window = RouteWindow::from_target(
event_loop,
event_proxy,
&new_config,
&self.font_library,
"Rio Settings",
None,
None,
self.clipboard.clone(),
None,
);
let id = window.winit_window.id();
let route = Route::new(Assistant::new(), RoutePath::Terminal, window);
self.routes.insert(id, route);
self.config_route = Some(id);
}
pub fn open_config_split(&mut self, config: &RioConfig) {
let current_config: RioConfig = config.clone();
let editor = config.editor.clone();
let mut args = editor.args;
args.push(
rio_backend::config::config_file_path()
.display()
.to_string(),
);
let new_config = RioConfig {
shell: rio_backend::config::Shell {
program: editor.program,
args,
},
..current_config
};
let window_id = match self.get_focused_route() {
Some(window_id) => window_id,
None => return,
};
let route = match self.routes.get_mut(&window_id) {
Some(window) => window,
None => return,
};
route.window.screen.split_right_with_config(new_config);
}
#[inline]
pub fn create_window<'a>(
&'a mut self,
event_loop: &'a ActiveEventLoop,
event_proxy: EventProxy,
config: &'a rio_backend::config::Config,
open_url: Option<String>,
app_id: Option<&str>,
) {
let tab_id = if config.navigation.is_native() {
let id = self.current_tab_id;
self.current_tab_id = self.current_tab_id.wrapping_add(1);
Some(id.to_string())
} else {
None
};
let window = RouteWindow::from_target(
event_loop,
event_proxy,
config,
&self.font_library,
RIO_TITLE,
tab_id.as_deref(),
open_url,
self.clipboard.clone(),
app_id,
);
let id = window.winit_window.id();
let mut route = Route {
window,
path: RoutePath::Terminal,
assistant: Assistant::new(),
};
if let Some(err) = &self.propagated_report {
route.report_error(err);
self.propagated_report = None;
}
self.routes.insert(id, route);
}
#[cfg(target_os = "macos")]
#[inline]
pub fn create_native_tab<'a>(
&'a mut self,
event_loop: &'a ActiveEventLoop,
event_proxy: EventProxy,
config: &'a rio_backend::config::Config,
tab_id: Option<&str>,
open_url: Option<String>,
) {
let window = RouteWindow::from_target(
event_loop,
event_proxy,
config,
&self.font_library,
RIO_TITLE,
tab_id,
open_url,
self.clipboard.clone(),
None,
);
self.routes.insert(
window.winit_window.id(),
Route {
window,
path: RoutePath::Terminal,
assistant: Assistant::new(),
},
);
}
}
pub struct RouteWindow<'a> {
pub is_focused: bool,
pub is_occluded: bool,
pub needs_render_after_occlusion: bool,
pub render_timestamp: Instant,
#[cfg_attr(target_os = "macos", allow(dead_code))]
pub vblank_interval: Duration,
pub winit_window: Window,
pub screen: Screen<'a>,
}
impl<'a> RouteWindow<'a> {
pub fn configure_window(&mut self, config: &rio_backend::config::Config) {
configure_window(&self.winit_window, config);
}
pub fn wait_until(&self) -> Option<Duration> {
if self.needs_render_after_occlusion {
return None;
}
#[cfg(target_os = "macos")]
{
None
}
#[cfg(not(target_os = "macos"))]
{
let now = Instant::now();
let elapsed = now.duration_since(self.render_timestamp);
let vblank = self.vblank_interval;
let frames_elapsed = elapsed.as_nanos() / vblank.as_nanos();
let next_frame_time = self.render_timestamp
+ Duration::from_nanos(
(frames_elapsed + 1) as u64 * vblank.as_nanos() as u64,
);
if next_frame_time > now {
Some(next_frame_time.duration_since(now))
} else {
None
}
}
}
#[inline]
pub fn update_vblank_interval(&mut self) {
#[cfg(not(target_os = "macos"))]
{
let refresh_rate_hz = self
.winit_window
.current_monitor()
.and_then(|monitor| monitor.refresh_rate_millihertz())
.unwrap_or(60_000) as f64
/ 1000.0;
let frame_time_us = (1_000_000.0 / refresh_rate_hz) as u64;
self.vblank_interval = Duration::from_micros(frame_time_us);
}
}
#[allow(clippy::too_many_arguments)]
pub fn from_target<'b>(
event_loop: &'b ActiveEventLoop,
event_proxy: EventProxy,
config: &'b RioConfig,
font_library: &rio_backend::sugarloaf::font::FontLibrary,
window_name: &str,
tab_id: Option<&str>,
open_url: Option<String>,
clipboard: Rc<RefCell<Clipboard>>,
app_id: Option<&str>,
) -> RouteWindow<'a> {
#[allow(unused_mut)]
let mut window_builder =
create_window_builder(window_name, config, tab_id, app_id);
#[cfg(not(any(target_os = "macos", windows)))]
if let Some(token) = event_loop.read_token_from_env() {
tracing::debug!("Activating window with token: {token:?}");
window_builder = window_builder.with_activation_token(token);
startup_notify::reset_activation_token_env();
}
let winit_window = event_loop.create_window(window_builder).unwrap();
configure_window(&winit_window, config);
let properties = ScreenWindowProperties {
size: winit_window.inner_size(),
scale: winit_window.scale_factor(),
raw_window_handle: winit_window.window_handle().unwrap().into(),
raw_display_handle: winit_window.display_handle().unwrap().into(),
window_id: winit_window.id(),
};
let screen = Screen::new(
properties,
config,
event_proxy,
font_library,
open_url,
clipboard,
)
.expect("Screen not created");
#[cfg(target_os = "windows")]
{
use rio_window::platform::windows::WindowExtWindows;
winit_window.set_cloaked(false);
}
#[cfg(target_os = "macos")]
let monitor_vblank_interval = Duration::from_micros(16667);
#[cfg(not(target_os = "macos"))]
let monitor_vblank_interval = {
let monitor_refresh_rate_hz = winit_window
.current_monitor()
.and_then(|monitor| monitor.refresh_rate_millihertz())
.unwrap_or(60_000) as f64
/ 1000.0;
let frame_time_us = (1_000_000.0 / monitor_refresh_rate_hz) as u64;
Duration::from_micros(frame_time_us)
};
Self {
vblank_interval: monitor_vblank_interval,
render_timestamp: Instant::now(),
is_focused: true,
is_occluded: false,
needs_render_after_occlusion: false,
winit_window,
screen,
}
}
}