more refactor

This commit is contained in:
2025-11-18 02:38:23 -05:00
parent db15f43610
commit 01cd02c0f9
11 changed files with 39 additions and 24 deletions

67
src/bin/client/app.rs Normal file
View File

@@ -0,0 +1,67 @@
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::{Window, WindowId},
};
use crate::ClientEvent;
use super::Client;
#[derive(Clone)]
pub struct AppHandle {
pub proxy: EventLoopProxy<ClientEvent>,
pub window: Arc<Window>,
}
pub struct App {
client: Option<Client>,
proxy: EventLoopProxy<ClientEvent>,
}
impl App {
pub fn run() {
let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy();
event_loop
.run_app(&mut App {
client: Default::default(),
proxy,
})
.unwrap();
}
}
impl ApplicationHandler<ClientEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.client.is_none() {
let client = Client::new(event_loop, self.proxy.clone());
self.client = Some(client);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let client = self.client.as_mut().unwrap();
client.window_event(event, event_loop);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: ClientEvent) {
let client = self.client.as_mut().unwrap();
client.event(event, event_loop);
}
fn exiting(&mut self, _: &ActiveEventLoop) {
let client = self.client.as_mut().unwrap();
client.exit();
}
}
impl AppHandle {
pub fn send(&self, event: ClientEvent) {
self.proxy.send_event(event).unwrap_or_else(|_| panic!());
self.window.request_redraw();
}
}

BIN
src/bin/client/assets/sungals.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

79
src/bin/client/input.rs Normal file
View File

@@ -0,0 +1,79 @@
use iris::{
core::{CursorState, Modifiers},
layout::Vec2,
};
use winit::{
event::{MouseButton, MouseScrollDelta, WindowEvent},
keyboard::{Key, NamedKey},
};
use super::Client;
#[derive(Default)]
pub struct Input {
cursor: CursorState,
pub modifiers: Modifiers,
}
impl Input {
pub fn event(&mut self, event: &WindowEvent) -> bool {
match event {
WindowEvent::CursorMoved { position, .. } => {
self.cursor.pos = Vec2::new(position.x as f32, position.y as f32);
self.cursor.exists = true;
}
WindowEvent::MouseInput { state, button, .. } => {
let buttons = &mut self.cursor.buttons;
let pressed = state.is_pressed();
match button {
MouseButton::Left => buttons.left.update(pressed),
MouseButton::Right => buttons.right.update(pressed),
MouseButton::Middle => buttons.middle.update(pressed),
_ => (),
}
}
WindowEvent::MouseWheel { delta, .. } => {
let delta = match *delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
self.cursor.scroll_delta = delta;
}
WindowEvent::CursorLeft { .. } => {
self.cursor.exists = false;
self.modifiers.clear();
}
WindowEvent::KeyboardInput { event, .. } => {
if let Key::Named(named) = event.logical_key {
let pressed = event.state.is_pressed();
match named {
NamedKey::Control => {
self.modifiers.control = pressed;
}
NamedKey::Shift => {
self.modifiers.shift = pressed;
}
_ => (),
}
}
}
_ => return false,
}
true
}
pub fn end_frame(&mut self) {
self.cursor.end_frame();
}
}
impl Client {
pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size();
(size.width, size.height).into()
}
pub fn cursor_state(&self) -> &CursorState {
&self.input.cursor
}
}

203
src/bin/client/main.rs Normal file
View File

@@ -0,0 +1,203 @@
#![windows_subsystem = "windows"]
use crate::{
app::App,
net::NetSender,
rsc::{CLIENT_DATA, ClientData},
ui::*,
};
pub use app::AppHandle;
use arboard::Clipboard;
use input::Input;
use iris::prelude::*;
use openworm::{
net::{ClientMsg, ServerMsg, install_crypto_provider},
rsc::DataDir,
};
use render::Renderer;
use std::sync::Arc;
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::Window,
};
mod app;
mod input;
mod net;
mod render;
mod rsc;
mod ui;
fn main() {
install_crypto_provider();
App::run();
}
pub enum ClientEvent {
Connect { send: NetSender, username: String },
ServerMsg(ServerMsg),
Err(String),
}
pub struct Client {
renderer: Renderer,
input: Input,
ui: Ui,
focus: Option<WidgetId<TextEdit>>,
channel: Option<WidgetId<Span>>,
username: String,
clipboard: Clipboard,
dir: DataDir,
data: ClientData,
handle: AppHandle,
error: Option<WidgetId<WidgetPtr>>,
ime: usize,
}
impl Client {
pub fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<ClientEvent>) -> Self {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("OPENWORM"))
.unwrap(),
);
let renderer = Renderer::new(window.clone());
let dir = DataDir::default();
let handle = AppHandle { proxy, window };
let mut s = Self {
handle,
renderer,
input: Input::default(),
ui: Ui::new(),
data: dir.load(CLIENT_DATA),
dir,
channel: None,
focus: None,
username: "<unknown>".to_string(),
clipboard: Clipboard::new().unwrap(),
error: None,
ime: 0,
};
ui::init(&mut s);
s
}
pub fn event(&mut self, event: ClientEvent, _: &ActiveEventLoop) {
match event {
ClientEvent::Connect { send, username } => {
self.username = username;
send.send(ClientMsg::RequestMsgs);
main_view(self, send).set_root(&mut self.ui);
}
ClientEvent::ServerMsg(msg) => match msg {
ServerMsg::SendMsg(msg) => {
if let Some(msg_area) = &self.channel {
let msg = msg_widget(msg).add(&mut self.ui);
self.ui[msg_area].children.push(msg.any());
}
}
ServerMsg::LoadMsgs(msgs) => {
if let Some(msg_area) = &self.channel {
for msg in msgs {
let msg = msg_widget(msg).add(&mut self.ui);
self.ui[msg_area].children.push(msg.any());
}
}
}
},
ClientEvent::Err(msg) => {
if let Some(err) = &self.error {
self.ui[err].inner = Some(ui::error(&mut self.ui, &msg));
}
}
}
}
pub fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let input_changed = self.input.event(&event);
let cursor_state = self.cursor_state().clone();
if let Some(focus) = &self.focus
&& cursor_state.buttons.left.is_start()
{
self.ui.text(focus).deselect();
self.focus = None;
}
if input_changed {
let window_size = self.window_size();
self.run_sensors(&cursor_state, window_size);
self.ui.run_sensors(&cursor_state, window_size);
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
self.ui.update();
self.renderer.update(&mut self.ui);
self.renderer.draw();
}
WindowEvent::Resized(size) => {
self.ui.resize((size.width, size.height));
self.renderer.resize(&size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = &self.focus
&& event.state.is_pressed()
{
let sel = &sel.clone();
let mut text = self.ui.text(sel);
match text.apply_event(&event, &self.input.modifiers) {
TextInputResult::Unfocus => {
self.focus = None;
self.handle.window.set_ime_allowed(false);
}
TextInputResult::Submit => {
self.run_event(sel, Submit, ());
}
TextInputResult::Paste => {
if let Ok(t) = self.clipboard.get_text() {
text.insert(&t);
}
self.run_event(sel, Edited, ());
}
TextInputResult::Used => {
self.run_event(sel, Edited, ());
}
TextInputResult::Unused => (),
}
}
}
WindowEvent::Ime(ime) => {
if let Some(sel) = &self.focus {
let mut text = self.ui.text(sel);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real
text.replace(self.ime, &content);
self.ime = content.chars().count();
}
Ime::Commit(content) => {
text.insert(&content);
}
}
}
}
_ => (),
}
if self.ui.needs_redraw() {
self.renderer.window().request_redraw();
}
self.input.end_frame();
}
pub fn exit(&mut self) {
self.dir.save(CLIENT_DATA, &self.data);
}
}
impl UiCtx for Client {
fn ui(&mut self) -> &mut Ui {
&mut self.ui
}
}

140
src/bin/client/net.rs Normal file
View File

@@ -0,0 +1,140 @@
use crate::{AppHandle, ClientEvent};
use openworm::net::{
ClientMsg, RecvHandler, SERVER_NAME, ServerMsg, SkipServerVerification, recv_uni, send_uni,
};
use quinn::{
ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig,
crypto::rustls::QuicClientConfig,
};
use std::{
net::{Ipv6Addr, SocketAddr, SocketAddrV6, ToSocketAddrs},
sync::Arc,
time::Duration,
};
use tokio::sync::mpsc::UnboundedSender;
pub const CLIENT_SOCKET: SocketAddr =
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0));
pub struct ConnectInfo {
pub ip: String,
pub username: String,
}
pub fn connect(handle: AppHandle, info: ConnectInfo) {
std::thread::spawn(move || {
if let Err(msg) = connect_the(handle.clone(), info) {
handle.send(ClientEvent::Err(msg));
}
});
}
type NetResult<T> = Result<T, String>;
type MsgPayload = ClientMsg;
pub struct NetSender {
send: UnboundedSender<MsgPayload>,
}
impl NetSender {
pub fn send(&self, msg: ClientMsg) {
let _ = self.send.send(msg);
}
}
// async fn connection_cert(addr: SocketAddr) -> NetResult<Connection> {
// let dirs = directories_next::ProjectDirs::from("", "", "openworm").unwrap();
// let mut roots = quinn::rustls::RootCertStore::empty();
// match fs::read(dirs.data_local_dir().join("cert.der")) {
// Ok(cert) => {
// roots.add(CertificateDer::from(cert))?;
// }
// Err(ref e) if e.kind() == ErrorKind::NotFound => {
// eprintln!("local server certificate not found");
// }
// Err(e) => {
// eprintln!("failed to open local server certificate: {}", e);
// }
// }
// let client_crypto = quinn::rustls::ClientConfig::builder()
// .with_root_certificates(roots)
// .with_no_client_auth();
// let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
// let mut endpoint = quinn::Endpoint::client(SocketAddr::from_str("[::]:0").unwrap())?;
// endpoint.set_default_client_config(client_config);
// endpoint
// .connect(addr, SERVER_NAME)?
// .await
// .map_err(|e| format!("failed to connect: {}", e))
// }
async fn connection_no_cert(addr: SocketAddr) -> NetResult<Connection> {
let mut endpoint = Endpoint::client(CLIENT_SOCKET).map_err(|e| e.to_string())?;
let quic = QuicClientConfig::try_from(
quinn::rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_no_client_auth(),
)
.map_err(|e| e.to_string())?;
let mut config = ClientConfig::new(Arc::new(quic));
let mut transport = TransportConfig::default();
transport.keep_alive_interval(Some(Duration::from_secs(5)));
transport.max_idle_timeout(Some(
IdleTimeout::try_from(Duration::from_secs(10)).unwrap(),
));
config.transport_config(transport.into());
endpoint.set_default_client_config(config);
// connect to server
endpoint
.connect(addr, SERVER_NAME)
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())
}
#[tokio::main]
async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
let (send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel::<MsgPayload>();
let addr = info
.ip
.to_socket_addrs()
.map_err(|e| e.to_string())?
.next()
.ok_or("no addresses found".to_string())?;
let conn = connection_no_cert(addr).await?;
let conn_ = conn.clone();
handle.send(ClientEvent::Connect {
username: info.username,
send: NetSender { send },
});
let recv = ServerRecv { handle };
tokio::spawn(recv_uni(conn_, recv.into()));
while let Some(msg) = ui_recv.recv().await {
if send_uni(&conn, msg).await.is_err() {
println!("disconnected from server");
break;
}
}
Ok(())
}
struct ServerRecv {
handle: AppHandle,
}
impl RecvHandler<ServerMsg> for ServerRecv {
async fn msg(&self, msg: ServerMsg) {
self.handle.send(ClientEvent::ServerMsg(msg));
}
}

View File

@@ -0,0 +1,151 @@
use iris::{
layout::Ui,
render::{UiLimits, UiRenderer},
};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::{util::StagingBelt, *};
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: Color = Color::BLACK;
pub struct Renderer {
window: Arc<Window>,
surface: Surface<'static>,
device: Device,
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
pub ui: UiRenderer,
}
impl Renderer {
pub fn update(&mut self, updates: &mut Ui) {
self.ui.update(&self.device, &self.queue, updates);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
{
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(CLEAR_COLOR),
store: StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
self.ui.draw(render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.finish();
output.present();
self.staging_belt.recall();
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui.resize(size, &self.queue);
}
fn create_encoder(device: &Device) -> CommandEncoder {
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
})
}
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
});
let surface = instance
.create_surface(window.clone())
.expect("Could not create window surface!");
let adapter = instance
.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
..Default::default()
},
..Default::default()
})
.block_on()
.expect("Could not get device!");
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
};
surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UiRenderer::new(&device, &queue, &config, ui_limits);
Self {
surface,
device,
queue,
config,
encoder,
staging_belt,
ui: shape_pipeline,
window,
}
}
pub fn window(&self) -> &Window {
self.window.as_ref()
}
}

16
src/bin/client/rsc.rs Normal file
View File

@@ -0,0 +1,16 @@
pub const CLIENT_DATA: &str = "client_data";
#[derive(bincode::Encode, bincode::Decode)]
pub struct ClientData {
pub ip: String,
pub username: String,
}
impl Default for ClientData {
fn default() -> Self {
Self {
ip: "localhost:39420".to_string(),
username: "your [NOVEMBER]".to_string(),
}
}
}

187
src/bin/client/ui.rs Normal file
View File

@@ -0,0 +1,187 @@
use iris::prelude::*;
use len_fns::*;
use openworm::net::{ClientMsg, Msg};
use winit::dpi::{LogicalPosition, LogicalSize};
use crate::{
Client,
net::{ConnectInfo, NetSender, connect},
};
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Submit;
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited;
impl DefaultEvent for Submit {
type Data = ();
}
impl DefaultEvent for Edited {
type Data = ();
}
pub fn init(client: &mut Client) {
login_screen(client).set_root(&mut client.ui);
}
pub fn main_view(client: &mut Client, network: NetSender) -> WidgetId {
let msg_panel = msg_panel(client, network);
let side_bar = rect(Color::BLACK.brighter(0.05)).width(80);
(side_bar, msg_panel)
.span(Dir::RIGHT)
.add(&mut client.ui)
.any()
}
pub fn error(ui: &mut Ui, msg: &str) -> WidgetId {
text(msg)
.size(20)
.color(Color::RED.brighter(0.3))
.pad(10)
.add(ui)
.any()
}
fn login_screen(client: &mut Client) -> WidgetId {
let Client {
ui, handle, data, ..
} = client;
let mut field = |name| text(name).editable().size(20).add(ui);
let ip = field(&data.ip);
let username = field(&data.username);
// let password = field("password");
let fbx = |field: WidgetId<TextEdit>| {
field
.clone()
.pad(10)
.background(rect(Color::BLACK.brighter(0.1)).radius(15))
.on(CursorSense::click(), focus_other(field))
};
// I LAV NOT HAVING ERGONOMIC CLONES
let handle = handle.clone();
let ip_ = ip.clone();
let username_ = username.clone();
let color = Color::GREEN;
let submit = rect(color)
.radius(15)
.id_on(CursorSense::click(), move |id, client: &mut Client, _| {
client.ui[id].color = color.darker(0.3);
let ip = client.ui[&ip_].content();
let username = client.ui[&username_].content();
connect(handle.clone(), ConnectInfo { ip, username });
})
.height(40);
let modal = (
text("login").size(30),
fbx(ip
.id_on(Edited, |id, client: &mut Client, _| {
client.data.ip = client.ui[id].content();
})
.add(ui)),
fbx(username
.id_on(Edited, |id, client: &mut Client, _| {
client.data.username = client.ui[id].content();
})
.add(ui)),
// fbx(password),
submit,
)
.span(Dir::DOWN)
.gap(10)
.pad(15)
.background(rect(Color::BLACK.brighter(0.2)).radius(15))
.width(400)
.align(Align::Center);
let err = WidgetPtr::default().add(ui);
client.error = Some(err.clone());
(modal, err.align(Align::Top)).stack().add(ui).any()
}
pub fn msg_widget(msg: Msg) -> impl WidgetLike<FnTag> {
let content = text(msg.content)
.editable()
.size(20)
.text_align(Align::Left)
.wrap(true)
.id_on(CursorSense::click(), |i, c, d| focus(i.clone(), c, d));
let header = text(msg.user).size(20);
(
image(include_bytes!("./assets/sungals.png"))
.sized((70, 70))
.align(Align::TopLeft),
(header.align(Align::TopLeft), content.align(Align::TopLeft))
.span(Dir::DOWN)
.gap(10),
)
.span(Dir::RIGHT)
.gap(10)
}
pub fn focus(id: WidgetId<TextEdit>, client: &mut Client, data: CursorData) {
client.ui.text(&id).select(data.cursor, data.size);
if let Some(region) = client.ui.window_region(&id) {
client.handle.window.set_ime_allowed(true);
client.handle.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().tuple()),
);
}
client.focus = Some(id);
}
pub fn focus_other(id: WidgetId<TextEdit>) -> impl Fn(&mut Client, CursorData) {
move |client: &mut Client, data: CursorData| {
focus(id.clone(), client, data);
}
}
pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn<Stack> + use<> {
let Client { ui, channel, .. } = client;
let msg_area = Span::empty(Dir::DOWN).gap(15).add(ui);
*channel = Some(msg_area.clone());
let send_text = text("")
.editable()
.size(20)
.wrap(true)
.text_align(Align::Left)
.add(ui);
(
msg_area
.clone()
.align(Align::BotLeft)
.scroll()
.pad(Padding::x(15))
.height(rest(1)),
send_text
.clone()
.id_on(Submit, move |id, client: &mut Client, _| {
let content = client.ui.text(id).take();
let msg = Msg {
content: content.clone(),
user: client.username.clone(),
};
network.send(ClientMsg::SendMsg(msg.clone()));
let msg = msg_widget(msg).add(&mut client.ui);
client.ui[&msg_area].children.push(msg.any());
})
.pad(15)
.on(CursorSense::click(), focus_other(send_text))
.background(rect(Color::BLACK.brighter(0.05)).radius(15))
.pad(15)
.height(80)
.z_offset(1),
)
.span(Dir::DOWN)
.width(rest(1))
.background(rect(Color::BLACK.brighter(0.1)))
}