use core::ffi::c_void;
use core::marker::PhantomData;
use core::ops::RangeInclusive;
use std::sync::Mutex;
use crate::abi::{Header, Status, Str, UiDialog, UiFrame, UiWidget};
use crate::{Host, Load, Off};
mod hud;
mod map;
pub use hud::{Anchor, Hud};
pub use map::{MAX_MARKERS, Marker, MarkerColor};
pub const MAX_OPTIONS: usize = 64;
#[derive(Clone, Copy, Debug)]
pub struct Ui {
host: Host,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum UiError {
#[error("no interface to show it in")]
Unavailable,
#[error("too many dialogs wait")]
Busy,
#[error("waited for on the thread that draws the interface")]
WrongThread,
#[error("the loader refused the dialog")]
Refused,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Response {
Retry,
Disable,
Erase,
Reset,
Overwrite,
Cancel,
Ok,
Ignore,
}
impl Response {
#[must_use]
pub fn to_raw(self) -> u32 {
self.bit()
}
fn bit(self) -> u32 {
match self {
Self::Retry => UiDialog::RETRY,
Self::Disable => UiDialog::DISABLE,
Self::Erase => UiDialog::ERASE,
Self::Reset => UiDialog::RESET,
Self::Overwrite => UiDialog::OVERWRITE,
Self::Cancel => UiDialog::CANCEL,
Self::Ok => UiDialog::OK,
Self::Ignore => UiDialog::IGNORE,
}
}
#[must_use]
pub fn from_raw(raw: u32) -> Option<Self> {
Some(match raw {
UiDialog::RETRY => Self::Retry,
UiDialog::DISABLE => Self::Disable,
UiDialog::ERASE => Self::Erase,
UiDialog::RESET => Self::Reset,
UiDialog::OVERWRITE => Self::Overwrite,
UiDialog::CANCEL => Self::Cancel,
UiDialog::OK => Self::Ok,
UiDialog::IGNORE => Self::Ignore,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Dialog<'a> {
title: &'a str,
message: &'a str,
detail: &'a str,
buttons: u32,
}
impl<'a> Dialog<'a> {
#[must_use]
pub const fn new(title: &'a str, message: &'a str) -> Self {
Self {
title,
message,
detail: "",
buttons: 1 << UiDialog::OK,
}
}
#[must_use]
pub const fn detail(mut self, detail: &'a str) -> Self {
self.detail = detail;
self
}
#[must_use]
pub fn buttons(mut self, buttons: &[Response]) -> Self {
let bits = buttons.iter().fold(0, |bits, r| bits | 1 << r.bit());
if bits != 0 {
self.buttons = bits;
}
self
}
#[must_use]
pub const fn title(&self) -> &'a str {
self.title
}
fn to_raw(self, flags: u32) -> UiDialog {
UiDialog {
header: Header::of::<UiDialog>(),
title: Str::new(self.title),
message: Str::new(self.message),
detail: Str::new(self.detail),
buttons: self.buttons,
flags,
}
}
}
struct Answer<F> {
on_answer: F,
}
unsafe extern "system" fn answered<F>(answer: u32, user: *mut c_void)
where
F: FnOnce(Option<Response>),
{
let answer_box = unsafe { Box::from_raw(user.cast::<Answer<F>>()) };
crate::host::guard(
"panic in a dialog's answer",
|| (),
move || (answer_box.on_answer)(Response::from_raw(answer)),
);
}
impl Ui {
pub fn dialog<F>(&self, dialog: &Dialog<'_>, on_answer: F) -> Result<(), UiError>
where
F: FnOnce(Option<Response>) + Send + 'static,
{
self.queue(dialog, 0, on_answer)
}
pub fn ask(&self, dialog: &Dialog<'_>) -> Result<Option<Response>, UiError> {
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
self.queue(dialog, UiDialog::WAITING, move |answer| {
let _sent = sender.send(answer);
})?;
Ok(receiver.recv().unwrap_or(None))
}
fn queue<F>(&self, dialog: &Dialog<'_>, flags: u32, on_answer: F) -> Result<(), UiError>
where
F: FnOnce(Option<Response>) + Send + 'static,
{
let api = self.host.api();
let raw = dialog.to_raw(flags);
let user = Box::into_raw(Box::new(Answer { on_answer }));
let status = (api.ui_dialog)(
self.host.handle(),
&raw const raw,
answered::<F>,
user.cast(),
);
if status.is_ok() {
return Ok(());
}
drop(unsafe { Box::from_raw(user) });
Err(match status {
Status::MISSING => UiError::Unavailable,
Status::REFUSED => UiError::Busy,
Status::WRONG_THREAD => UiError::WrongThread,
_ => UiError::Refused,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Screen {
Menu,
Loading,
InGame,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Style {
#[default]
Body,
Secondary,
H1,
H2,
H3,
Warning,
Positive,
Negative,
}
impl Style {
fn to_raw(self) -> u32 {
match self {
Self::Body => UiWidget::STYLE_BODY,
Self::Secondary => UiWidget::STYLE_SECONDARY,
Self::H1 => UiWidget::STYLE_H1,
Self::H2 => UiWidget::STYLE_H2,
Self::H3 => UiWidget::STYLE_H3,
Self::Warning => UiWidget::STYLE_WARNING,
Self::Positive => UiWidget::STYLE_POSITIVE,
Self::Negative => UiWidget::STYLE_NEGATIVE,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Sound {
Click,
Confirm,
Back,
Error,
}
impl Sound {
fn to_raw(self) -> u32 {
match self {
Self::Click => UiWidget::SOUND_CLICK,
Self::Confirm => UiWidget::SOUND_CONFIRM,
Self::Back => UiWidget::SOUND_BACK,
Self::Error => UiWidget::SOUND_ERROR,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Label<'a> {
text: &'a str,
style: Style,
flags: u32,
}
impl<'a> Label<'a> {
#[must_use]
pub const fn new(text: &'a str) -> Self {
Self {
text,
style: Style::Body,
flags: 0,
}
}
#[must_use]
pub const fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub const fn wrap(mut self) -> Self {
self.flags |= UiWidget::WRAP;
self
}
#[must_use]
pub const fn center(mut self) -> Self {
self.flags |= UiWidget::CENTER;
self
}
}
impl<'a> From<&'a str> for Label<'a> {
fn from(text: &'a str) -> Self {
Self::new(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Button<'a> {
text: &'a str,
enabled: bool,
}
impl<'a> Button<'a> {
#[must_use]
pub const fn new(text: &'a str) -> Self {
Self {
text,
enabled: true,
}
}
#[must_use]
pub const fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
impl<'a> From<&'a str> for Button<'a> {
fn from(text: &'a str) -> Self {
Self::new(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Toggle<'a> {
label: &'a str,
read_only: bool,
}
impl<'a> Toggle<'a> {
#[must_use]
pub const fn new(label: &'a str) -> Self {
Self {
label,
read_only: false,
}
}
#[must_use]
pub const fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
}
impl<'a> From<&'a str> for Toggle<'a> {
fn from(label: &'a str) -> Self {
Self::new(label)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Slider<'a> {
label: &'a str,
range: RangeInclusive<f32>,
steps: u32,
read_only: bool,
}
impl<'a> Slider<'a> {
#[must_use]
pub const fn new(label: &'a str, range: RangeInclusive<f32>) -> Self {
Self {
label,
range,
steps: 0,
read_only: false,
}
}
#[must_use]
pub const fn steps(mut self, steps: u32) -> Self {
self.steps = steps;
self
}
#[must_use]
pub const fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Dropdown<'a> {
label: &'a str,
options: &'a [&'a str],
read_only: bool,
}
impl<'a> Dropdown<'a> {
#[must_use]
pub const fn new(label: &'a str, options: &'a [&'a str]) -> Self {
Self {
label,
options,
read_only: false,
}
}
#[must_use]
pub const fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
}
const fn read_only_flag(read_only: bool) -> u32 {
if read_only { UiWidget::READ_ONLY } else { 0 }
}
#[derive(Debug)]
pub struct Frame<'f> {
host: Host,
token: u64,
screen: Screen,
_life: PhantomData<&'f mut ()>,
_thread: PhantomData<*const ()>,
}
impl Frame<'_> {
#[must_use]
pub fn screen(&self) -> Screen {
self.screen
}
fn draw(&mut self, widget: &mut UiWidget) -> bool {
let status = (self.host.api().ui_widget)(self.host.handle(), self.token, widget);
status.is_ok() && widget.result != 0
}
pub fn window(&mut self, id: &str, title: &str, body: impl FnOnce(&mut Self)) -> bool {
let mut open = UiWidget {
kind: UiWidget::WINDOW,
text: Str::new(id),
detail: Str::new(title),
..UiWidget::EMPTY
};
let status = (self.host.api().ui_widget)(self.host.handle(), self.token, &raw mut open);
if !status.is_ok() {
return false;
}
body(self);
let mut end = UiWidget {
kind: UiWidget::END,
..UiWidget::EMPTY
};
let _closed = (self.host.api().ui_widget)(self.host.handle(), self.token, &raw mut end);
true
}
pub fn label<'t>(&mut self, label: impl Into<Label<'t>>) {
let label = label.into();
let _drawn = self.draw(&mut UiWidget {
kind: UiWidget::LABEL,
flags: label.flags,
style: label.style.to_raw(),
text: Str::new(label.text),
..UiWidget::EMPTY
});
}
pub fn button<'t>(&mut self, button: impl Into<Button<'t>>) -> bool {
let button = button.into();
self.draw(&mut UiWidget {
kind: UiWidget::BUTTON,
flags: read_only_flag(!button.enabled),
text: Str::new(button.text),
..UiWidget::EMPTY
})
}
pub fn toggle<'t>(&mut self, toggle: impl Into<Toggle<'t>>, on: &mut bool) -> bool {
let toggle = toggle.into();
let mut widget = UiWidget {
kind: UiWidget::TOGGLE,
flags: read_only_flag(toggle.read_only),
text: Str::new(toggle.label),
index: u32::from(*on),
..UiWidget::EMPTY
};
let changed = self.draw(&mut widget);
if changed {
*on = widget.index != 0;
}
changed
}
pub fn slider(&mut self, slider: Slider<'_>, value: &mut f32) -> bool {
let (start, end) = slider.range.into_inner();
if !(start.is_finite() && end.is_finite()) {
return false;
}
let (min, max) = (start.min(end), start.max(end));
let mut widget = UiWidget {
kind: UiWidget::SLIDER,
flags: read_only_flag(slider.read_only),
steps: slider.steps,
text: Str::new(slider.label),
value: if value.is_finite() {
value.clamp(min, max)
} else {
min
},
min,
max,
..UiWidget::EMPTY
};
let changed = self.draw(&mut widget) && widget.value.is_finite();
if changed {
*value = widget.value.clamp(min, max);
}
changed
}
pub fn dropdown(&mut self, dropdown: Dropdown<'_>, index: &mut usize) -> bool {
let mut options = [Str::EMPTY; MAX_OPTIONS];
let count = dropdown.options.len().min(MAX_OPTIONS);
for (slot, option) in options.iter_mut().zip(dropdown.options) {
*slot = Str::new(option);
}
let mut widget = UiWidget {
kind: UiWidget::DROPDOWN,
flags: read_only_flag(dropdown.read_only),
text: Str::new(dropdown.label),
options: options.as_ptr(),
option_count: u32::try_from(count).unwrap_or(0),
index: u32::try_from(*index).unwrap_or(u32::MAX),
..UiWidget::EMPTY
};
let changed = self.draw(&mut widget);
let picked = widget.index as usize;
if changed && picked < count {
*index = picked;
return true;
}
false
}
pub fn progress(&mut self, fraction: f32) {
let _drawn = self.draw(&mut UiWidget {
kind: UiWidget::PROGRESS,
value: if fraction.is_finite() {
fraction.clamp(0.0, 1.0)
} else {
0.0
},
min: 0.0,
max: 1.0,
..UiWidget::EMPTY
});
}
pub fn sound(&mut self, sound: Sound) {
let _played = self.draw(&mut UiWidget {
kind: UiWidget::SOUND,
style: sound.to_raw(),
..UiWidget::EMPTY
});
}
}
struct Drawer<F> {
draw: Mutex<F>,
}
unsafe extern "system" fn draw_frame<F>(frame: *const UiFrame, user: *mut c_void)
where
F: FnMut(&mut Frame<'_>),
{
let Some(raw) = (unsafe { frame.as_ref() }) else {
return;
};
let Some(host) = crate::host() else {
return;
};
let drawer = unsafe { &*user.cast::<Drawer<F>>() };
let screen = match raw.screen {
UiFrame::IN_GAME => Screen::InGame,
UiFrame::LOADING => Screen::Loading,
_ => Screen::Menu,
};
crate::host::guard(
"panic in a frame callback",
|| (),
|| {
let Ok(mut draw) = drawer.draw.lock() else {
return;
};
(draw)(&mut Frame {
host,
token: raw.token,
screen,
_life: PhantomData,
_thread: PhantomData,
});
},
);
}
fn client_only(host: Host, what: &str) -> Result<(), Off> {
if host.build().side == crate::abi::Side::SERVER {
return Err(Off::new(format!("{what}: on clients only")));
}
Ok(())
}
impl Load<'_> {
pub fn ui(&mut self) -> Result<Ui, Off> {
let host = self.host();
client_only(host, "the interface")?;
Ok(Ui { host })
}
pub fn on_frame<F>(&mut self, draw: F) -> Result<(), Off>
where
F: FnMut(&mut Frame<'_>) + Send + 'static,
{
let host = self.host();
client_only(host, "frame callbacks")?;
crate::load::hand_over(
Drawer {
draw: Mutex::new(draw),
},
|user| (host.api().ui_frame)(host.handle(), draw_frame::<F>, user),
"frame callback",
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn responses_round_trip_through_their_raw_form() {
for response in [
Response::Retry,
Response::Disable,
Response::Erase,
Response::Reset,
Response::Overwrite,
Response::Cancel,
Response::Ok,
Response::Ignore,
] {
assert_eq!(Response::from_raw(response.bit()), Some(response));
assert_ne!(UiDialog::BUTTONS & 1 << response.bit(), 0);
}
assert_eq!(Response::from_raw(4), None);
assert_eq!(Response::from_raw(UiDialog::CLOSED), None);
}
#[test]
fn a_dialog_keeps_ok_without_buttons_and_takes_the_ones_given() {
let plain = Dialog::new("t", "m");
assert_eq!(plain.to_raw(0).buttons, 1 << UiDialog::OK);
assert_eq!(plain.buttons(&[]).to_raw(0).buttons, 1 << UiDialog::OK);
let asked = plain.buttons(&[Response::Cancel, Response::Ok]);
assert_eq!(asked.to_raw(UiDialog::WAITING).buttons, 0xc0);
assert_eq!(asked.to_raw(UiDialog::WAITING).flags, UiDialog::WAITING);
}
}