use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use crate::dispatch::DragState;
use crate::event::{Point, Rect, UiEvent, UserPayload, Viewport};
use crate::focus::FocusManager;
use crate::interaction::InteractionState;
use crate::modal_stack::ModalStack;
use crate::primitives::activity_bar::{ActivityBarRowHit, ActivityBarStyle};
use crate::primitives::board::{BoardLayout, BoardModel};
use crate::primitives::button::{Button, ButtonLayout};
use crate::primitives::canvas::{Canvas, CanvasLayout};
use crate::primitives::chart::{Chart, ChartLayout};
use crate::primitives::combo_box::{ComboBox, ComboBoxLayout};
use crate::primitives::command_center::{CommandCenter, CommandCenterLayout};
use crate::primitives::command_line::{CommandLine, CommandLineLayout};
use crate::primitives::completions::{Completions, CompletionsLayout};
use crate::primitives::context_menu::{ContextMenu, ContextMenuGlyphs, ContextMenuLayout};
use crate::primitives::data_table::{DataTable, DataTableGlyphs, DataTableLayout};
use crate::primitives::date_picker::{DatePicker, DatePickerLayout};
use crate::primitives::dialog::{Dialog, DialogLayout, DialogSeverity};
use crate::primitives::diff_view::{DiffMode, DiffView, DiffViewLayout};
use crate::primitives::drop_zone::DropOverlay;
use crate::primitives::editor::{CursorShape as EditorCursorShape, Editor, EditorLayout};
use crate::primitives::find_replace::FindReplacePanel;
use crate::primitives::float::{Float, FloatLayout};
use crate::primitives::form::FormLayout;
use crate::primitives::image::{Image, ImageSource};
use crate::primitives::list::ListViewLayout;
use crate::primitives::menu_bar::{MenuBar, MenuBarLayout};
use crate::primitives::message_list::MessageList;
use crate::primitives::minimap::{Minimap, MinimapLayout, MinimapScale};
use crate::primitives::multi_section_view::{
MsvLayoutMetrics, MultiSectionView, MultiSectionViewLayout,
};
use crate::primitives::number_input::{NumberInput, NumberInputLayout};
use crate::primitives::panel::{Panel, PanelLayout};
use crate::primitives::pipeline_view::{PipelineView, PipelineViewLayout};
use crate::primitives::progress::{ProgressBar, ProgressBarLayout};
use crate::primitives::radio_group::{RadioGroup, RadioGroupLayout};
use crate::primitives::rich_text_popup::{RichTextPopup, RichTextPopupLayout};
use crate::primitives::scrollbar::Scrollbar;
use crate::primitives::sidebar_panel::{SidebarPanel, SidebarPanelLayout};
use crate::primitives::spinner::{Spinner, SpinnerLayout};
use crate::primitives::split::{Split, SplitLayout};
use crate::primitives::split_tree::{SplitTree, SplitTreeLayout};
use crate::primitives::status_bar::StatusBarLayout;
use crate::primitives::switch::{Switch, SwitchLayout};
#[allow(deprecated)]
use crate::primitives::tab_bar::{TabBarGlyphs, TabBarHits, TabBarLayout, TabChrome, TabIcon};
use crate::primitives::text_display::TextDisplayLayout;
use crate::primitives::text_input::{TextInput, TextInputLayout};
use crate::primitives::toast::{ToastOverlay, ToastStackLayout};
use crate::primitives::toolbar::{Toolbar, ToolbarLayout, ToolbarPaintOptions};
use crate::primitives::tooltip::{Tooltip, TooltipChrome, TooltipLayout};
use crate::primitives::tree::TreeViewLayout;
use crate::types::{Color, WidgetId};
use crate::{
Accelerator, AcceleratorId, ActivityBar, Form, ListView, Palette, PaletteLayout, StatusBar,
TabBar, Terminal, TextDisplay, TreeView,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeEdge {
North,
South,
East,
West,
NorthEast,
NorthWest,
SouthEast,
SouthWest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerShape {
Default,
Resize(ResizeEdge),
}
pub trait WindowControl {
fn set_title(&mut self, _title: &str) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_size(&mut self, _width: f32, _height: f32) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_min_size(&mut self, _width: f32, _height: f32) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_max_size(&mut self, _width: f32, _height: f32) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn bounds(&self) -> ServiceResult<Rect> {
Err(BackendError::Unsupported)
}
fn set_bounds(&mut self, _bounds: Rect) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn center(&mut self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_fullscreen(&mut self, _fullscreen: bool) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn is_maximized(&self) -> ServiceResult<bool> {
Err(BackendError::Unsupported)
}
fn set_always_on_top(&mut self, _on_top: bool) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_decorated(&mut self, _decorated: bool) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn minimize(&mut self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn restore(&mut self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn hide(&mut self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn show(&mut self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn focus(&mut self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
}
pub trait TrayService {
fn set_icon(&mut self, _icon: ImageSource) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_tooltip(&mut self, _tooltip: &str) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_menu(&mut self, _menu: &ContextMenu) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorDepth {
Ansi16,
Indexed256,
TrueColor,
}
impl Default for ColorDepth {
fn default() -> Self {
Self::TrueColor
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BackendCaps {
pub mouse: bool,
pub scroll: bool,
pub drag: bool,
pub text_selection: bool,
pub native_menu: bool,
pub window_chrome: bool,
pub pointer_cursor: bool,
pub ime: bool,
pub file_dialogs: bool,
pub folder_dialogs: bool,
pub native_dialogs: bool,
pub notifications: bool,
pub app_font_registration: bool,
pub window_control: bool,
pub tray: bool,
pub generic_font_families: bool,
pub color_depth: ColorDepth,
pub kitty_keyboard: bool,
pub sgr_pixel_mouse: bool,
}
type NamedCap = (&'static str, fn(&BackendCaps) -> bool);
impl BackendCaps {
pub const fn empty() -> Self {
Self {
mouse: false,
scroll: false,
drag: false,
text_selection: false,
native_menu: false,
window_chrome: false,
pointer_cursor: false,
ime: false,
file_dialogs: false,
folder_dialogs: false,
native_dialogs: false,
notifications: false,
app_font_registration: false,
window_control: false,
tray: false,
generic_font_families: false,
color_depth: ColorDepth::TrueColor,
kitty_keyboard: false,
sgr_pixel_mouse: false,
}
}
pub fn names(&self) -> Vec<&'static str> {
Self::ALL_NAMES
.iter()
.copied()
.filter(|(_, get)| get(self))
.map(|(name, _)| name)
.collect()
}
pub fn has(&self, cap: &str) -> bool {
Self::ALL_NAMES
.iter()
.any(|(name, get)| *name == cap && get(self))
}
pub fn vocabulary() -> Vec<&'static str> {
Self::ALL_NAMES.iter().map(|(name, _)| *name).collect()
}
const ALL_NAMES: &'static [NamedCap] = &[
("mouse", |c| c.mouse),
("scroll", |c| c.scroll),
("drag", |c| c.drag),
("text_selection", |c| c.text_selection),
("native_menu", |c| c.native_menu),
("window_chrome", |c| c.window_chrome),
("pointer_cursor", |c| c.pointer_cursor),
("ime", |c| c.ime),
("file_dialogs", |c| c.file_dialogs),
("folder_dialogs", |c| c.folder_dialogs),
("native_dialogs", |c| c.native_dialogs),
("notifications", |c| c.notifications),
("app_font_registration", |c| c.app_font_registration),
("window_control", |c| c.window_control),
("tray", |c| c.tray),
("generic_font_families", |c| c.generic_font_families),
];
}
pub(crate) mod sealed {
pub trait Sealed {}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BackendError {
Unsupported,
PlatformFailure {
context: String,
},
SurfaceLost,
}
pub type ServiceResult<T> = Result<T, BackendError>;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct Metrics {
pub char_width: f32,
pub line_height: f32,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct PlatformFontDefaults {
pub editor_family: String,
pub editor_size_pt: f32,
pub ui_family: String,
pub ui_size_pt: f32,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct InvalidationLog {
rects: Vec<Rect>,
}
impl InvalidationLog {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, rect: Rect) {
self.rects.push(rect);
}
pub fn rects(&self) -> &[Rect] {
&self.rects
}
pub fn clear(&mut self) {
self.rects.clear();
}
pub fn is_empty(&self) -> bool {
self.rects.is_empty()
}
pub fn len(&self) -> usize {
self.rects.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MenuStyle {
#[default]
Auto,
Native,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolvedMenuStyle {
Native,
Custom,
}
impl MenuStyle {
pub fn resolve(self, caps: &BackendCaps) -> ResolvedMenuStyle {
match self {
MenuStyle::Custom => ResolvedMenuStyle::Custom,
MenuStyle::Auto | MenuStyle::Native => {
if caps.native_menu {
ResolvedMenuStyle::Native
} else {
ResolvedMenuStyle::Custom
}
}
}
}
}
#[cfg(test)]
mod menu_style_tests {
use super::{BackendCaps, MenuStyle, ResolvedMenuStyle};
fn caps(native_menu: bool) -> BackendCaps {
BackendCaps {
native_menu,
..BackendCaps::empty()
}
}
#[test]
fn auto_resolves_native_when_caps_declare_native_menu() {
assert_eq!(
MenuStyle::Auto.resolve(&caps(true)),
ResolvedMenuStyle::Native
);
}
#[test]
fn auto_resolves_custom_when_caps_lack_native_menu() {
assert_eq!(
MenuStyle::Auto.resolve(&caps(false)),
ResolvedMenuStyle::Custom
);
}
#[test]
fn native_resolves_native_when_caps_declare_native_menu() {
assert_eq!(
MenuStyle::Native.resolve(&caps(true)),
ResolvedMenuStyle::Native
);
}
#[test]
fn native_falls_back_to_custom_when_caps_lack_native_menu() {
assert_eq!(
MenuStyle::Native.resolve(&caps(false)),
ResolvedMenuStyle::Custom
);
}
#[test]
fn custom_resolves_custom_when_caps_declare_native_menu() {
assert_eq!(
MenuStyle::Custom.resolve(&caps(true)),
ResolvedMenuStyle::Custom
);
}
#[test]
fn custom_resolves_custom_when_caps_lack_native_menu() {
assert_eq!(
MenuStyle::Custom.resolve(&caps(false)),
ResolvedMenuStyle::Custom
);
}
#[test]
fn default_is_auto() {
assert_eq!(MenuStyle::default(), MenuStyle::Auto);
}
}
pub trait Backend: sealed::Sealed {
fn viewport(&self) -> Viewport;
fn begin_frame(&mut self, viewport: Viewport);
fn end_frame(&mut self);
fn set_theme(&mut self, _theme: crate::Theme) {}
fn theme(&self) -> crate::Theme {
crate::Theme::default()
}
fn set_style(&mut self, _style: crate::Style) {}
fn style(&self) -> crate::Style {
crate::Style::default()
}
fn set_nerd_fonts(&mut self, _enabled: bool) {}
fn nerd_fonts_enabled(&self) -> bool {
false
}
fn set_editor_font(&mut self, _family: &str, _size_pt: f32) {}
fn set_ui_font(&mut self, _font_desc: &str) {}
fn default_fonts(&self) -> PlatformFontDefaults;
fn register_font_from_memory(&mut self, _bytes: &[u8]) -> Option<Vec<String>> {
None
}
fn set_nerd_font_fallback(&mut self, _family: &str) {}
fn has_font_family(&self, _family: &str) -> Option<bool> {
None
}
fn register_text_region(&mut self, _region: crate::dispatch::TextRegion) {}
fn register_zone(&mut self, _id: WidgetId, _bounds: Rect) {}
fn cancel_text_selection_drag(&mut self) {}
fn poll_events(&mut self) -> Vec<UiEvent>;
fn wait_events(&mut self, timeout: Duration) -> Vec<UiEvent>;
fn waker(&self) -> Arc<dyn Fn(UserPayload) + Send + Sync>;
fn request_frame_in(&self, delay: Duration);
fn request_full_repaint(&mut self) {}
fn invalidate_rect(&mut self, rect: Rect);
fn invalidation_log(&self) -> &InvalidationLog;
fn register_accelerator(&mut self, acc: &Accelerator);
fn unregister_accelerator(&mut self, id: &AcceleratorId);
fn install_menu_bar(&mut self, _bar: &crate::primitives::menu_bar::MenuBar) {}
fn show_context_menu(
&mut self,
_menu: &crate::primitives::context_menu::ContextMenu,
_anchor: crate::event::Point,
) {
}
fn menu_style(&self) -> MenuStyle {
MenuStyle::Auto
}
fn set_menu_style(&mut self, _style: MenuStyle) {}
fn effective_menu_style(&self) -> ResolvedMenuStyle {
self.menu_style().resolve(&self.backend_caps())
}
fn begin_window_drag(&mut self) -> bool {
false
}
fn toggle_window_maximize(&mut self) -> bool {
false
}
fn begin_window_resize(&mut self, _edge: ResizeEdge) -> bool {
false
}
fn set_cursor(&mut self, _shape: PointerShape) -> bool {
false
}
fn titlebar_control_inset(&self) -> Rect {
Rect::default()
}
fn window(&mut self) -> Option<&mut dyn WindowControl> {
None
}
fn tray(&mut self) -> Option<&mut dyn TrayService> {
None
}
fn modal_stack_handle(&self) -> Rc<RefCell<ModalStack>>;
fn drag_state_handle(&self) -> Rc<RefCell<DragState>>;
fn focus_manager(&self) -> &FocusManager;
fn draw_focus_ring(&mut self, rect: Rect);
fn last_error(&mut self) -> Option<BackendError> {
None
}
fn services(&self) -> &dyn PlatformServices;
fn paint_surface(&mut self) -> Option<&mut dyn crate::PaintSurface> {
None
}
fn backend_caps(&self) -> BackendCaps;
fn line_height(&self) -> f32;
fn char_width(&self) -> f32;
fn chrome_line_height(&self) -> f32 {
self.line_height()
}
fn set_current_line_height(&mut self, _line_height: f32) {}
fn set_current_char_width(&mut self, _char_width: f32) {}
fn list_char_width(&self) -> f32;
fn measure(&self) -> Metrics {
Metrics {
char_width: self.char_width(),
line_height: self.line_height(),
}
}
fn measure_text(&self, text: &str, role: crate::FontRole) -> (f32, f32);
fn scrollbar_reserve(&self) -> f32 {
0.0
}
fn context_menu_border_inset(&self) -> f32 {
0.0
}
fn context_menu_viewport(&self) -> Rect {
let vp = self.viewport();
let inset = self.context_menu_border_inset();
Rect::new(
inset,
inset,
(vp.width - inset * 2.0).max(0.0),
(vp.height - inset * 2.0).max(0.0),
)
}
fn scales_text_rows(&self) -> bool {
false
}
fn snap_height(&self, h: f32) -> f32 {
h
}
fn draw_tree(&mut self, rect: Rect, tree: &TreeView);
fn draw_list(&mut self, rect: Rect, list: &ListView);
fn draw_data_table(
&mut self,
rect: Rect,
table: &DataTable,
hovered_idx: Option<usize>,
) -> DataTableLayout;
fn draw_data_table_with_glyphs(
&mut self,
rect: Rect,
table: &DataTable,
hovered_idx: Option<usize>,
glyphs: Option<&DataTableGlyphs>,
) -> DataTableLayout {
let _ = glyphs;
self.draw_data_table(rect, table, hovered_idx)
}
fn data_table_layout(&self, rect: Rect, table: &DataTable) -> DataTableLayout;
fn list_hscrollbar(&self, rect: Rect, list: &ListView) -> Option<Scrollbar>;
fn list_vscrollbar(&self, rect: Rect, list: &ListView) -> Option<Scrollbar>;
fn list_layout(&self, rect: Rect, list: &ListView) -> ListViewLayout;
fn draw_form(&mut self, rect: Rect, form: &Form);
fn draw_palette(&mut self, rect: Rect, palette: &Palette);
fn palette_layout(&self, rect: Rect, palette: &Palette) -> PaletteLayout;
fn draw_settings_chrome(
&mut self,
rect: Rect,
header_text: &str,
query: &str,
placeholder: &str,
active: bool,
);
fn draw_status_bar_interactive(
&mut self,
rect: Rect,
bar: &StatusBar,
interaction: &InteractionState,
) -> StatusBarLayout;
fn draw_status_bar_interactive_scaled(
&mut self,
rect: Rect,
bar: &StatusBar,
interaction: &InteractionState,
_font_scale: f32,
) -> StatusBarLayout {
self.draw_status_bar_interactive(rect, bar, interaction)
}
#[deprecated(
since = "0.0.1",
note = "use `draw_status_bar_interactive` (hover/pressed come from an `InteractionState` keyed by `WidgetId`) — issue #819; removal target: once the sealed `tests/acceptance/ms-11` slices that still call this positionally migrate, not before v0.2.0"
)]
fn draw_status_bar(
&mut self,
rect: Rect,
bar: &StatusBar,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
) -> StatusBarLayout {
let interaction = InteractionState::from_parts(hovered_id.cloned(), pressed_id.cloned());
self.draw_status_bar_interactive(rect, bar, &interaction)
}
#[allow(deprecated)]
fn draw_tab_bar(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
) -> TabBarHits;
fn draw_tab_bar_layout(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
) -> TabBarLayout;
#[allow(deprecated)]
fn draw_tab_bar_icons(
&mut self,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
hovered_close_tab: Option<usize>,
) -> TabBarHits;
fn draw_tab_bar_icons_layout(
&mut self,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
hovered_close_tab: Option<usize>,
) -> TabBarLayout;
#[allow(deprecated)]
fn draw_tab_bar_with_chrome(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
chrome: &TabChrome,
) -> TabBarHits {
let _ = chrome;
self.draw_tab_bar(rect, bar, hovered_close_tab)
}
fn draw_tab_bar_with_chrome_layout(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
chrome: &TabChrome,
) -> TabBarLayout {
let _ = chrome;
self.draw_tab_bar_layout(rect, bar, hovered_close_tab)
}
#[allow(deprecated)]
fn draw_tab_bar_with_chrome_and_glyphs(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
chrome: &TabChrome,
glyphs: Option<&TabBarGlyphs>,
) -> TabBarHits {
let _ = glyphs;
self.draw_tab_bar_with_chrome(rect, bar, hovered_close_tab, chrome)
}
fn draw_activity_bar(
&mut self,
rect: Rect,
bar: &ActivityBar,
hovered_idx: Option<usize>,
) -> Vec<ActivityBarRowHit>;
fn draw_activity_bar_with_style(
&mut self,
rect: Rect,
bar: &ActivityBar,
hovered_idx: Option<usize>,
style: &ActivityBarStyle,
) -> Vec<ActivityBarRowHit> {
let _ = style;
self.draw_activity_bar(rect, bar, hovered_idx)
}
fn status_bar_layout(&self, rect: Rect, bar: &StatusBar) -> StatusBarLayout;
#[allow(deprecated)]
fn tab_bar_layout(&self, rect: Rect, bar: &TabBar) -> TabBarHits;
fn resolve_tab_bar_layout(&self, rect: Rect, bar: &TabBar) -> TabBarLayout;
#[allow(deprecated)]
fn tab_bar_layout_icons(
&self,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
) -> TabBarHits;
fn resolve_tab_bar_layout_icons(
&self,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
) -> TabBarLayout;
#[allow(deprecated)]
fn tab_bar_layout_with_chrome(
&self,
rect: Rect,
bar: &TabBar,
chrome: &TabChrome,
) -> TabBarHits {
let _ = chrome;
self.tab_bar_layout(rect, bar)
}
fn resolve_tab_bar_layout_with_chrome(
&self,
rect: Rect,
bar: &TabBar,
chrome: &TabChrome,
) -> TabBarLayout {
let _ = chrome;
self.resolve_tab_bar_layout(rect, bar)
}
fn activity_bar_layout(&self, rect: Rect, bar: &ActivityBar) -> Vec<ActivityBarRowHit>;
fn draw_terminal(&mut self, rect: Rect, term: &Terminal);
fn terminal_scrollbar_default_width(&self) -> f32 {
8.0
}
fn terminal_layout(
&self,
rect: Rect,
term: &Terminal,
) -> crate::primitives::terminal::TerminalLayout {
let sb_reserved = match &term.scrollbar {
Some(sb) => sb
.width
.map(|w| w as f32)
.unwrap_or_else(|| self.terminal_scrollbar_default_width()),
None => 0.0,
};
let cell_area_w = (rect.width - sb_reserved).max(0.0);
term.layout(
cell_area_w,
rect.height,
self.char_width(),
self.line_height(),
)
}
fn draw_terminal_divider(&mut self, rect: Rect);
fn draw_solid_fill(&mut self, rect: Rect, color: Color);
fn draw_text_display(&mut self, rect: Rect, td: &TextDisplay);
fn draw_command_line(&mut self, rect: Rect, cmd: &CommandLine);
fn draw_command_line_selection(
&mut self,
rect: Rect,
cmd: &CommandLine,
selection: Option<(usize, usize)>,
);
fn command_line_layout(&self, rect: Rect, cmd: &CommandLine) -> CommandLineLayout;
fn text_display_layout(&self, rect: Rect, td: &TextDisplay) -> TextDisplayLayout;
fn draw_text_input(&mut self, rect: Rect, ti: &TextInput) -> TextInputLayout;
fn text_input_layout(&self, rect: Rect, ti: &TextInput) -> TextInputLayout;
fn draw_number_input(&mut self, number_input: &NumberInput, layout: &NumberInputLayout);
fn draw_combo_box(&mut self, combo_box: &ComboBox, layout: &ComboBoxLayout);
fn draw_date_picker(&mut self, date_picker: &DatePicker, layout: &DatePickerLayout);
fn draw_tooltip(&mut self, tooltip: &Tooltip, layout: &TooltipLayout);
fn draw_tooltip_with_chrome(
&mut self,
tooltip: &Tooltip,
layout: &TooltipLayout,
chrome: &TooltipChrome,
) {
let _ = chrome;
self.draw_tooltip(tooltip, layout);
}
fn draw_context_menu(
&mut self,
menu: &ContextMenu,
layout: &ContextMenuLayout,
) -> Vec<(Rect, WidgetId)>;
fn draw_context_menu_with_glyphs(
&mut self,
menu: &ContextMenu,
layout: &ContextMenuLayout,
glyphs: Option<&ContextMenuGlyphs>,
) -> Vec<(Rect, WidgetId)> {
let _ = glyphs;
self.draw_context_menu(menu, layout)
}
fn draw_dialog(&mut self, dialog: &Dialog, layout: &DialogLayout) -> Vec<Rect>;
fn draw_float(&mut self, float: &Float, layout: &FloatLayout);
fn draw_multi_section_view(&mut self, rect: Rect, view: &MultiSectionView);
fn msv_layout(&self, rect: Rect, view: &MultiSectionView) -> MultiSectionViewLayout;
fn msv_metrics(&self) -> MsvLayoutMetrics;
fn tree_layout(&self, rect: Rect, tree: &TreeView) -> TreeViewLayout;
fn tree_vscrollbar(&self, rect: Rect, tree: &TreeView) -> Option<Scrollbar>;
fn tree_paints_own_scrollbar(&self) -> bool {
false
}
fn form_layout(&self, rect: Rect, form: &Form) -> FormLayout;
fn draw_editor(&mut self, rect: Rect, editor: &Editor) -> EditorPaintResult;
fn set_editor_v_scrollbar_width(&mut self, _px: Option<f32>) {}
fn editor_v_scrollbar_width(&self) -> Option<f32> {
None
}
fn set_editor_right_reserve_w(&mut self, _px: Option<f32>) {}
fn editor_right_reserve_w(&self) -> Option<f32> {
None
}
fn editor_layout(&self, rect: Rect, editor: &Editor) -> EditorLayout {
editor.layout_with_options_and_reserve(
rect,
self.char_width(),
self.line_height(),
crate::primitives::editor::EditorPaintOptions::default(),
self.editor_v_scrollbar_width(),
self.editor_right_reserve_w().unwrap_or(0.0),
)
}
fn editor_col_at_x(
&self,
layout: &EditorLayout,
editor: &Editor,
view_row: usize,
x: f32,
) -> usize {
layout.col_at_x(editor, view_row, x)
}
fn set_caret_shape(&mut self, _shape: EditorCursorShape) {}
fn draw_message_list(&mut self, rect: Rect, list: &MessageList);
fn draw_rich_text_popup(&mut self, popup: &RichTextPopup, layout: &RichTextPopupLayout);
fn draw_rich_text_popup_with_font_role(
&mut self,
popup: &RichTextPopup,
layout: &RichTextPopupLayout,
role: crate::FontRole,
) {
let _ = role;
self.draw_rich_text_popup(popup, layout);
}
fn draw_find_replace(&mut self, rect: Rect, panel: &FindReplacePanel);
fn draw_completions(&mut self, completions: &Completions, layout: &CompletionsLayout);
fn draw_scrollbar(&mut self, rect: Rect, scrollbar: &Scrollbar);
fn draw_drop_overlay(&mut self, overlay: &DropOverlay);
fn draw_menu_bar(&mut self, rect: Rect, bar: &MenuBar) -> MenuBarLayout;
fn menu_bar_layout(&self, rect: Rect, bar: &MenuBar) -> MenuBarLayout;
fn draw_split(&mut self, rect: Rect, split: &Split) -> SplitLayout;
fn split_layout(&self, rect: Rect, split: &Split) -> SplitLayout;
fn draw_split_tree(&mut self, rect: Rect, tree: &SplitTree) -> SplitTreeLayout;
fn split_tree_layout(&self, rect: Rect, tree: &SplitTree) -> SplitTreeLayout;
fn draw_panel(&mut self, rect: Rect, panel: &Panel) -> PanelLayout;
fn panel_layout(&self, rect: Rect, panel: &Panel) -> PanelLayout;
fn draw_toast_overlay(&mut self, rect: Rect, stack: &ToastOverlay) -> ToastStackLayout;
fn toast_stack_layout(&self, rect: Rect, stack: &ToastOverlay) -> ToastStackLayout;
fn draw_pipeline_view(&mut self, rect: Rect, view: &PipelineView) -> PipelineViewLayout;
fn pipeline_view_layout(&self, rect: Rect, view: &PipelineView) -> PipelineViewLayout;
fn draw_diff_view(&mut self, rect: Rect, view: &DiffView) -> DiffViewLayout;
fn diff_view_layout(&self, rect: Rect, view: &DiffView) -> DiffViewLayout {
if rect.width <= 0.0 || rect.height <= 0.0 {
return DiffViewLayout {
visible_rows: 0,
total_rows: view.total_rows(),
};
}
let lh = self.line_height();
match view.mode {
DiffMode::SideBySide => {
let has_header = view.left_label.is_some() || view.right_label.is_some();
let header_h = if has_header { lh } else { 0.0 };
let content_h = (rect.height - header_h).max(0.0);
let visible_rows = if lh > 0.0 {
(content_h / lh).floor() as usize
} else {
0
};
DiffViewLayout {
visible_rows,
total_rows: view.total_rows(),
}
}
DiffMode::Unified => {
let visible_rows = if lh > 0.0 {
(rect.height / lh).floor() as usize
} else {
0
};
let total_rows: usize = view.hunks.iter().map(|h| h.rows.len() + 1).sum();
DiffViewLayout {
visible_rows,
total_rows,
}
}
}
}
fn draw_progress(&mut self, rect: Rect, bar: &ProgressBar) -> ProgressBarLayout;
fn progress_layout(&self, rect: Rect, bar: &ProgressBar) -> ProgressBarLayout;
fn draw_spinner(&mut self, rect: Rect, spinner: &Spinner) -> SpinnerLayout;
fn spinner_layout(&self, rect: Rect, spinner: &Spinner) -> SpinnerLayout;
fn draw_button(&mut self, rect: Rect, button: &Button) -> ButtonLayout;
fn button_layout(&self, rect: Rect, button: &Button) -> ButtonLayout;
fn draw_radio_group(&mut self, rect: Rect, group: &RadioGroup) -> RadioGroupLayout;
fn radio_group_layout(&self, rect: Rect, group: &RadioGroup) -> RadioGroupLayout;
fn draw_switch(&mut self, rect: Rect, switch: &Switch) -> SwitchLayout;
fn switch_layout(&self, rect: Rect, switch: &Switch) -> SwitchLayout;
fn draw_command_center(&mut self, rect: Rect, cc: &CommandCenter) -> CommandCenterLayout;
fn command_center_layout(&self, rect: Rect, cc: &CommandCenter) -> CommandCenterLayout;
fn draw_toolbar_interactive(
&mut self,
rect: Rect,
bar: &Toolbar,
interaction: &InteractionState,
) -> ToolbarLayout {
self.draw_toolbar_with_options(rect, bar, interaction, ToolbarPaintOptions::default())
}
fn draw_toolbar_with_options(
&mut self,
rect: Rect,
bar: &Toolbar,
interaction: &InteractionState,
options: ToolbarPaintOptions,
) -> ToolbarLayout;
fn toolbar_layout(&self, rect: Rect, bar: &Toolbar) -> ToolbarLayout;
fn draw_sidebar_panel_interactive(
&mut self,
rect: Rect,
panel: &SidebarPanel,
interaction: &InteractionState,
) -> SidebarPanelLayout;
fn sidebar_panel_layout(&self, rect: Rect, panel: &SidebarPanel) -> SidebarPanelLayout;
fn draw_chart(
&mut self,
rect: Rect,
chart: &Chart,
hovered_point: Option<(usize, usize)>,
crosshair_x: Option<f64>,
) -> ChartLayout;
fn chart_layout(&self, rect: Rect, chart: &Chart) -> ChartLayout;
fn draw_board(&mut self, rect: Rect, model: &BoardModel) -> BoardLayout;
fn board_layout(&self, rect: Rect, model: &BoardModel) -> BoardLayout;
fn draw_minimap(&mut self, rect: Rect, minimap: &Minimap) -> MinimapPaintResult;
fn minimap_scale(&self) -> MinimapScale {
MinimapScale::One
}
fn set_minimap_scale(&mut self, _scale: MinimapScale) {}
fn minimap_sizing(&self) -> Option<crate::primitives::minimap::MinimapSizing> {
None
}
fn set_minimap_sizing(&mut self, _sizing: Option<crate::primitives::minimap::MinimapSizing>) {}
fn minimap_layout(&self, rect: Rect, minimap: &Minimap) -> MinimapLayout;
fn draw_image(&mut self, rect: Rect, image: &Image) -> ImagePaintResult;
fn draw_canvas(&mut self, rect: Rect, canvas: &Canvas) -> CanvasLayout;
fn canvas_layout(&self, rect: Rect, canvas: &Canvas) -> CanvasLayout {
let _ = canvas;
CanvasLayout { bounds: rect }
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MinimapPaintResult {
pub layout: MinimapLayout,
pub painted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImagePaintResult {
Painted,
Unsupported,
}
#[allow(deprecated)]
pub fn shift_tab_bar_hits(hits: &mut TabBarHits, dx: f64) {
if dx == 0.0 {
return;
}
for sp in &mut hits.slot_positions {
if *sp != (0.0, 0.0) {
sp.0 += dx;
sp.1 += dx;
}
}
for cb in hits.close_bounds.iter_mut().flatten() {
cb.0 += dx;
cb.1 += dx;
}
for rb in &mut hits.right_segment_bounds {
rb.0 += dx;
rb.1 += dx;
}
}
#[allow(deprecated)]
pub fn tab_bar_hits_from_layout(layout: &TabBarLayout, bar: &TabBar) -> TabBarHits {
let mut slot_positions = vec![(0.0, 0.0); bar.tabs.len()];
let mut close_bounds = vec![None; bar.tabs.len()];
let mut right_segment_bounds = Vec::new();
for vt in &layout.visible_tabs {
let b = vt.bounds;
slot_positions[vt.tab_idx] = (b.x as f64, (b.x + b.width) as f64);
if let Some(cb) = vt.close_bounds {
close_bounds[vt.tab_idx] = Some((cb.x as f64, (cb.x + cb.width) as f64));
}
}
for vs in &layout.visible_segments {
let b = vs.bounds;
right_segment_bounds.push((b.x as f64, (b.x + b.width) as f64));
}
TabBarHits {
slot_positions,
close_bounds,
right_segment_bounds,
available_cols: layout.bar_width as usize,
correct_scroll_offset: layout.resolved_scroll_offset,
}
}
pub fn activity_bar_hits(rect: Rect, bar: &ActivityBar, lh: f32) -> Vec<ActivityBarRowHit> {
let mut hits = Vec::new();
let mut y = 0.0_f32;
for item in &bar.top_items {
hits.push(ActivityBarRowHit {
id: item.id,
tooltip: item.tooltip.clone(),
y_start: y,
y_end: y + lh,
});
y += lh;
}
let bottom_start = rect.height - bar.bottom_items.len() as f32 * lh;
let mut by = bottom_start.max(y);
for item in &bar.bottom_items {
hits.push(ActivityBarRowHit {
id: item.id,
tooltip: item.tooltip.clone(),
y_start: by,
y_end: by + lh,
});
by += lh;
}
hits
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct EditorPaintResult {
pub cursor_position_native: Option<Point>,
}
pub trait BackendWidget: Send + 'static {
fn render(&self, backend: &mut dyn Backend, rect: Rect);
}
pub trait PlatformServices {
fn clipboard(&self) -> &dyn Clipboard;
fn show_file_open_dialog(&self, opts: FileDialogOptions) -> Option<PathBuf>;
fn show_file_save_dialog(&self, opts: FileDialogOptions) -> Option<PathBuf>;
fn show_folder_open_dialog(&self, opts: FileDialogOptions) -> Option<PathBuf>;
fn show_message_dialog(&self, opts: MessageDialogOptions) -> Option<MessageDialogChoice>;
fn send_notification(&self, n: Notification);
fn open_url(&self, url: &str);
fn open_url_result(&self, url: &str) -> ServiceResult<()> {
self.open_url(url);
Ok(())
}
fn reveal_in_file_manager(&self, path: &Path) -> ServiceResult<()> {
let _ = path;
Err(BackendError::Unsupported)
}
fn open_path(&self, path: &Path) -> ServiceResult<()> {
let _ = path;
Err(BackendError::Unsupported)
}
fn move_to_trash(&self, path: &Path) -> ServiceResult<()> {
let _ = path;
Err(BackendError::Unsupported)
}
fn beep(&self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn system_theme(&self) -> ServiceResult<SystemTheme> {
Err(BackendError::Unsupported)
}
fn secret_store(&self) -> &dyn SecretStore {
static STORE: KeyringSecretStore = KeyringSecretStore;
&STORE
}
fn displays(&self) -> ServiceResult<Vec<Display>> {
Err(BackendError::Unsupported)
}
fn cursor_screen_point(&self) -> ServiceResult<Point> {
Err(BackendError::Unsupported)
}
fn platform_name(&self) -> &'static str;
}
pub trait SecretStore {
fn get(&self, service: &str, account: &str) -> ServiceResult<Option<String>>;
fn set(&self, service: &str, account: &str, secret: &str) -> ServiceResult<()>;
fn delete(&self, service: &str, account: &str) -> ServiceResult<()>;
}
#[cfg(any(feature = "tui", feature = "gtk", feature = "macos", feature = "win"))]
struct KeyringSecretStore;
#[cfg(any(feature = "tui", feature = "gtk", feature = "macos", feature = "win"))]
impl SecretStore for KeyringSecretStore {
fn get(&self, service: &str, account: &str) -> ServiceResult<Option<String>> {
let entry = keyring::Entry::new(service, account).map_err(keyring_error_to_backend)?;
match entry.get_password() {
Ok(secret) => Ok(Some(secret)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(keyring_error_to_backend(e)),
}
}
fn set(&self, service: &str, account: &str, secret: &str) -> ServiceResult<()> {
let entry = keyring::Entry::new(service, account).map_err(keyring_error_to_backend)?;
entry.set_password(secret).map_err(keyring_error_to_backend)
}
fn delete(&self, service: &str, account: &str) -> ServiceResult<()> {
let entry = keyring::Entry::new(service, account).map_err(keyring_error_to_backend)?;
entry.delete_credential().map_err(keyring_error_to_backend)
}
}
#[cfg(any(feature = "tui", feature = "gtk", feature = "macos", feature = "win"))]
fn keyring_error_to_backend(e: keyring::Error) -> BackendError {
BackendError::PlatformFailure {
context: format!("keyring: {e}"),
}
}
#[cfg(not(any(feature = "tui", feature = "gtk", feature = "macos", feature = "win")))]
struct KeyringSecretStore;
#[cfg(not(any(feature = "tui", feature = "gtk", feature = "macos", feature = "win")))]
impl SecretStore for KeyringSecretStore {
fn get(&self, _service: &str, _account: &str) -> ServiceResult<Option<String>> {
Err(BackendError::Unsupported)
}
fn set(&self, _service: &str, _account: &str, _secret: &str) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn delete(&self, _service: &str, _account: &str) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Display {
pub bounds: Rect,
pub work_area: Rect,
pub scale: f32,
pub primary: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SystemTheme {
pub dark: bool,
pub accent: Option<Color>,
pub high_contrast: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RgbaImage {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipboardFormat {
Text,
Image,
FileList,
}
pub trait Clipboard {
fn read_text(&self) -> Option<String>;
fn write_text(&self, text: &str);
fn write_text_result(&self, text: &str) -> ServiceResult<()> {
self.write_text(text);
Ok(())
}
fn read_primary_selection(&self) -> Option<String> {
None
}
fn read_image(&self) -> ServiceResult<RgbaImage> {
Err(BackendError::Unsupported)
}
fn write_image(&self, _image: &RgbaImage) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn write_html(&self, _html: &str, _alt_text: &str) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn read_file_list(&self) -> ServiceResult<Vec<PathBuf>> {
Err(BackendError::Unsupported)
}
fn formats(&self) -> Vec<ClipboardFormat> {
let mut out = Vec::new();
if self.read_text().is_some() {
out.push(ClipboardFormat::Text);
}
if self.read_image().is_ok() {
out.push(ClipboardFormat::Image);
}
if matches!(self.read_file_list(), Ok(list) if !list.is_empty()) {
out.push(ClipboardFormat::FileList);
}
out
}
fn clear(&self) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
}
#[derive(Debug, Clone, Default)]
pub struct FileDialogOptions {
pub title: Option<String>,
pub initial_dir: Option<PathBuf>,
pub initial_filename: Option<String>,
pub filters: Vec<(String, Vec<String>)>,
}
#[derive(Debug, Clone)]
pub struct MessageDialogOptions {
pub title: String,
pub body: String,
pub buttons: Vec<MessageDialogButton>,
pub severity: Option<DialogSeverity>,
}
#[derive(Debug, Clone)]
pub struct MessageDialogButton {
pub id: WidgetId,
pub label: String,
pub is_default: bool,
pub is_cancel: bool,
}
pub type MessageDialogChoice = WidgetId;
#[derive(Debug, Clone)]
pub struct Notification {
pub title: String,
pub body: String,
pub urgent: bool,
icon: Option<ImageSource>,
actions: Vec<(WidgetId, String)>,
silent: bool,
tag: Option<String>,
}
impl Notification {
pub fn new(title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
title: title.into(),
body: body.into(),
urgent: false,
icon: None,
actions: Vec::new(),
silent: false,
tag: None,
}
}
#[must_use]
pub fn with_icon(mut self, icon: ImageSource) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn with_action(mut self, id: WidgetId, label: impl Into<String>) -> Self {
self.actions.push((id, label.into()));
self
}
#[must_use]
pub fn with_silent(mut self, silent: bool) -> Self {
self.silent = silent;
self
}
#[must_use]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
pub fn icon(&self) -> Option<&ImageSource> {
self.icon.as_ref()
}
pub fn actions(&self) -> &[(WidgetId, String)] {
&self.actions
}
pub fn is_silent(&self) -> bool {
self.silent
}
pub fn tag(&self) -> Option<&str> {
self.tag.as_deref()
}
}
#[cfg(test)]
mod notification_tests {
use super::Notification;
use crate::primitives::image::ImageSource;
use crate::types::WidgetId;
#[test]
fn new_has_no_icon_actions_tag_and_is_not_silent_or_urgent() {
let n = Notification::new("t", "b");
assert_eq!(n.title, "t");
assert_eq!(n.body, "b");
assert!(!n.urgent);
assert!(n.icon().is_none());
assert!(n.actions().is_empty());
assert!(!n.is_silent());
assert!(n.tag().is_none());
}
#[test]
fn with_icon_is_read_back_by_icon() {
let n = Notification::new("t", "b").with_icon(ImageSource::Bytes(vec![1, 2, 3]));
assert_eq!(n.icon(), Some(&ImageSource::Bytes(vec![1, 2, 3])));
}
#[test]
fn with_action_appends_in_registration_order() {
let n = Notification::new("t", "b")
.with_action(WidgetId::new("first"), "First")
.with_action(WidgetId::new("second"), "Second");
assert_eq!(
n.actions(),
&[
(WidgetId::new("first"), "First".to_string()),
(WidgetId::new("second"), "Second".to_string()),
]
);
}
#[test]
fn with_silent_is_read_back_by_is_silent() {
assert!(Notification::new("t", "b").with_silent(true).is_silent());
assert!(!Notification::new("t", "b").with_silent(false).is_silent());
}
#[test]
fn with_tag_is_read_back_by_tag() {
let n = Notification::new("t", "b").with_tag("build-status");
assert_eq!(n.tag(), Some("build-status"));
}
}
#[cfg(test)]
mod backend_caps_tests {
use super::BackendCaps;
#[test]
fn empty_declares_nothing() {
assert!(BackendCaps::empty().names().is_empty());
assert!(!BackendCaps::empty().has("text_selection"));
}
#[test]
fn names_round_trips_through_has() {
let caps = BackendCaps {
text_selection: true,
file_dialogs: true,
..BackendCaps::empty()
};
let names = caps.names();
assert_eq!(names, vec!["text_selection", "file_dialogs"]);
for name in &names {
assert!(caps.has(name), "{name:?} in names() but has() disagrees");
}
for (name, _) in BackendCaps::ALL_NAMES {
if !names.contains(name) {
assert!(!caps.has(name), "{name:?} not in names() but has() = true");
}
}
}
#[test]
fn has_is_false_not_a_panic_for_an_unknown_name() {
assert!(!BackendCaps::empty().has("not_a_real_capability"));
}
type NamedSetter = (&'static str, fn(&mut BackendCaps));
const SETTERS: &[NamedSetter] = &[
("mouse", |c| c.mouse = true),
("scroll", |c| c.scroll = true),
("drag", |c| c.drag = true),
("text_selection", |c| c.text_selection = true),
("native_menu", |c| c.native_menu = true),
("window_chrome", |c| c.window_chrome = true),
("pointer_cursor", |c| c.pointer_cursor = true),
("ime", |c| c.ime = true),
("file_dialogs", |c| c.file_dialogs = true),
("folder_dialogs", |c| c.folder_dialogs = true),
("native_dialogs", |c| c.native_dialogs = true),
("notifications", |c| c.notifications = true),
("app_font_registration", |c| c.app_font_registration = true),
("window_control", |c| c.window_control = true),
("tray", |c| c.tray = true),
("generic_font_families", |c| c.generic_font_families = true),
];
#[test]
fn all_names_lists_every_field_exactly_once() {
let BackendCaps {
mouse: _,
scroll: _,
drag: _,
text_selection: _,
native_menu: _,
window_chrome: _,
pointer_cursor: _,
ime: _,
file_dialogs: _,
folder_dialogs: _,
native_dialogs: _,
notifications: _,
app_font_registration: _,
window_control: _,
tray: _,
generic_font_families: _,
color_depth: _,
kitty_keyboard: _,
sgr_pixel_mouse: _,
} = BackendCaps::empty();
let want: Vec<&str> = SETTERS.iter().map(|(n, _)| *n).collect();
let got: Vec<&str> = BackendCaps::ALL_NAMES.iter().map(|(n, _)| *n).collect();
assert_eq!(got, want);
assert_eq!(BackendCaps::vocabulary(), want);
}
#[test]
fn each_accessor_reads_its_own_field() {
for (name, set) in SETTERS {
let mut caps = BackendCaps::empty();
set(&mut caps);
assert_eq!(
caps.names(),
vec![*name],
"setting only `{name}` should make exactly `{name}` declared — an `ALL_NAMES` \
accessor is reading the wrong field"
);
assert!(caps.has(name));
}
}
#[test]
fn vocabulary_is_the_superset_names_draws_from() {
let vocab = BackendCaps::vocabulary();
let mut all = BackendCaps::empty();
for (_, set) in SETTERS {
set(&mut all);
}
assert_eq!(
all.names(),
vocab,
"with every field true, `names()` must be the whole vocabulary"
);
}
}
#[cfg(test)]
mod clipboard_default_tests {
use super::*;
use std::cell::RefCell;
#[derive(Default)]
struct FakeTextOnlyClipboard {
text: RefCell<Option<String>>,
}
impl Clipboard for FakeTextOnlyClipboard {
fn read_text(&self) -> Option<String> {
self.text.borrow().clone()
}
fn write_text(&self, text: &str) {
*self.text.borrow_mut() = Some(text.to_string());
}
}
#[test]
fn image_html_file_list_and_clear_default_to_unsupported() {
let cb = FakeTextOnlyClipboard::default();
assert_eq!(cb.read_image(), Err(BackendError::Unsupported));
assert_eq!(
cb.write_image(&RgbaImage {
width: 1,
height: 1,
pixels: vec![0, 0, 0, 255],
}),
Err(BackendError::Unsupported)
);
assert_eq!(
cb.write_html("<b>hi</b>", "hi"),
Err(BackendError::Unsupported)
);
assert_eq!(cb.read_file_list(), Err(BackendError::Unsupported));
assert_eq!(cb.clear(), Err(BackendError::Unsupported));
}
#[test]
fn formats_is_empty_when_clipboard_is_empty() {
let cb = FakeTextOnlyClipboard::default();
assert!(cb.formats().is_empty());
}
#[test]
fn formats_reports_text_once_written_and_nothing_else() {
let cb = FakeTextOnlyClipboard::default();
cb.write_text("hello");
assert_eq!(cb.formats(), vec![ClipboardFormat::Text]);
}
#[derive(Default)]
struct FakeFullClipboard {
text: RefCell<Option<String>>,
image: RefCell<Option<RgbaImage>>,
file_list: RefCell<Vec<PathBuf>>,
}
impl Clipboard for FakeFullClipboard {
fn read_text(&self) -> Option<String> {
self.text.borrow().clone()
}
fn write_text(&self, text: &str) {
*self.text.borrow_mut() = Some(text.to_string());
}
fn read_image(&self) -> ServiceResult<RgbaImage> {
self.image.borrow().clone().ok_or(BackendError::Unsupported)
}
fn read_file_list(&self) -> ServiceResult<Vec<PathBuf>> {
Ok(self.file_list.borrow().clone())
}
}
#[test]
fn formats_dispatches_through_overrides_for_image_and_file_list() {
let cb = FakeFullClipboard::default();
assert!(cb.formats().is_empty());
*cb.image.borrow_mut() = Some(RgbaImage {
width: 2,
height: 2,
pixels: vec![0; 16],
});
assert_eq!(cb.formats(), vec![ClipboardFormat::Image]);
cb.file_list.borrow_mut().push(PathBuf::from("/tmp/a.txt"));
assert_eq!(
cb.formats(),
vec![ClipboardFormat::Image, ClipboardFormat::FileList]
);
cb.write_text("hi");
assert_eq!(
cb.formats(),
vec![
ClipboardFormat::Text,
ClipboardFormat::Image,
ClipboardFormat::FileList
]
);
}
#[test]
fn empty_file_list_does_not_count_as_the_file_list_format() {
let cb = FakeFullClipboard::default();
assert!(cb.formats().is_empty());
}
}
#[cfg(all(
test,
any(feature = "tui", feature = "gtk", feature = "macos", feature = "win")
))]
mod secret_store_tests {
use super::*;
const SERVICE: &str = "quadraui-958-secret-store-test";
fn unique_account(suffix: &str) -> String {
format!("account-{}-{suffix}", std::process::id())
}
#[allow(clippy::print_stderr)]
fn answered<T>(op: &str, result: ServiceResult<T>) -> Option<T> {
match result {
Ok(v) => Some(v),
Err(e) => {
eprintln!(
"skipping: secret_store {op} failed in this environment ({e:?}) — \
no reachable/unlockable keyring (no D-Bus session, or a collection \
that only an interactive prompt can unlock)"
);
None
}
}
}
#[test]
fn round_trip_set_get_delete() {
let store = KeyringSecretStore;
let account = unique_account("round-trip");
if answered("set()", store.set(SERVICE, &account, "s3cr3t")).is_none() {
return;
}
let Some(read_back) = answered("get()", store.get(SERVICE, &account)) else {
let _ = store.delete(SERVICE, &account);
return;
};
assert_eq!(
read_back,
Some("s3cr3t".to_string()),
"get() must return exactly what set() just wrote"
);
if answered("delete()", store.delete(SERVICE, &account)).is_none() {
return;
}
let Some(after_delete) = answered("get() after delete()", store.get(SERVICE, &account))
else {
return;
};
assert_eq!(
after_delete, None,
"get() after delete() must report no entry, not the deleted secret"
);
}
#[test]
fn get_on_an_entry_that_was_never_set_reports_no_entry() {
let store = KeyringSecretStore;
let account = unique_account("never-set");
let Some(found) = answered("get()", store.get(SERVICE, &account)) else {
return;
};
assert_eq!(
found, None,
"fresh service/account pair should have no entry — leftover from a previous \
test run?"
);
}
#[test]
fn delete_on_a_nonexistent_entry_is_a_reported_failure() {
let store = KeyringSecretStore;
let account = unique_account("delete-missing");
assert!(
store.delete(SERVICE, &account).is_err(),
"delete() on an entry that was never set must not report success"
);
}
#[test]
fn keyring_errors_become_platform_failures_carrying_their_display_text() {
let mapped = keyring_error_to_backend(keyring::Error::NoEntry);
let BackendError::PlatformFailure { context } = mapped else {
panic!("every keyring::Error must map to PlatformFailure, got {mapped:?}");
};
assert!(
context.starts_with("keyring: "),
"context must name the failing subsystem, got {context:?}"
);
assert!(
context.len() > "keyring: ".len(),
"context must keep the keyring error's own Display text, got {context:?}"
);
}
}
#[cfg(all(
test,
not(any(feature = "tui", feature = "gtk", feature = "macos", feature = "win"))
))]
mod secret_store_unsupported_tests {
use super::*;
#[test]
fn every_method_reports_unsupported_without_a_keyring_backend() {
let store = KeyringSecretStore;
assert_eq!(store.get("svc", "acct"), Err(BackendError::Unsupported));
assert_eq!(
store.set("svc", "acct", "secret"),
Err(BackendError::Unsupported)
);
assert_eq!(store.delete("svc", "acct"), Err(BackendError::Unsupported));
}
}