use std::time::Instant;
use serde::Serialize;
use tao::dpi::LogicalSize;
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
use tao::window::{Window, WindowBuilder};
use wry::{WebContext, WebView, WebViewBuilder};
use crate::platform::{self, bring_to_front};
use crate::{AppEvent, Msg, Reply, Request, Review, Slot, fail, is_vjuu, serve, tray};
pub type TabId = u64;
pub struct Tab {
pub webview: WebView,
_context: Option<WebContext>,
pub slot: Slot,
pub reply: Option<Reply>,
pub latest: Review,
pub started: Instant,
pub context: Option<String>,
pub applied: bool,
pub background: bool,
seen: bool,
}
impl Tab {
pub fn label(&self) -> String {
let slot = self.slot.lock().expect("slot lock");
let (session, what) =
slot.as_ref().map(|r| (r.session.clone(), r.kind.clone().unwrap_or_else(|| r.name()))).unzip();
[session.flatten(), what, self.context.clone(), Some(tray::age(self.started.elapsed()))]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" · ")
}
pub fn title(&self) -> String {
let title = self.slot.lock().expect("slot lock").as_ref().map(Request::title).unwrap_or_default();
match &self.context {
Some(git) => format!("{title} · {git}"),
None => title,
}
}
}
#[derive(Serialize)]
struct StripTab {
id: TabId,
name: String,
kind: Option<String>,
title: String,
active: bool,
unseen: bool,
}
pub struct Host {
tabs: Vec<(TabId, Tab)>,
spare: Option<(TabId, Tab)>,
active: Option<TabId>,
next: TabId,
sessions: Vec<String>,
pub window: Window,
}
impl Host {
pub fn new(target: &EventLoopWindowTarget<AppEvent>) -> Host {
let builder = WindowBuilder::new()
.with_title("vjuu")
.with_visible(false)
.with_focused(false)
.with_inner_size(LogicalSize::new(1100.0, 900.0));
let window =
platform::frame(builder).build(target).unwrap_or_else(|e| fail(&format!("Cannot open window: {e}")));
platform::style_frame(&window);
platform::fit_height(&window);
Host { tabs: Vec::new(), spare: None, active: None, next: 0, sessions: Vec::new(), window }
}
pub fn warm_spare(&mut self, proxy: &EventLoopProxy<AppEvent>, context: &mut WebContext) {
if self.spare.is_none() {
self.spare = Some(self.new_tab(proxy, context, None, false));
}
}
pub fn open(
&mut self,
proxy: &EventLoopProxy<AppEvent>,
context: &mut WebContext,
request: Request,
reply: Option<Reply>,
started: Instant,
focus: bool,
) -> TabId {
let rank = self.rank(request.session.as_deref());
let (id, mut tab) = match self.spare.take() {
Some((id, tab)) => {
*tab.slot.lock().expect("slot lock") = Some(request);
let _ = tab.webview.evaluate_script("window.vjuuLoad && vjuuLoad()");
(id, tab)
}
None => self.new_tab(proxy, context, Some(request), focus),
};
tab.reply = reply;
tab.started = started;
let here = self.window.is_visible() && self.window.is_focused();
let front = self.active.is_none() || (focus && !here);
tab.background = !(front && focus);
let ranks: Vec<usize> = self.tabs.iter().map(|(_, t)| self.rank_of(t)).collect();
self.tabs.insert(position(&ranks, rank), (id, tab));
if front {
self.show(id);
}
if !self.window.is_visible() {
if focus {
self.window.set_visible(true);
} else {
self.window.set_focusable(false);
self.window.set_visible(true);
self.window.set_focusable(true);
}
}
if front && focus {
self.raise(id);
}
self.show_strip();
id
}
pub fn get(&self, id: TabId) -> Option<&Tab> {
self.tabs.iter().find(|(t, _)| *t == id).map(|(_, tab)| tab)
}
pub fn get_mut(&mut self, id: TabId) -> Option<&mut Tab> {
self.tabs.iter_mut().find(|(t, _)| *t == id).map(|(_, tab)| tab)
}
pub fn is_active(&self, id: TabId) -> bool {
self.active == Some(id)
}
pub fn tabs(&self) -> impl Iterator<Item = &(TabId, Tab)> {
self.tabs.iter()
}
pub fn webviews(&self) -> impl Iterator<Item = &WebView> {
self.tabs.iter().chain(&self.spare).map(|(_, tab)| &tab.webview)
}
pub fn show(&mut self, id: TabId) {
if self.get(id).is_none() {
return;
}
self.active = Some(id);
for (t, tab) in &mut self.tabs {
let _ = tab.webview.set_visible(*t == id);
if *t == id {
tab.seen = true;
self.window.set_title(&tab.title());
let _ = tab.webview.focus();
}
}
self.show_strip();
}
pub fn raise(&mut self, id: TabId) {
self.show(id);
if let Some(tab) = self.get(id) {
self.window.set_minimized(false);
bring_to_front(&self.window, &tab.webview);
}
}
pub fn remove(&mut self, id: TabId) -> Option<Tab> {
let at = self.tabs.iter().position(|(t, _)| *t == id)?;
let (_, tab) = self.tabs.remove(at);
if self.active == Some(id) {
self.active = None;
match self.tabs.get(at).or(self.tabs.last()).map(|(t, _)| *t) {
Some(next) => self.show(next),
None => self.window.set_visible(false),
}
}
self.show_strip();
Some(tab)
}
pub fn fit(&self) {
for webview in self.webviews() {
platform::fit_webview(webview, &self.window);
}
}
pub fn show_strip(&self) {
for (_, tab) in &self.tabs {
self.show_strip_in(tab);
}
}
pub fn show_strip_in(&self, tab: &Tab) {
let strip: Vec<StripTab> = self
.tabs
.iter()
.map(|(id, t)| {
let title = t.label();
let request = t.slot.lock().expect("slot lock");
StripTab {
id: *id,
name: request.as_ref().map(|r| r.session.clone().unwrap_or_else(|| r.name())).unwrap_or_default(),
kind: request.as_ref().and_then(|r| r.kind.clone()),
title,
active: self.active == Some(*id),
unseen: !t.seen,
}
})
.collect();
let json = serde_json::to_string(&strip).expect("strip serialises");
let _ = tab.webview.evaluate_script(&format!("window.vjuuTabs && vjuuTabs({json})"));
}
fn rank(&mut self, session: Option<&str>) -> usize {
let Some(session) = session else { return usize::MAX };
match self.sessions.iter().position(|s| s == session) {
Some(rank) => rank,
None => {
self.sessions.push(session.to_string());
self.sessions.len() - 1
}
}
}
fn rank_of(&self, tab: &Tab) -> usize {
let request = tab.slot.lock().expect("slot lock");
let session = request.as_ref().and_then(|r| r.session.as_deref());
session.and_then(|s| self.sessions.iter().position(|known| known == s)).unwrap_or(usize::MAX)
}
fn new_tab(
&mut self,
proxy: &EventLoopProxy<AppEvent>,
context: &mut WebContext,
request: Option<Request>,
focus: bool,
) -> (TabId, Tab) {
let id = self.next;
self.next += 1;
let slot: Slot = std::sync::Arc::new(std::sync::Mutex::new(request));
let proxy = proxy.clone();
let serve_slot = slot.clone();
let mut own =
platform::CONTEXT_PER_WEBVIEW.then(|| WebContext::new(Some(platform::data_dir().join("webview"))));
let context = own.as_mut().unwrap_or(context);
let builder = WebViewBuilder::new_with_web_context(context)
.with_visible(false)
.with_focused(focus)
.with_initialization_script(format!("window.VJUU_FRAMELESS = {};", platform::FRAMELESS))
.with_custom_protocol("vjuu".into(), move |_id, request| serve(request.uri().path(), &serve_slot))
.with_url("vjuu://localhost/")
.with_navigation_handler(|url| {
if is_vjuu(&url) {
return true;
}
if url.starts_with("http://") || url.starts_with("https://") {
platform::open_url(&url);
}
false
})
.with_ipc_handler(move |request| {
if !is_vjuu(&request.uri().to_string()) {
eprintln!("Ignored a message from {}", request.uri());
return;
}
match serde_json::from_str::<Msg>(request.body()) {
Ok(msg) => {
let _ = proxy.send_event(AppEvent::Ui(id, msg));
}
Err(e) => eprintln!("Bad message from UI: {e}"),
}
});
let webview = platform::build_webview(builder, &self.window)
.unwrap_or_else(|e| fail(&format!("Cannot create webview: {e}")));
let tab = Tab {
webview,
_context: own,
slot,
reply: None,
latest: Review::default(),
started: Instant::now(),
context: None,
applied: false,
background: false,
seen: false,
};
(id, tab)
}
}
fn position(ranks: &[usize], rank: usize) -> usize {
ranks.iter().take_while(|&&r| r <= rank).count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn puts_a_tab_next_to_its_session() {
assert_eq!(position(&[], usize::MAX), 0);
assert_eq!(position(&[0, 0, 1, usize::MAX], 0), 2, "after the session's other tabs");
assert_eq!(position(&[0, 2], 1), 1, "between the sessions that came before and after");
assert_eq!(position(&[0, usize::MAX], usize::MAX), 2, "no session: last");
assert_eq!(position(&[1, usize::MAX], 0), 0, "an earlier session goes first");
}
}