use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use iced::font::Weight as FontWeight;
use iced::widget::{
button, checkbox, column, container, pick_list, rule, scrollable, slider as iced_slider,
span as iced_span, text, text_input, tooltip, Column, Row, Space, Stack,
};
use iced::{Color, Element, Font};
use crate::theme::palette_to_iced_theme;
use oxiui_core::response::{
CheckboxResponse, DropdownResponse, SliderResponse, TextAreaResponse, TextInputResponse,
WidgetResponse,
};
use oxiui_core::{ButtonResponse, Palette, UiCtx};
#[inline]
fn palettes_equal(a: &Palette, b: &Palette) -> bool {
a.background == b.background
&& a.surface == b.surface
&& a.primary == b.primary
&& a.on_primary == b.on_primary
&& a.text == b.text
&& a.muted == b.muted
}
#[derive(Default)]
pub struct ThemeCache {
last_palette: Option<Palette>,
cached_theme: Option<iced::Theme>,
}
impl ThemeCache {
pub fn get_or_compute(&mut self, palette: &Palette) -> iced::Theme {
let hit = self
.last_palette
.as_ref()
.is_some_and(|prev| palettes_equal(prev, palette));
if !hit {
let theme = palette_to_iced_theme(palette);
self.last_palette = Some(palette.clone());
self.cached_theme = Some(theme);
}
self.cached_theme.clone().unwrap_or(iced::Theme::Light)
}
}
#[derive(Debug, Clone)]
pub enum Message {
ButtonPressed(usize),
TextChanged(usize, String),
CheckboxToggled(usize, bool),
SliderChanged(usize, f64),
DropdownSelected(usize, usize),
TextAreaChanged(usize, String),
}
#[derive(Debug, Clone)]
pub enum WidgetState {
Text(String),
Checked(bool),
Slider(f64),
Selected(usize),
TextArea(String),
}
#[derive(Debug, Default, Clone)]
pub struct IcedConfig {
pub pending_clicks: HashSet<usize>,
pub state: HashMap<usize, WidgetState>,
pub spacing: f32,
pub padding: f32,
pub title: String,
pub spec_capacity_hint: usize,
}
impl IcedConfig {
#[must_use]
pub fn with_spacing(mut self, px: f32) -> Self {
self.spacing = px;
self
}
#[must_use]
pub fn with_padding(mut self, px: f32) -> Self {
self.padding = px;
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn with_spec_capacity(mut self, hint: usize) -> Self {
self.spec_capacity_hint = hint;
self
}
}
pub fn apply_message(
state: &mut HashMap<usize, WidgetState>,
clicks: &mut HashSet<usize>,
msg: &Message,
) {
match msg {
Message::ButtonPressed(id) => {
clicks.insert(*id);
}
Message::TextChanged(id, s) => {
state.insert(*id, WidgetState::Text(s.clone()));
}
Message::CheckboxToggled(id, b) => {
state.insert(*id, WidgetState::Checked(*b));
}
Message::SliderChanged(id, v) => {
state.insert(*id, WidgetState::Slider(*v));
}
Message::DropdownSelected(id, i) => {
state.insert(*id, WidgetState::Selected(*i));
}
Message::TextAreaChanged(id, s) => {
state.insert(*id, WidgetState::TextArea(s.clone()));
}
}
}
#[derive(Clone, Debug)]
pub struct IcedSpan {
pub text: String,
pub color: Option<[u8; 4]>,
pub bold: bool,
pub size: Option<f32>,
}
#[derive(Debug, Clone)]
pub enum WidgetSpec {
Heading(Cow<'static, str>),
Label(Cow<'static, str>),
Button {
id: usize,
label: Cow<'static, str>,
},
TextInput {
id: usize,
value: Cow<'static, str>,
placeholder: Cow<'static, str>,
},
TextArea {
id: usize,
value: Cow<'static, str>,
min_rows: usize,
},
Checkbox {
id: usize,
label: Cow<'static, str>,
checked: bool,
},
Slider {
id: usize,
value: f64,
start: f64,
end: f64,
},
Dropdown {
id: usize,
options: Vec<String>,
selected: usize,
},
Image {
uri: Cow<'static, str>,
size: Option<oxiui_core::geometry::Size>,
},
Separator,
Spacer {
size: f32,
},
Scroll {
children: Vec<WidgetSpec>,
},
Tooltip {
inner: Box<WidgetSpec>,
text: Cow<'static, str>,
},
Popup {
children: Vec<WidgetSpec>,
},
Modal {
title: Cow<'static, str>,
children: Vec<WidgetSpec>,
},
Horizontal(Vec<WidgetSpec>),
Vertical(Vec<WidgetSpec>),
Grid {
cols: usize,
children: Vec<WidgetSpec>,
},
RichText(Vec<IcedSpan>),
}
pub struct IcedUiCtx {
specs: Vec<WidgetSpec>,
next_id: usize,
pending_clicks: HashSet<usize>,
state: HashMap<usize, WidgetState>,
spacing: f32,
padding: f32,
}
impl IcedUiCtx {
pub fn new(config: IcedConfig) -> Self {
let capacity = config.spec_capacity_hint.max(8);
Self {
specs: Vec::with_capacity(capacity),
next_id: 0,
pending_clicks: config.pending_clicks,
state: config.state,
spacing: config.spacing,
padding: config.padding,
}
}
pub fn spec_count(&self) -> usize {
self.specs.len()
}
fn alloc_id(&mut self) -> usize {
let i = self.next_id;
self.next_id += 1;
i
}
fn child(&self) -> IcedUiCtx {
IcedUiCtx {
specs: Vec::new(),
next_id: self.next_id,
pending_clicks: self.pending_clicks.clone(),
state: self.state.clone(),
spacing: self.spacing,
padding: self.padding,
}
}
pub fn into_specs(self) -> Vec<WidgetSpec> {
self.specs
}
pub fn into_iced_element(self) -> Element<'static, Message> {
build_column(self.specs, self.spacing)
}
}
impl UiCtx for IcedUiCtx {
fn heading(&mut self, t: &str) {
self.specs
.push(WidgetSpec::Heading(Cow::Owned(t.to_owned())));
}
fn label(&mut self, t: &str) {
self.specs.push(WidgetSpec::Label(Cow::Owned(t.to_owned())));
}
fn button(&mut self, label: &str) -> ButtonResponse {
let id = self.alloc_id();
self.specs.push(WidgetSpec::Button {
id,
label: Cow::Owned(label.to_owned()),
});
ButtonResponse {
clicked: self.pending_clicks.contains(&id),
hovered: false,
}
}
fn text_input(&mut self, text: &str) -> TextInputResponse {
let id = self.alloc_id();
let cur = match self.state.get(&id) {
Some(WidgetState::Text(s)) => s.clone(),
_ => text.to_owned(),
};
let changed = cur != text;
self.specs.push(WidgetSpec::TextInput {
id,
value: Cow::Owned(cur.clone()),
placeholder: Cow::Borrowed(""),
});
TextInputResponse::supported(cur, changed)
}
fn text_area(&mut self, text: &str, min_rows: usize) -> TextAreaResponse {
let id = self.alloc_id();
let cur = match self.state.get(&id) {
Some(WidgetState::TextArea(s)) => s.clone(),
_ => text.to_owned(),
};
let changed = cur != text;
let cursor_pos = {
let lines: Vec<&str> = cur.lines().collect();
let row = lines.len().saturating_sub(1);
let col = lines.last().map(|l| l.len()).unwrap_or(0);
(row, col)
};
self.specs.push(WidgetSpec::TextArea {
id,
value: Cow::Owned(cur.clone()),
min_rows: min_rows.max(1),
});
TextAreaResponse::supported(cur, changed, cursor_pos)
}
fn checkbox(&mut self, label: &str, checked: bool) -> CheckboxResponse {
let id = self.alloc_id();
let cur = match self.state.get(&id) {
Some(WidgetState::Checked(b)) => *b,
_ => checked,
};
let changed = cur != checked;
self.specs.push(WidgetSpec::Checkbox {
id,
label: Cow::Owned(label.to_owned()),
checked: cur,
});
CheckboxResponse::supported(cur, changed)
}
fn slider(&mut self, value: f64, range: std::ops::RangeInclusive<f64>) -> SliderResponse {
let id = self.alloc_id();
let cur = match self.state.get(&id) {
Some(WidgetState::Slider(v)) => *v,
_ => value,
};
let changed = (cur - value).abs() > f64::EPSILON;
self.specs.push(WidgetSpec::Slider {
id,
value: cur,
start: *range.start(),
end: *range.end(),
});
SliderResponse::supported(cur, changed)
}
fn dropdown(&mut self, options: &[&str], selected: usize) -> DropdownResponse {
let id = self.alloc_id();
let cur = match self.state.get(&id) {
Some(WidgetState::Selected(i)) => *i,
_ => selected,
};
let changed = cur != selected;
let opts: Vec<String> = options.iter().map(|s| s.to_string()).collect();
self.specs.push(WidgetSpec::Dropdown {
id,
options: opts,
selected: cur,
});
DropdownResponse::supported(cur, changed)
}
fn image(&mut self, uri: &str, size: Option<oxiui_core::geometry::Size>) -> WidgetResponse {
self.specs.push(WidgetSpec::Image {
uri: Cow::Owned(uri.to_owned()),
size,
});
WidgetResponse::supported()
}
fn separator(&mut self) -> WidgetResponse {
self.specs.push(WidgetSpec::Separator);
WidgetResponse::supported()
}
fn spacer(&mut self, size: f32) -> WidgetResponse {
self.specs.push(WidgetSpec::Spacer { size });
WidgetResponse::supported()
}
fn scroll_area(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let mut child = self.child();
content(&mut child);
self.next_id = child.next_id;
self.specs.push(WidgetSpec::Scroll {
children: child.specs,
});
WidgetResponse::supported()
}
fn tooltip(&mut self, text: &str) -> WidgetResponse {
if let Some(inner) = self.specs.pop() {
self.specs.push(WidgetSpec::Tooltip {
inner: Box::new(inner),
text: Cow::Owned(text.to_owned()),
});
WidgetResponse::supported()
} else {
WidgetResponse::unsupported()
}
}
fn popup(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let mut child = self.child();
content(&mut child);
self.next_id = child.next_id;
self.specs.push(WidgetSpec::Popup {
children: child.specs,
});
WidgetResponse::supported()
}
fn modal(&mut self, title: &str, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let mut child = self.child();
content(&mut child);
self.next_id = child.next_id;
self.specs.push(WidgetSpec::Modal {
title: Cow::Owned(title.to_owned()),
children: child.specs,
});
WidgetResponse::supported()
}
fn horizontal(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let mut child = self.child();
content(&mut child);
self.next_id = child.next_id;
self.specs.push(WidgetSpec::Horizontal(child.specs));
WidgetResponse::supported()
}
fn vertical(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let mut child = self.child();
content(&mut child);
self.next_id = child.next_id;
self.specs.push(WidgetSpec::Vertical(child.specs));
WidgetResponse::supported()
}
fn grid(&mut self, cols: usize, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
let mut child = self.child();
content(&mut child);
self.next_id = child.next_id;
self.specs.push(WidgetSpec::Grid {
cols,
children: child.specs,
});
WidgetResponse::supported()
}
fn rich_text(&mut self, spans: &[oxiui_core::RichTextSpan]) -> WidgetResponse {
let iced_spans: Vec<IcedSpan> = spans
.iter()
.map(|s| IcedSpan {
text: s.text.clone(),
color: Some(s.color),
bold: s.bold,
size: Some(s.font_size),
})
.collect();
self.specs.push(WidgetSpec::RichText(iced_spans));
WidgetResponse::supported()
}
}
pub fn spec_fingerprint(spec: &WidgetSpec) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
format!("{spec:?}").hash(&mut h);
h.finish()
}
#[derive(Debug, Default, Clone)]
pub struct SpecCache {
fingerprints: Vec<u64>,
rebuild_count: usize,
}
impl SpecCache {
pub fn sync(&mut self, specs: &[WidgetSpec]) -> bool {
let changed = if specs.len() != self.fingerprints.len() {
true
} else {
specs
.iter()
.zip(self.fingerprints.iter())
.any(|(spec, &cached)| spec_fingerprint(spec) != cached)
};
if changed {
self.fingerprints = specs.iter().map(spec_fingerprint).collect();
self.rebuild_count += 1;
}
changed
}
pub fn rebuild_count(&self) -> usize {
self.rebuild_count
}
}
fn build_one(spec: WidgetSpec, spacing: f32) -> Element<'static, Message> {
match spec {
WidgetSpec::Heading(t) => text(t.into_owned()).size(24).into(),
WidgetSpec::Label(t) => text(t.into_owned()).size(14).into(),
WidgetSpec::Button { id, label } => button(text(label.into_owned()))
.on_press(Message::ButtonPressed(id))
.into(),
WidgetSpec::TextInput {
id,
value,
placeholder,
} => {
let placeholder_owned = placeholder.into_owned();
let value_owned = value.into_owned();
text_input(&placeholder_owned, &value_owned)
.on_input(move |s| Message::TextChanged(id, s))
.into()
}
WidgetSpec::TextArea {
id,
value,
min_rows,
} => {
let lines: Vec<String> = {
let raw: Vec<&str> = value.as_ref().lines().collect();
let count = raw.len().max(min_rows);
let mut v: Vec<String> = raw.iter().map(|l| l.to_string()).collect();
v.resize(count, String::new());
v
};
let total_lines = lines.len();
let mut col: Column<'static, Message> = column![].spacing(2);
for (row_idx, line) in lines.into_iter().enumerate() {
let line_clone = line.clone();
let input = text_input("", &line).on_input(move |new_line| {
let _ = (total_lines, row_idx, line_clone.as_str());
Message::TextAreaChanged(id, new_line)
});
col = col.push(input);
}
col.into()
}
WidgetSpec::Checkbox { id, label, checked } => checkbox(checked)
.label(label.into_owned())
.on_toggle(move |b| Message::CheckboxToggled(id, b))
.into(),
WidgetSpec::Slider {
id,
value,
start,
end,
} => {
iced_slider((start as f32)..=(end as f32), value as f32, move |v| {
Message::SliderChanged(id, v as f64)
})
.into()
}
WidgetSpec::Dropdown {
id,
options,
selected,
} => {
let sel = options.get(selected).cloned();
let opts_clone = options.clone();
pick_list(options, sel, move |chosen: String| {
let idx = opts_clone.iter().position(|o| *o == chosen).unwrap_or(0);
Message::DropdownSelected(id, idx)
})
.into()
}
WidgetSpec::Image { uri, .. } => {
let handle = iced::widget::image::Handle::from_path(uri.as_ref());
iced::widget::image(handle).into()
}
WidgetSpec::Separator => rule::horizontal(1.0_f32).into(),
WidgetSpec::Spacer { size } => Space::new().height(size).into(),
WidgetSpec::Scroll { children } => {
let col = build_column(children, spacing);
scrollable(col).into()
}
WidgetSpec::Tooltip { inner, text: tip } => {
let tip_widget = container(text(tip.into_owned()));
tooltip(
build_one(*inner, spacing),
tip_widget,
tooltip::Position::Top,
)
.into()
}
WidgetSpec::Popup { children } => {
let col = build_column(children, spacing);
Stack::with_children([container(col).into()]).into()
}
WidgetSpec::Modal { title, children } => {
let mut col: Column<'static, Message> =
column![text(title.into_owned()).size(18)].spacing(spacing);
for c in children {
col = col.push(build_one(c, spacing));
}
container(col).padding(12).into()
}
WidgetSpec::Horizontal(specs) => {
let children: Vec<Element<'static, Message>> =
specs.into_iter().map(|s| build_one(s, spacing)).collect();
Row::with_children(children).spacing(spacing).into()
}
WidgetSpec::Vertical(specs) => build_column(specs, spacing),
WidgetSpec::Grid { cols, children } => {
let safe_cols = cols.max(1);
let row_elements: Vec<Element<'static, Message>> = children
.chunks(safe_cols)
.map(|row_specs| {
let row_children: Vec<Element<'static, Message>> = row_specs
.iter()
.map(|s| build_one(s.clone(), spacing))
.collect();
Row::with_children(row_children).spacing(spacing).into()
})
.collect();
build_column_from_elements(row_elements, spacing)
}
WidgetSpec::RichText(spans) => {
let iced_spans: Vec<iced::widget::text::Span<'static, (), Font>> = spans
.into_iter()
.map(|s| {
let mut sp = iced_span::<(), Font>(s.text);
if let Some([r, g, b, a]) = s.color {
sp = sp.color(Color::from_rgba8(r, g, b, a as f32 / 255.0));
}
if s.bold {
sp = sp.font(Font {
weight: FontWeight::Bold,
..Font::default()
});
}
if let Some(sz) = s.size {
sp = sp.size(sz);
}
sp
})
.collect();
iced::widget::rich_text(iced_spans).into()
}
}
}
fn build_column_from_elements(
elements: Vec<Element<'static, Message>>,
spacing: f32,
) -> Element<'static, Message> {
let mut col: Column<'static, Message> = column![].spacing(spacing);
for el in elements {
col = col.push(el);
}
col.into()
}
fn build_column(specs: Vec<WidgetSpec>, spacing: f32) -> Element<'static, Message> {
let mut col: Column<'static, Message> = column![].spacing(spacing);
for spec in specs {
col = col.push(build_one(spec, spacing));
}
col.into()
}
#[derive(Default)]
pub struct IcedNullCtx {
pub log: Option<Vec<(&'static str, String)>>,
}
impl IcedNullCtx {
pub fn recording() -> Self {
Self {
log: Some(Vec::new()),
}
}
fn record(&mut self, method: &'static str, arg: impl Into<String>) {
if let Some(l) = self.log.as_mut() {
l.push((method, arg.into()));
}
}
}
impl UiCtx for IcedNullCtx {
fn heading(&mut self, t: &str) {
self.record("heading", t);
}
fn label(&mut self, t: &str) {
self.record("label", t);
}
fn button(&mut self, label: &str) -> ButtonResponse {
self.record("button", label);
ButtonResponse::default()
}
fn text_input(&mut self, text: &str) -> TextInputResponse {
self.record("text_input", text);
TextInputResponse::unsupported()
}
fn text_area(&mut self, text: &str, min_rows: usize) -> TextAreaResponse {
self.record("text_area", format!("{text}|rows={min_rows}"));
TextAreaResponse::unsupported()
}
fn checkbox(&mut self, label: &str, _checked: bool) -> CheckboxResponse {
self.record("checkbox", label);
CheckboxResponse::unsupported()
}
fn slider(&mut self, value: f64, _range: std::ops::RangeInclusive<f64>) -> SliderResponse {
self.record("slider", value.to_string());
SliderResponse::unsupported()
}
fn dropdown(&mut self, _options: &[&str], selected: usize) -> DropdownResponse {
self.record("dropdown", selected.to_string());
DropdownResponse::unsupported()
}
fn image(&mut self, uri: &str, _size: Option<oxiui_core::geometry::Size>) -> WidgetResponse {
self.record("image", uri);
WidgetResponse::supported()
}
fn separator(&mut self) -> WidgetResponse {
self.record("separator", "");
WidgetResponse::unsupported()
}
fn spacer(&mut self, size: f32) -> WidgetResponse {
self.record("spacer", size.to_string());
WidgetResponse::unsupported()
}
fn scroll_area(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.record("scroll_area", "");
WidgetResponse::unsupported()
}
fn tooltip(&mut self, text: &str) -> WidgetResponse {
self.record("tooltip", text);
WidgetResponse::unsupported()
}
fn popup(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.record("popup", "");
WidgetResponse::unsupported()
}
fn modal(&mut self, title: &str, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.record("modal", title);
WidgetResponse::unsupported()
}
fn horizontal(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.record("horizontal", "");
WidgetResponse::unsupported()
}
fn vertical(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.record("vertical", "");
WidgetResponse::unsupported()
}
fn grid(&mut self, cols: usize, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
self.record("grid", cols.to_string());
WidgetResponse::unsupported()
}
fn rich_text(&mut self, spans: &[oxiui_core::RichTextSpan]) -> WidgetResponse {
self.record("rich_text", spans.len().to_string());
WidgetResponse::unsupported()
}
}
use iced::advanced::{layout, renderer, widget as adv_widget};
pub struct OxiIcedWidget {
spec: WidgetSpec,
width: iced::Length,
height: iced::Length,
}
impl OxiIcedWidget {
pub fn new(spec: WidgetSpec) -> Self {
OxiIcedWidget {
spec,
width: iced::Length::Shrink,
height: iced::Length::Shrink,
}
}
pub fn spec(&self) -> &WidgetSpec {
&self.spec
}
pub fn width(mut self, w: iced::Length) -> Self {
self.width = w;
self
}
pub fn height(mut self, h: iced::Length) -> Self {
self.height = h;
self
}
}
impl<Msg, Theme, Renderer> iced::advanced::Widget<Msg, Theme, Renderer> for OxiIcedWidget
where
Renderer: iced::advanced::Renderer,
{
fn size(&self) -> iced::Size<iced::Length> {
iced::Size::new(self.width, self.height)
}
fn layout(
&mut self,
_tree: &mut adv_widget::Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let size = limits.resolve(self.width, self.height, iced::Size::ZERO);
layout::Node::new(size)
}
fn draw(
&self,
_tree: &adv_widget::Tree,
_renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
_layout: iced::advanced::Layout<'_>,
_cursor: iced::advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
) {
}
}
pub fn oxi_widget(spec: WidgetSpec) -> OxiIcedWidget {
OxiIcedWidget::new(spec)
}
pub fn map_iced_keyboard_event(ev: &iced::keyboard::Event) -> Option<oxiui_core::UiEvent> {
use iced::keyboard::Event as KbEv;
match ev {
KbEv::KeyPressed {
key,
modifiers,
repeat,
..
} => Some(oxiui_core::UiEvent::KeyDown {
key: map_iced_key(key),
modifiers: map_iced_modifiers(*modifiers),
repeat: *repeat,
}),
KbEv::KeyReleased { key, modifiers, .. } => Some(oxiui_core::UiEvent::KeyUp {
key: map_iced_key(key),
modifiers: map_iced_modifiers(*modifiers),
}),
KbEv::ModifiersChanged(_) => None,
}
}
pub fn map_iced_key(key: &iced::keyboard::Key) -> oxiui_core::events::Key {
use iced::keyboard::key::Named;
use iced::keyboard::Key as IK;
use oxiui_core::events::Key as OxiKey;
match key {
IK::Character(s) => OxiKey::Character(s.as_str().to_owned()),
IK::Named(named) => match named {
Named::Enter => OxiKey::Enter,
Named::Tab => OxiKey::Tab,
Named::Space => OxiKey::Space,
Named::Backspace => OxiKey::Backspace,
Named::Delete => OxiKey::Delete,
Named::Escape => OxiKey::Escape,
Named::ArrowLeft => OxiKey::ArrowLeft,
Named::ArrowRight => OxiKey::ArrowRight,
Named::ArrowUp => OxiKey::ArrowUp,
Named::ArrowDown => OxiKey::ArrowDown,
Named::Home => OxiKey::Home,
Named::End => OxiKey::End,
Named::PageUp => OxiKey::PageUp,
Named::PageDown => OxiKey::PageDown,
Named::F1 => OxiKey::Function(1),
Named::F2 => OxiKey::Function(2),
Named::F3 => OxiKey::Function(3),
Named::F4 => OxiKey::Function(4),
Named::F5 => OxiKey::Function(5),
Named::F6 => OxiKey::Function(6),
Named::F7 => OxiKey::Function(7),
Named::F8 => OxiKey::Function(8),
Named::F9 => OxiKey::Function(9),
Named::F10 => OxiKey::Function(10),
Named::F11 => OxiKey::Function(11),
Named::F12 => OxiKey::Function(12),
other => OxiKey::Named(format!("{other:?}")),
},
IK::Unidentified => OxiKey::Named("Unidentified".to_owned()),
}
}
pub fn map_iced_modifiers(mods: iced::keyboard::Modifiers) -> oxiui_core::events::Modifiers {
oxiui_core::events::Modifiers {
ctrl: mods.control(),
shift: mods.shift(),
alt: mods.alt(),
meta: mods.logo(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_ctx_returns_supported() {
let mut ctx = IcedUiCtx::new(IcedConfig::default());
let resp = ctx.image("test.png", None);
assert!(resp.supported, "IcedUiCtx::image() must return supported");
}
#[test]
fn image_null_ctx_returns_supported() {
let mut ctx = IcedNullCtx::recording();
let resp = ctx.image("test.png", None);
assert!(resp.supported, "IcedNullCtx::image() must return supported");
}
#[test]
fn oxi_widget_constructs_with_shrink_defaults() {
let w = oxi_widget(WidgetSpec::Label(Cow::Borrowed("hello")));
assert_eq!(w.width, iced::Length::Shrink);
assert_eq!(w.height, iced::Length::Shrink);
}
#[test]
fn oxi_widget_builder_overrides_dimensions() {
let w = oxi_widget(WidgetSpec::Label(Cow::Borrowed("hi")))
.width(iced::Length::Fill)
.height(iced::Length::Fixed(100.0));
assert_eq!(w.width, iced::Length::Fill);
assert_eq!(w.height, iced::Length::Fixed(100.0));
}
#[test]
fn map_character_key_a() {
let key = iced::keyboard::Key::Character("a".into());
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::Character("a".to_owned()));
}
#[test]
fn map_character_key_z() {
let key = iced::keyboard::Key::Character("z".into());
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::Character("z".to_owned()));
}
#[test]
fn map_named_enter() {
let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::Enter);
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::Enter);
}
#[test]
fn map_named_escape() {
let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape);
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::Escape);
}
#[test]
fn map_named_arrow_left() {
let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowLeft);
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::ArrowLeft);
}
#[test]
fn map_named_arrow_right() {
let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowRight);
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::ArrowRight);
}
#[test]
fn map_named_f1() {
let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::F1);
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::Function(1));
}
#[test]
fn map_named_f12() {
let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::F12);
let result = map_iced_key(&key);
assert_eq!(result, oxiui_core::events::Key::Function(12));
}
#[test]
fn map_unidentified_key() {
let key = iced::keyboard::Key::Unidentified;
let result = map_iced_key(&key);
assert_eq!(
result,
oxiui_core::events::Key::Named("Unidentified".to_owned())
);
}
#[test]
fn map_modifiers_ctrl_shift() {
use iced::keyboard::Modifiers;
let mods = Modifiers::CTRL | Modifiers::SHIFT;
let result = map_iced_modifiers(mods);
assert!(result.ctrl, "ctrl must be set");
assert!(result.shift, "shift must be set");
assert!(!result.alt, "alt must not be set");
assert!(!result.meta, "meta must not be set");
}
#[test]
fn map_modifiers_none() {
use iced::keyboard::Modifiers;
let result = map_iced_modifiers(Modifiers::NONE);
assert!(!result.ctrl);
assert!(!result.shift);
assert!(!result.alt);
assert!(!result.meta);
}
#[test]
fn map_modifiers_alt_logo() {
use iced::keyboard::Modifiers;
let mods = Modifiers::ALT | Modifiers::LOGO;
let result = map_iced_modifiers(mods);
assert!(result.alt, "alt must be set");
assert!(result.meta, "meta must be set");
assert!(!result.ctrl);
assert!(!result.shift);
}
}