use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use serde_json::Value;
use crate::event::Event;
pub use plushie_core::ops::*;
pub type AsyncTaskFn =
Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = Result<Value, Value>> + Send>> + Send>;
pub type StreamTaskFn = Box<
dyn FnOnce(StreamEmitter) -> Pin<Box<dyn Future<Output = Result<Value, Value>> + Send>> + Send,
>;
#[derive(Clone)]
pub struct StreamEmitter {
tag: String,
inner: Arc<Mutex<StreamEmitterInner>>,
}
enum StreamEmitterInner {
Buffer(Vec<Value>),
Sink(Box<dyn FnMut(String, Value) + Send>),
}
impl StreamEmitter {
pub fn buffered(tag: &str) -> Self {
Self {
tag: tag.to_string(),
inner: Arc::new(Mutex::new(StreamEmitterInner::Buffer(Vec::new()))),
}
}
pub fn attach_sink(&self, mut sink: Box<dyn FnMut(String, Value) + Send>) {
let buffered: Vec<Value> = {
let mut guard = self.inner.lock();
match &mut *guard {
StreamEmitterInner::Buffer(values) => std::mem::take(values),
StreamEmitterInner::Sink(_) => Vec::new(),
}
};
for v in buffered {
sink(self.tag.clone(), v);
}
let mut guard = self.inner.lock();
*guard = StreamEmitterInner::Sink(sink);
}
pub fn drain_buffer(&self) -> Vec<Value> {
let mut guard = self.inner.lock();
match &mut *guard {
StreamEmitterInner::Buffer(v) => std::mem::take(v),
StreamEmitterInner::Sink(_) => Vec::new(),
}
}
pub fn tag(&self) -> &str {
&self.tag
}
pub fn emit(&self, value: impl Into<Value>) {
let value = value.into();
let sink = {
let mut guard = self.inner.lock();
match &mut *guard {
StreamEmitterInner::Buffer(buf) => {
buf.push(value);
return;
}
StreamEmitterInner::Sink(_) => {
let placeholder: Box<dyn FnMut(String, Value) + Send> = Box::new(|_, _| {});
match std::mem::replace(&mut *guard, StreamEmitterInner::Sink(placeholder)) {
StreamEmitterInner::Sink(s) => s,
StreamEmitterInner::Buffer(_) => unreachable!(),
}
}
}
};
struct Restore<'a> {
inner: &'a Mutex<StreamEmitterInner>,
sink: Option<Box<dyn FnMut(String, Value) + Send>>,
}
impl Drop for Restore<'_> {
fn drop(&mut self) {
if let Some(sink) = self.sink.take() {
let mut guard = self.inner.lock();
*guard = StreamEmitterInner::Sink(sink);
}
}
}
let mut restore = Restore {
inner: &self.inner,
sink: Some(sink),
};
if let Some(sink) = restore.sink.as_mut() {
sink(self.tag.clone(), value);
}
}
pub fn emit_event(&self, event: impl plushie_core::types::WidgetEventEncode) {
let (_family, value) = event.to_wire();
self.emit(Value::from(value));
}
}
impl std::fmt::Debug for StreamEmitter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamEmitter")
.field("tag", &self.tag)
.finish()
}
}
#[non_exhaustive]
pub enum Command {
None,
Batch(Vec<Command>),
Exit,
Async {
tag: String,
task: AsyncTaskFn,
},
Stream {
tag: String,
task: StreamTaskFn,
},
Cancel {
tag: String,
},
SendAfter {
delay: Duration,
event: Box<Event>,
},
Renderer(RendererOp),
}
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Batch(cmds) => f.debug_tuple("Batch").field(cmds).finish(),
Self::Exit => write!(f, "Exit"),
Self::Async { tag, .. } => f.debug_struct("Async").field("tag", tag).finish(),
Self::Stream { tag, .. } => f.debug_struct("Stream").field("tag", tag).finish(),
Self::Cancel { tag } => f.debug_struct("Cancel").field("tag", tag).finish(),
Self::SendAfter { delay, .. } => {
f.debug_struct("SendAfter").field("delay", delay).finish()
}
Self::Renderer(op) => f.debug_tuple("Renderer").field(op).finish(),
}
}
}
fn assert_pixel_buffer_size(handle: &str, width: u32, height: u32, actual: usize) {
let expected = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(4))
.unwrap_or(usize::MAX);
assert!(
actual == expected,
"create_image_rgba / update_image_rgba: pixel buffer for {handle:?} has {actual} bytes, \
expected {expected} (width={width} height={height} channels=4)",
);
}
impl Command {
pub fn none() -> Self {
Self::None
}
pub fn batch(cmds: impl IntoIterator<Item = Command>) -> Self {
Self::Batch(cmds.into_iter().collect())
}
pub fn exit() -> Self {
Self::Exit
}
pub fn send_after(delay: Duration, event: Event) -> Self {
Self::SendAfter {
delay,
event: Box::new(event),
}
}
pub fn dispatch(event: Event) -> Self {
Self::send_after(Duration::ZERO, event)
}
pub fn task<F, Fut>(tag: &str, f: F) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<Value, Value>> + Send + 'static,
{
Self::Async {
tag: tag.to_string(),
task: Box::new(move || Box::pin(f())),
}
}
pub fn cancel(tag: &str) -> Self {
Self::Cancel {
tag: tag.to_string(),
}
}
pub fn stream<F, Fut>(tag: &str, f: F) -> Self
where
F: FnOnce(StreamEmitter) -> Fut + Send + 'static,
Fut: Future<Output = Result<Value, Value>> + Send + 'static,
{
Self::Stream {
tag: tag.to_string(),
task: Box::new(move |emitter| Box::pin(f(emitter))),
}
}
pub fn focus(id: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: id.to_string(),
family: "focus".to_string(),
value: Value::Null,
})
}
pub fn focus_next() -> Self {
Self::Renderer(RendererOp::FocusNext)
}
pub fn focus_previous() -> Self {
Self::Renderer(RendererOp::FocusPrevious)
}
pub fn focus_next_within(scope: &str) -> Self {
Self::Renderer(RendererOp::FocusNextWithin {
scope: scope.to_string(),
})
}
pub fn focus_previous_within(scope: &str) -> Self {
Self::Renderer(RendererOp::FocusPreviousWithin {
scope: scope.to_string(),
})
}
pub fn select_all(target: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "select_all".to_string(),
value: Value::Null,
})
}
pub fn move_cursor_to_front(target: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "move_cursor_to_front".to_string(),
value: Value::Null,
})
}
pub fn move_cursor_to_end(target: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "move_cursor_to_end".to_string(),
value: Value::Null,
})
}
pub fn move_cursor_to(target: &str, position: usize) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "move_cursor_to".to_string(),
value: serde_json::json!({"position": position}),
})
}
pub fn select_range(target: &str, start: usize, end: usize) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "select_range".to_string(),
value: serde_json::json!({"start": start, "end": end}),
})
}
pub fn scroll_to(target: &str, x: f32, y: f32) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "scroll_to".to_string(),
value: serde_json::json!({"x": x, "y": y}),
})
}
pub fn scroll_by(target: &str, x: f32, y: f32) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "scroll_by".to_string(),
value: serde_json::json!({"x": x, "y": y}),
})
}
pub fn snap_to(target: &str, x: f32, y: f32) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "snap_to".to_string(),
value: serde_json::json!({"x": x, "y": y}),
})
}
pub fn snap_to_end(target: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "snap_to_end".to_string(),
value: Value::Null,
})
}
pub fn close_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Close(id.to_string())))
}
pub fn resize_window(id: &str, width: f32, height: f32) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Resize {
window_id: id.to_string(),
width,
height,
}))
}
pub fn move_window(id: &str, x: f32, y: f32) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Move {
window_id: id.to_string(),
x,
y,
}))
}
pub fn maximize_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Maximize {
window_id: id.to_string(),
maximized: true,
}))
}
pub fn unmaximize_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Maximize {
window_id: id.to_string(),
maximized: false,
}))
}
pub fn minimize_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Minimize {
window_id: id.to_string(),
minimized: true,
}))
}
pub fn unminimize_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Minimize {
window_id: id.to_string(),
minimized: false,
}))
}
pub fn set_window_mode(id: &str, mode: WindowMode) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetMode {
window_id: id.to_string(),
mode,
}))
}
pub fn toggle_maximize(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::ToggleMaximize(id.to_string())))
}
pub fn toggle_decorations(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::ToggleDecorations(
id.to_string(),
)))
}
pub fn focus_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::FocusWindow(id.to_string())))
}
pub fn set_window_level(id: &str, level: WindowLevel) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetLevel {
window_id: id.to_string(),
level,
}))
}
pub fn drag_window(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::DragWindow(id.to_string())))
}
pub fn drag_resize_window(id: &str, direction: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::DragResize {
window_id: id.to_string(),
direction: direction.to_string(),
}))
}
pub fn request_attention(id: &str, urgency: Option<NotificationUrgency>) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::RequestAttention {
window_id: id.to_string(),
urgency,
}))
}
pub fn screenshot(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::Screenshot {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn set_resizable(id: &str, resizable: bool) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetResizable {
window_id: id.to_string(),
resizable,
}))
}
pub fn set_min_size(id: &str, width: f32, height: f32) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetMinSize {
window_id: id.to_string(),
width,
height,
}))
}
pub fn set_max_size(id: &str, width: f32, height: f32) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetMaxSize {
window_id: id.to_string(),
width,
height,
}))
}
pub fn enable_mouse_passthrough(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::EnableMousePassthrough(
id.to_string(),
)))
}
pub fn disable_mouse_passthrough(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::DisableMousePassthrough(
id.to_string(),
)))
}
pub fn show_system_menu(id: &str) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::ShowSystemMenu(id.to_string())))
}
pub fn set_icon(id: &str, rgba_data: Vec<u8>, width: u32, height: u32) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetIcon {
window_id: id.to_string(),
data: rgba_data,
width,
height,
}))
}
pub fn set_resize_increments(id: &str, width: f32, height: f32) -> Self {
Self::Renderer(RendererOp::Window(WindowOp::SetResizeIncrements {
window_id: id.to_string(),
width,
height,
}))
}
pub fn window_size(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::GetSize {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn window_position(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::GetPosition {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn is_maximized(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::IsMaximized {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn is_minimized(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::IsMinimized {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn window_mode(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::GetMode {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn scale_factor(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::GetScaleFactor {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn monitor_size(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::MonitorSize {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn raw_id(window_id: &str, tag: &str) -> Self {
Self::Renderer(RendererOp::WindowQuery(WindowQuery::RawId {
window_id: window_id.to_string(),
tag: tag.to_string(),
}))
}
pub fn allow_automatic_tabbing(enabled: bool) -> Self {
Self::Renderer(RendererOp::SystemOp(SystemOp::AllowAutomaticTabbing(
enabled,
)))
}
pub fn system_theme(tag: &str) -> Self {
Self::Renderer(RendererOp::SystemQuery(SystemQuery::GetTheme {
tag: tag.to_string(),
}))
}
pub fn system_info(tag: &str) -> Self {
Self::Renderer(RendererOp::SystemQuery(SystemQuery::GetInfo {
tag: tag.to_string(),
}))
}
pub fn create_image(handle: &str, data: Vec<u8>) -> Self {
Self::Renderer(RendererOp::Image(ImageOp::Create {
handle: handle.to_string(),
data,
}))
}
pub fn create_image_rgba(handle: &str, width: u32, height: u32, pixels: Vec<u8>) -> Self {
assert_pixel_buffer_size(handle, width, height, pixels.len());
Self::Renderer(RendererOp::Image(ImageOp::CreateRaw {
handle: handle.to_string(),
width,
height,
pixels,
}))
}
pub fn update_image(handle: &str, data: Vec<u8>) -> Self {
Self::Renderer(RendererOp::Image(ImageOp::Update {
handle: handle.to_string(),
data,
}))
}
pub fn update_image_rgba(handle: &str, width: u32, height: u32, pixels: Vec<u8>) -> Self {
assert_pixel_buffer_size(handle, width, height, pixels.len());
Self::Renderer(RendererOp::Image(ImageOp::UpdateRaw {
handle: handle.to_string(),
width,
height,
pixels,
}))
}
pub fn delete_image(handle: &str) -> Self {
Self::Renderer(RendererOp::Image(ImageOp::Delete(handle.to_string())))
}
pub fn list_images(tag: &str) -> Self {
Self::Renderer(RendererOp::Image(ImageOp::List {
tag: tag.to_string(),
}))
}
pub fn clear_images() -> Self {
Self::Renderer(RendererOp::Image(ImageOp::Clear))
}
pub fn pane_split(target: &str, pane: &str, axis: &str, new_pane_id: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "pane_split".to_string(),
value: serde_json::json!({
"pane": pane,
"axis": axis,
"new_pane_id": new_pane_id,
}),
})
}
pub fn pane_close(target: &str, pane: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "pane_close".to_string(),
value: serde_json::json!({"pane": pane}),
})
}
pub fn pane_swap(target: &str, a: &str, b: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "pane_swap".to_string(),
value: serde_json::json!({"a": a, "b": b}),
})
}
pub fn pane_maximize(target: &str, pane: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "pane_maximize".to_string(),
value: serde_json::json!({"pane": pane}),
})
}
pub fn pane_restore(target: &str) -> Self {
Self::Renderer(RendererOp::Command {
id: target.to_string(),
family: "pane_restore".to_string(),
value: Value::Null,
})
}
pub fn announce(text: &str, politeness: plushie_core::types::Live) -> Self {
Self::Renderer(RendererOp::Announce {
text: text.to_string(),
politeness,
})
}
pub fn announce_text(text: &str) -> Self {
Self::announce(text, plushie_core::types::Live::Polite)
}
pub fn load_font(family: impl Into<String>, bytes: Vec<u8>) -> Self {
Self::Renderer(RendererOp::LoadFont {
family: family.into(),
bytes,
})
}
pub fn tree_hash(tag: &str) -> Self {
Self::Renderer(RendererOp::TreeHash {
tag: tag.to_string(),
})
}
pub fn find_focused(tag: &str) -> Self {
Self::Renderer(RendererOp::FindFocused {
tag: tag.to_string(),
})
}
pub fn advance_frame(timestamp: u64) -> Self {
Self::Renderer(RendererOp::AdvanceFrame { timestamp })
}
pub fn file_open(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::FileOpen(Default::default()),
})
}
pub fn file_open_with(tag: &str, opts: FileDialogOpts) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::FileOpen(opts),
})
}
pub fn file_open_multiple(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::FileOpenMultiple(Default::default()),
})
}
pub fn file_open_multiple_with(tag: &str, opts: FileDialogOpts) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::FileOpenMultiple(opts),
})
}
pub fn file_save(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::FileSave(Default::default()),
})
}
pub fn file_save_with(tag: &str, opts: FileDialogOpts) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::FileSave(opts),
})
}
pub fn directory_select(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::DirectorySelect(Default::default()),
})
}
pub fn directory_select_with(tag: &str, opts: FileDialogOpts) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::DirectorySelect(opts),
})
}
pub fn directory_select_multiple(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::DirectorySelectMultiple(Default::default()),
})
}
pub fn directory_select_multiple_with(tag: &str, opts: FileDialogOpts) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::DirectorySelectMultiple(opts),
})
}
pub fn clipboard_read(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardRead,
})
}
pub fn clipboard_write(tag: &str, text: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardWrite(text.to_string()),
})
}
pub fn clipboard_read_html(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardReadHtml,
})
}
pub fn clipboard_write_html(tag: &str, html: &str, alt_text: Option<&str>) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardWriteHtml {
html: html.to_string(),
alt_text: alt_text.map(|s| s.to_string()),
},
})
}
pub fn clipboard_clear(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardClear,
})
}
pub fn clipboard_read_primary(tag: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardReadPrimary,
})
}
pub fn clipboard_write_primary(tag: &str, text: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::ClipboardWritePrimary(text.to_string()),
})
}
pub fn notification(tag: &str, title: &str, body: &str) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::Notification {
title: title.to_string(),
body: body.to_string(),
opts: Default::default(),
},
})
}
pub fn notification_with(tag: &str, title: &str, body: &str, opts: NotificationOpts) -> Self {
Self::Renderer(RendererOp::Effect {
tag: tag.to_string(),
timeout: None,
request: EffectRequest::Notification {
title: title.to_string(),
body: body.to_string(),
opts,
},
})
}
pub fn widget<C: plushie_core::WidgetCommandEncode>(id: &str, cmd: C) -> Self {
let wc = plushie_core::ops::WidgetCommand::new(id, cmd);
Self::Renderer(RendererOp::Command {
id: wc.id,
family: wc.family,
value: wc.value,
})
}
pub fn send(id: &str, family: &str, value: Value) -> Self {
let wc = plushie_core::ops::WidgetCommand::raw(id, family, value);
Self::Renderer(RendererOp::Command {
id: wc.id,
family: wc.family,
value: wc.value,
})
}
pub fn widget_batch(cmds: impl IntoIterator<Item = plushie_core::ops::WidgetCommand>) -> Self {
Self::Renderer(RendererOp::Commands(cmds.into_iter().collect()))
}
}