#![allow(unused_imports)]
use any_spawner::Executor;
use futuresdr::futures::StreamExt;
use futuresdr::runtime::Pmt;
use futuresdr_types::FlowgraphId;
use gloo_net::websocket::Message;
use gloo_net::websocket::futures::WebSocket;
use leptos::html::Input;
use leptos::html::Span;
use leptos::logging::*;
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos::wasm_bindgen::JsCast;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use web_sys::HtmlInputElement;
use prophecy::FlowgraphCanvas;
use prophecy::FlowgraphHandle;
use prophecy::FlowgraphTable;
use prophecy::ListSelector;
use prophecy::Pmt;
use prophecy::PmtEditor;
use prophecy::PmtInput;
use prophecy::PmtInputList;
use prophecy::RadioSelector;
use prophecy::RuntimeHandle;
use prophecy::Slider;
use prophecy::TimeSink;
use prophecy::TimeSinkMode;
use prophecy::Waterfall;
use prophecy::WaterfallMode;
use prophecy::poll_periodically;
#[derive(Clone, Debug, PartialEq)]
struct MessageInputTarget {
block_id: usize,
block_name: String,
handler: String,
source: &'static str,
}
#[component]
pub fn Flowgraph(fg_handle: FlowgraphHandle) -> impl IntoView {
let fg_desc = {
let fg_handle = fg_handle.clone();
LocalResource::new(move || {
let fg_handle = fg_handle.clone();
async move { fg_handle.describe().await.ok() }
})
};
let (target, set_target) = signal(None::<MessageInputTarget>);
let (submit_error, set_submit_error) = signal(None::<String>);
let (submitting, set_submitting) = signal(false);
let _esc_listener =
window_event_listener(leptos::ev::keydown, move |ev: web_sys::KeyboardEvent| {
if ev.key() == "Escape" && target.get_untracked().is_some() {
set_target(None);
}
});
let on_canvas_message_input_click = Callback::new(move |(block_id, block_name, handler)| {
set_submit_error(None);
set_target(Some(MessageInputTarget {
block_id,
block_name,
handler,
source: "canvas",
}));
});
let on_table_message_input_click = Callback::new(move |(block_id, block_name, handler)| {
set_submit_error(None);
set_target(Some(MessageInputTarget {
block_id,
block_name,
handler,
source: "table",
}));
});
let on_submit_pmt = Callback::new(move |pmt: Pmt| {
let selected = target.get_untracked();
if let Some(selected) = selected {
set_submitting(true);
set_submit_error(None);
let fg = fg_handle.clone();
spawn_local(async move {
let result = fg
.put_message_input(selected.block_id, selected.handler.clone(), pmt)
.await;
set_submitting(false);
match result {
Ok(()) => set_target(None),
Err(e) => set_submit_error(Some(format!("failed to send PMT: {e}"))),
}
});
}
});
view! {
{move || match fg_desc.get() {
Some(wrapped) => {
match wrapped {
Some(data) => {
view! {
<div>
<FlowgraphCanvas
fg=data.clone()
on_message_input_click=on_canvas_message_input_click
/>
<FlowgraphTable
fg=data
on_message_input_click=on_table_message_input_click
/>
</div>
}
.into_any()
}
None => "Flowgraph Handle not set".into_any(),
}
}
None => {
view! { <p>"Connecting..."</p> }
.into_any()
}
}}
{move || target
.get()
.map(|current| {
view! {
<div
class="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4"
on:click=move |_| set_target(None)
>
<div
class="w-full max-w-2xl rounded-lg bg-slate-900 border border-slate-700 p-4"
on:click=move |ev| ev.stop_propagation()
>
<div class="flex items-center justify-between">
<div>
<h3 class="text-white text-lg font-semibold">"Send PMT"</h3>
<p class="text-slate-300 text-sm">
{format!(
"{} -> block {} ({}) / handler '{}'",
current.source,
current.block_id,
current.block_name,
current.handler
)}
</p>
</div>
<button
class="rounded bg-slate-700 hover:bg-slate-600 px-3 py-1 text-sm text-white"
on:click=move |_| set_target(None)
disabled=submitting
>
"Close"
</button>
</div>
<div class="mt-3">
<PmtEditor
on_submit=on_submit_pmt
disabled=submitting()
select_class="w-full rounded bg-slate-800 text-white px-2 py-2"
input_class="w-full h-32 rounded bg-slate-800 text-white px-2 py-2 font-mono"
error_class="text-red-400 text-sm"
button_class="rounded bg-blue-600 hover:bg-blue-500 text-white px-3 py-2"
button_text=if submitting() {
"Sending...".to_string()
} else {
"Send".to_string()
}
/>
</div>
<div class="mt-2 text-red-400 text-sm">
{move || submit_error.get().unwrap_or_default()}
</div>
</div>
</div>
}
})}
}
}
#[component]
pub fn RuntimeControl() -> impl IntoView {
let default_url = window().location().origin().unwrap();
let (rt_handle, set_rt_handle) = signal_local(RuntimeHandle::from_url(default_url.clone()));
let (fg_handle, set_fg_handle) = signal(None::<FlowgraphHandle>);
let fg_ids: RwSignal<Vec<FlowgraphId>> = RwSignal::new(vec![]);
Effect::new(move |_| {
let rt = rt_handle.get();
spawn_local(async move {
let fgs = rt.get_flowgraphs().await.unwrap_or_default();
fg_ids.set(fgs.clone());
if let Some(&first) = fgs.first()
&& let Ok(fg) = rt.get_flowgraph(first).await
{
set_fg_handle(Some(fg));
}
});
});
let on_fg_change = move |ev: web_sys::Event| {
let val = event_target_value(&ev);
if let Ok(idx) = val.parse::<usize>() {
let ids = fg_ids.get_untracked();
if let Some(&id) = ids.get(idx) {
spawn_local(async move {
if let Ok(fg) = rt_handle.get_untracked().get_flowgraph(id).await {
set_fg_handle(Some(fg));
}
});
}
}
};
let url_ref = NodeRef::<Input>::new();
let on_url_keydown = move |ev: web_sys::KeyboardEvent| {
if ev.key() == "Enter"
&& let Some(input) = url_ref.get()
{
set_rt_handle.set(RuntimeHandle::from_url(input.value()));
}
};
let connected = move || fg_handle.get().is_some();
view! {
<header class="bg-slate-800 border-b border-slate-700 shadow-lg">
<div class="flex items-center gap-4 px-4 py-3">
<div class="flex items-center gap-2.5 mr-auto select-none">
<svg
class="w-7 h-7 text-cyan-400 shrink-0"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 14 Q4 6, 7 14 Q10 22, 13 14 Q16 6, 19 14 Q22 22, 25 14 Q27 9, 28 14"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</svg>
<div class="leading-tight">
<span class="text-white font-semibold tracking-tight text-base">"FutureSDR"</span>
<span class="text-cyan-400 font-light text-base ml-1.5">"Prophecy"</span>
</div>
</div>
<div class="flex items-center gap-1.5 text-xs font-medium">
<span class={move || if connected() {
"w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_6px_1px_rgba(52,211,153,0.6)]"
} else {
"w-2 h-2 rounded-full bg-slate-500"
}} />
<span class={move || if connected() {
"text-emerald-400"
} else {
"text-slate-400"
}}>
{move || if connected() { "Connected" } else { "Disconnected" }}
</span>
</div>
<div class="w-px h-5 bg-slate-600" />
<div class="flex items-center gap-2">
<label class="text-slate-400 text-xs uppercase tracking-wider whitespace-nowrap">
"Runtime"
</label>
<input
node_ref=url_ref
type="text"
value=default_url
class="bg-slate-700 text-white text-sm rounded-md px-2.5 py-1.5 w-56 focus:outline-none focus:ring-1 focus:ring-cyan-500 border border-slate-600 placeholder-slate-500"
placeholder="http://localhost:1337"
on:keydown=on_url_keydown
/>
</div>
<div class="flex items-center gap-2">
<label class="text-slate-400 text-xs uppercase tracking-wider whitespace-nowrap">
"Flowgraph"
</label>
<select
class="bg-slate-700 text-white text-sm rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-cyan-500 border border-slate-600 disabled:opacity-40"
on:change=on_fg_change
disabled=move || fg_ids.get().is_empty()
>
{move || {
let ids = fg_ids.get();
if ids.is_empty() {
view! {
<option value="">"No flowgraphs"</option>
}.into_any()
} else {
ids.into_iter()
.enumerate()
.map(|(i, id)| {
view! {
<option value={i.to_string()}>
{format!("Flowgraph #{}", id.0)}
</option>
}
})
.collect::<Vec<_>>()
.into_any()
}
}}
</select>
</div>
</div>
</header>
{move || match fg_handle.get() {
Some(h) => view! { <Flowgraph fg_handle=h /> }.into_any(),
None => "".into_any(),
}}
}
}
#[component]
pub fn Prophecy() -> impl IntoView {
view! {
<div class="min-h-screen bg-slate-900 flex flex-col">
<RuntimeControl />
</div>
}
}
pub fn main() {
console_error_panic_hook::set_once();
Executor::init_wasm_bindgen().unwrap();
mount_to_body(|| view! { <Prophecy /> })
}