#![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::marker::PhantomData; use serde::{Deserialize, Serialize}; pub const SURFACE_SCHEMA_VERSION: u32 = 1; pub const EFFECT_BATCH_ABI_VERSION: u32 = 1; pub const RUNTIME_ABI_VERSION: u32 = 1; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct BuildFingerprint(pub u64); #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct AtomSnapshot { pub id: u32, pub bytes: Vec, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct AtomState { pub atoms: Vec, } impl AtomState { pub fn to_postcard(&self) -> Result, postcard::Error> { postcard::to_allocvec(self) } pub fn from_postcard(bytes: &[u8]) -> Result { postcard::from_bytes(bytes) } } impl BuildFingerprint { pub const fn from_parts(parts: &[u32]) -> Self { let mut hash = 0xcbf29ce484222325u64; let mut i = 0; while i < parts.len() { hash ^= parts[i] as u64; hash = hash.wrapping_mul(0x100000001b3); i += 1; } Self(hash) } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ResourceKind { Slot, Atom, Handle, Form, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ResourceId { pub kind: ResourceKind, pub id: u32, } impl ResourceId { pub const fn new(kind: ResourceKind, id: u32) -> Self { Self { kind, id } } } /// A generated UI target that can be inspected without exposing raw slots. pub trait GeneratedTarget { #[doc(hidden)] fn __hemx_resource_id(self) -> ResourceId; } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ScopeKey { KeyValue(String), Field(String), } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ResourceRef { pub resource: ResourceId, pub scope: Option, } impl ResourceRef { pub const fn unscoped(resource: ResourceId) -> Self { Self { resource, scope: None, } } pub fn scoped(resource: ResourceId, scope: ScopeKey) -> Self { Self { resource, scope: Some(scope), } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum PatchMode { Morph, Replace, } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum InsertPosition { First, Last, Before(ResourceRef), After(ResourceRef), } pub type MovePosition = InsertPosition; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ScrollBlock { Start, Center, End, Nearest, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ScrollBehavior { Auto, Instant, Smooth, } pub type Url = String; pub type EventRef = String; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum HistoryMode { Push, Replace, } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct SafeHtml(String); impl SafeHtml { pub fn trusted(value: impl Into) -> Self { Self(value.into()) } pub fn into_string(self) -> String { self.0 } pub fn as_str(&self) -> &str { &self.0 } pub fn join(fragments: impl IntoIterator) -> Self { fragments.into_iter().collect() } } impl FromIterator for SafeHtml { fn from_iter>(iter: T) -> Self { let mut html = String::new(); for fragment in iter { html.push_str(fragment.as_str()); } Self(html) } } impl AsRef for SafeHtml { fn as_ref(&self) -> &str { self.as_str() } } impl core::fmt::Display for SafeHtml { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.as_str()) } } /// A generated, checked CSS class token. /// /// Plain CSS/SCSS owns appearance; hemx only gives Rust a typed reference to /// class names discovered from build inputs. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct CssClass { name: &'static str, } impl CssClass { pub const fn new(name: &'static str) -> Self { Self { name } } pub const fn as_str(self) -> &'static str { self.name } pub fn with(self, class: CssClass) -> CssClasses { CssClasses::from([self, class]) } pub fn with_if(self, condition: bool, class: CssClass) -> CssClasses { CssClasses::from(self).with_if(condition, class) } } impl AsRef for CssClass { fn as_ref(&self) -> &str { self.name } } impl core::fmt::Display for CssClass { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.name) } } /// A generated, checked parameter token. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct ParamName { name: &'static str, } impl ParamName { pub const fn new(name: &'static str) -> Self { Self { name } } pub const fn as_str(self) -> &'static str { self.name } } impl AsRef for ParamName { fn as_ref(&self) -> &str { self.name } } impl core::fmt::Display for ParamName { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.name) } } /// A generated, checked component token. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct ComponentRef { name: &'static str, } impl ComponentRef { pub const fn new(name: &'static str) -> Self { Self { name } } pub const fn as_str(self) -> &'static str { self.name } } impl AsRef for ComponentRef { fn as_ref(&self) -> &str { self.name } } impl core::fmt::Display for ComponentRef { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.name) } } /// A generated, checked event token. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct EventName { name: &'static str, } impl EventName { pub const fn new(name: &'static str) -> Self { Self { name } } pub const fn as_str(self) -> &'static str { self.name } pub fn emit(self, payload: impl Into) -> Effect { event(self, payload) } } impl AsRef for EventName { fn as_ref(&self) -> &str { self.name } } impl From for String { fn from(event: EventName) -> Self { event.name.to_string() } } impl core::fmt::Display for EventName { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.name) } } /// A small displayable list of generated CSS class tokens for hemplate `+class`. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct CssClasses { names: String, } impl CssClasses { pub fn new(classes: impl IntoIterator) -> Self { let mut names = String::new(); for class in classes { if !names.is_empty() { names.push(' '); } names.push_str(class.as_str()); } Self { names } } pub fn as_str(&self) -> &str { &self.names } pub fn with(mut self, class: CssClass) -> Self { if !self.names.is_empty() { self.names.push(' '); } self.names.push_str(class.as_str()); self } pub fn with_if(self, condition: bool, class: CssClass) -> Self { if condition { self.with(class) } else { self } } } impl From for CssClasses { fn from(class: CssClass) -> Self { Self::new([class]) } } impl From<[CssClass; N]> for CssClasses { fn from(classes: [CssClass; N]) -> Self { Self::new(classes) } } impl AsRef for CssClasses { fn as_ref(&self) -> &str { self.as_str() } } impl core::fmt::Display for CssClasses { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(&self.names) } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct FormContract { pub fields: &'static [FormField], } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct FormField { pub name: &'static str, pub kind: FormControlKind, pub required: bool, } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum FormControlKind { Text, Number { min: Option<&'static str>, max: Option<&'static str>, step: Option<&'static str>, }, Checkbox, Radio, Select { multiple: bool, }, TextArea, File, Hidden, Submit, Other { tag: &'static str, input_type: Option<&'static str>, }, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum Effect { Patch { target: ResourceRef, mode: PatchMode, html: SafeHtml, }, Insert { target: ResourceRef, position: InsertPosition, html: SafeHtml, }, Remove { target: ResourceRef, }, Move { target: ResourceRef, position: MovePosition, }, Focus { target: ResourceRef, prevent_scroll: bool, }, Scroll { target: ResourceRef, block: ScrollBlock, behavior: ScrollBehavior, }, Visit { url: Url, history: HistoryMode, }, Dispatch { event: EventRef, payload: Vec, }, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EffectBatch { pub abi_version: u32, pub fingerprint: BuildFingerprint, pub ops: Vec, } impl EffectBatch { /// The versioned hemx codec is the sole public `EffectBatch` wire API. /// ```compile_fail /// let batch = hemx_core::EffectBatch::new(hemx_core::BuildFingerprint(1)); /// let _ = batch.to_postcard(); /// ``` /// Return the exact number of bytes produced by [`Self::to_wire`]. pub fn encoded_len(&self) -> usize { batch_wire_len(self) } pub fn to_wire(&self) -> Vec { let encoded_len = self.encoded_len(); let mut out = Vec::with_capacity(encoded_len); let initial_capacity = out.capacity(); write_batch(self, &mut out); debug_assert_eq!(out.len(), encoded_len); debug_assert_eq!(out.capacity(), initial_capacity); out } pub fn from_wire(bytes: &[u8]) -> Result { read_batch(bytes) } pub const fn is_compatible(&self) -> bool { self.abi_version == EFFECT_BATCH_ABI_VERSION } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WireError { BadMagic, UnsupportedVersion, Truncated, InvalidUtf8, UnknownTag, TrailingBytes, } const WIRE_MAGIC: &[u8; 4] = b"HEMX"; const WIRE_BATCH_HEADER_LEN: usize = WIRE_MAGIC.len() + 4 + 8 + 4; fn batch_wire_len(batch: &EffectBatch) -> usize { WIRE_BATCH_HEADER_LEN + batch.ops.iter().map(effect_wire_len).sum::() } fn effect_wire_len(effect: &Effect) -> usize { 1 + match effect { Effect::Patch { target, html, .. } => { ref_wire_len(target) + 1 + str_wire_len(html.as_str()) } Effect::Insert { target, position, html, } => ref_wire_len(target) + position_wire_len(position) + str_wire_len(html.as_str()), Effect::Remove { target } => ref_wire_len(target), Effect::Move { target, position } => ref_wire_len(target) + position_wire_len(position), Effect::Focus { target, .. } => ref_wire_len(target) + 1, Effect::Scroll { target, .. } => ref_wire_len(target) + 2, Effect::Visit { url, .. } => str_wire_len(url) + 1, Effect::Dispatch { event, payload } => { str_wire_len(event.as_str()) + str_wire_len_bytes(payload) } } } fn position_wire_len(position: &InsertPosition) -> usize { 1 + match position { InsertPosition::First | InsertPosition::Last => 0, InsertPosition::Before(reference) | InsertPosition::After(reference) => { ref_wire_len(reference) } } } fn str_wire_len_bytes(value: &[u8]) -> usize { 4 + value.len() } fn ref_wire_len(reference: &ResourceRef) -> usize { 1 + 4 + 1 + match &reference.scope { None => 0, Some(ScopeKey::KeyValue(value) | ScopeKey::Field(value)) => str_wire_len(value), } } const fn str_wire_len(value: &str) -> usize { 4 + value.len() } fn write_batch(batch: &EffectBatch, out: &mut Vec) { out.extend_from_slice(WIRE_MAGIC); write_u32(batch.abi_version, out); write_u64(batch.fingerprint.0, out); write_u32(batch.ops.len() as u32, out); for op in &batch.ops { write_effect(op, out); } } fn write_effect(effect: &Effect, out: &mut Vec) { match effect { Effect::Patch { target, mode, html } => { write_u8(0, out); write_ref(target, out); write_u8( match mode { PatchMode::Morph => 0, PatchMode::Replace => 1, }, out, ); write_str(html.as_str(), out); } Effect::Insert { target, position, html, } => { write_u8(1, out); write_ref(target, out); write_position(position, out); write_str(html.as_str(), out); } Effect::Remove { target } => { write_u8(2, out); write_ref(target, out); } Effect::Move { target, position } => { write_u8(3, out); write_ref(target, out); write_position(position, out); } Effect::Focus { target, prevent_scroll, } => { write_u8(4, out); write_ref(target, out); write_u8(u8::from(*prevent_scroll), out); } Effect::Scroll { target, block, behavior, } => { write_u8(5, out); write_ref(target, out); write_u8( match block { ScrollBlock::Start => 0, ScrollBlock::Center => 1, ScrollBlock::End => 2, ScrollBlock::Nearest => 3, }, out, ); write_u8( match behavior { ScrollBehavior::Auto => 0, ScrollBehavior::Instant => 1, ScrollBehavior::Smooth => 2, }, out, ); } Effect::Visit { url, history } => { write_u8(6, out); write_str(url, out); write_u8( match history { HistoryMode::Push => 0, HistoryMode::Replace => 1, }, out, ); } Effect::Dispatch { event, payload } => { write_u8(7, out); write_str(event.as_str(), out); write_bytes(payload, out); } } } fn write_position(position: &InsertPosition, out: &mut Vec) { match position { InsertPosition::First => write_u8(0, out), InsertPosition::Last => write_u8(1, out), InsertPosition::Before(reference) => { write_u8(2, out); write_ref(reference, out); } InsertPosition::After(reference) => { write_u8(3, out); write_ref(reference, out); } } } fn write_ref(reference: &ResourceRef, out: &mut Vec) { write_u8( match reference.resource.kind { ResourceKind::Slot => 0, ResourceKind::Atom => 1, ResourceKind::Handle => 2, ResourceKind::Form => 3, }, out, ); write_u32(reference.resource.id, out); match &reference.scope { None => write_u8(0, out), Some(ScopeKey::KeyValue(value)) => { write_u8(1, out); write_str(value, out); } Some(ScopeKey::Field(value)) => { write_u8(2, out); write_str(value, out); } } } fn write_bytes(value: &[u8], out: &mut Vec) { write_u32(value.len() as u32, out); out.extend_from_slice(value); } fn write_str(value: &str, out: &mut Vec) { write_u32(value.len() as u32, out); out.extend_from_slice(value.as_bytes()); } fn write_u8(value: u8, out: &mut Vec) { out.push(value); } fn write_u32(value: u32, out: &mut Vec) { out.extend_from_slice(&value.to_le_bytes()); } fn write_u64(value: u64, out: &mut Vec) { out.extend_from_slice(&value.to_le_bytes()); } struct WireReader<'a> { bytes: &'a [u8], offset: usize, } impl<'a> WireReader<'a> { fn new(bytes: &'a [u8]) -> Self { Self { bytes, offset: 0 } } fn finish(&self) -> Result<(), WireError> { if self.offset == self.bytes.len() { Ok(()) } else { Err(WireError::TrailingBytes) } } fn read_u8(&mut self) -> Result { let bytes = self.read_exact(1)?; Ok(bytes[0]) } fn read_u32(&mut self) -> Result { let bytes = self.read_exact(4)?; Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) } fn read_u64(&mut self) -> Result { let bytes = self.read_exact(8)?; Ok(u64::from_le_bytes([ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], ])) } fn read_str(&mut self) -> Result { let len = self.read_u32()? as usize; let bytes = self.read_exact(len)?; core::str::from_utf8(bytes) .map(ToString::to_string) .map_err(|_| WireError::InvalidUtf8) } fn read_bytes(&mut self) -> Result, WireError> { let len = self.read_u32()? as usize; Ok(self.read_exact(len)?.to_vec()) } fn read_exact(&mut self, len: usize) -> Result<&'a [u8], WireError> { let remaining = &self.bytes[self.offset..]; let bytes = remaining.get(..len).ok_or(WireError::Truncated)?; self.offset += len; Ok(bytes) } } fn read_batch(bytes: &[u8]) -> Result { let mut reader = WireReader::new(bytes); if reader.read_exact(WIRE_MAGIC.len())? != WIRE_MAGIC { return Err(WireError::BadMagic); } let abi_version = reader.read_u32()?; if abi_version != EFFECT_BATCH_ABI_VERSION { return Err(WireError::UnsupportedVersion); } let fingerprint = BuildFingerprint(reader.read_u64()?); let ops_len = reader.read_u32()?; let mut ops = Vec::new(); for _ in 0..ops_len { ops.push(read_effect(&mut reader)?); } reader.finish()?; Ok(EffectBatch { abi_version, fingerprint, ops, }) } fn read_effect(reader: &mut WireReader<'_>) -> Result { match reader.read_u8()? { 0 => Ok(Effect::Patch { target: read_ref(reader)?, mode: match reader.read_u8()? { 0 => PatchMode::Morph, 1 => PatchMode::Replace, _ => return Err(WireError::UnknownTag), }, html: SafeHtml::trusted(reader.read_str()?), }), 1 => Ok(Effect::Insert { target: read_ref(reader)?, position: read_position(reader)?, html: SafeHtml::trusted(reader.read_str()?), }), 2 => Ok(Effect::Remove { target: read_ref(reader)?, }), 3 => Ok(Effect::Move { target: read_ref(reader)?, position: read_position(reader)?, }), 4 => Ok(Effect::Focus { target: read_ref(reader)?, prevent_scroll: match reader.read_u8()? { 0 => false, 1 => true, _ => return Err(WireError::UnknownTag), }, }), 5 => Ok(Effect::Scroll { target: read_ref(reader)?, block: match reader.read_u8()? { 0 => ScrollBlock::Start, 1 => ScrollBlock::Center, 2 => ScrollBlock::End, 3 => ScrollBlock::Nearest, _ => return Err(WireError::UnknownTag), }, behavior: match reader.read_u8()? { 0 => ScrollBehavior::Auto, 1 => ScrollBehavior::Instant, 2 => ScrollBehavior::Smooth, _ => return Err(WireError::UnknownTag), }, }), 6 => Ok(Effect::Visit { url: reader.read_str()?, history: match reader.read_u8()? { 0 => HistoryMode::Push, 1 => HistoryMode::Replace, _ => return Err(WireError::UnknownTag), }, }), 7 => Ok(Effect::Dispatch { event: reader.read_str()?, payload: reader.read_bytes()?, }), _ => Err(WireError::UnknownTag), } } fn read_position(reader: &mut WireReader<'_>) -> Result { match reader.read_u8()? { 0 => Ok(InsertPosition::First), 1 => Ok(InsertPosition::Last), 2 => Ok(InsertPosition::Before(read_ref(reader)?)), 3 => Ok(InsertPosition::After(read_ref(reader)?)), _ => Err(WireError::UnknownTag), } } fn read_ref(reader: &mut WireReader<'_>) -> Result { let kind = match reader.read_u8()? { 0 => ResourceKind::Slot, 1 => ResourceKind::Atom, 2 => ResourceKind::Handle, 3 => ResourceKind::Form, _ => return Err(WireError::UnknownTag), }; let resource = ResourceId::new(kind, reader.read_u32()?); let scope = match reader.read_u8()? { 0 => None, 1 => Some(ScopeKey::KeyValue(reader.read_str()?)), 2 => Some(ScopeKey::Field(reader.read_str()?)), _ => return Err(WireError::UnknownTag), }; Ok(ResourceRef { resource, scope }) } pub trait IntoEffect { fn append_to(self, ops: &mut Vec); fn into_batch(self, fingerprint: BuildFingerprint) -> EffectBatch where Self: Sized, { let mut ops = Vec::new(); self.append_to(&mut ops); EffectBatch { abi_version: EFFECT_BATCH_ABI_VERSION, fingerprint, ops, } } } impl IntoEffect for Effect { fn append_to(self, ops: &mut Vec) { ops.push(self); } } impl IntoEffect for () { fn append_to(self, _ops: &mut Vec) {} } impl IntoEffect for Option { fn append_to(self, ops: &mut Vec) { if let Some(effect) = self { effect.append_to(ops); } } } impl IntoEffect for Vec { fn append_to(self, ops: &mut Vec) { for effect in self { effect.append_to(ops); } } } impl IntoEffect for [T; N] { fn append_to(self, ops: &mut Vec) { for effect in self { effect.append_to(ops); } } } macro_rules! impl_tuple_into_effect { ($($name:ident $idx:tt),+) => { impl<$($name),+> IntoEffect for ($($name,)+) where $($name: IntoEffect),+ { fn append_to(self, ops: &mut Vec) { $(self.$idx.append_to(ops);)+ } } }; } impl_tuple_into_effect!(A 0, B 1); impl_tuple_into_effect!(A 0, B 1, C 2); impl_tuple_into_effect!(A 0, B 1, C 2, D 3); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5, G 6); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10); impl_tuple_into_effect!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11); #[derive(Debug, Eq, PartialEq, Hash)] pub struct Slot { id: ResourceId, _marker: PhantomData T>, } impl Clone for Slot { fn clone(&self) -> Self { *self } } impl Copy for Slot {} impl core::fmt::Display for Slot { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.id.id.fmt(f) } } impl Slot { pub const fn new(id: u32) -> Self { Self { id: ResourceId::new(ResourceKind::Slot, id), _marker: PhantomData, } } pub const fn id(self) -> ResourceId { self.id } pub fn text(self, value: impl ToString) -> Effect { self.html(SafeHtml::trusted(value.to_string())) } pub fn html(self, value: impl Into) -> Effect { Effect::Patch { target: ResourceRef::unscoped(self.id), mode: PatchMode::Morph, html: value.into(), } } } #[derive(Debug, Eq, PartialEq, Hash)] pub struct KeyedSlot { id: ResourceId, _marker: PhantomData T>, } impl Clone for KeyedSlot { fn clone(&self) -> Self { *self } } impl Copy for KeyedSlot {} impl core::fmt::Display for KeyedSlot { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.id.id.fmt(f) } } impl KeyedSlot where K: ToString, { pub const fn new(id: u32) -> Self { Self { id: ResourceId::new(ResourceKind::Slot, id), _marker: PhantomData, } } pub const fn id(self) -> ResourceId { self.id } pub fn append_text(self, key: K, value: T) -> Effect where T: ToString, { Effect::Insert { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), position: InsertPosition::Last, html: SafeHtml::trusted(value.to_string()), } } pub fn prepend_text(self, key: K, value: T) -> Effect where T: ToString, { Effect::Insert { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), position: InsertPosition::First, html: SafeHtml::trusted(value.to_string()), } } pub fn replace_text(self, key: K, value: T) -> Effect where T: ToString, { let key = key.to_string(); Effect::Patch { target: ResourceRef { resource: self.id, scope: Some(ScopeKey::KeyValue(key)), }, mode: PatchMode::Morph, html: SafeHtml::trusted(value.to_string()), } } pub fn append_html(self, key: K, value: impl Into) -> Effect { Effect::Insert { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), position: InsertPosition::Last, html: value.into(), } } pub fn prepend_html(self, key: K, value: impl Into) -> Effect { Effect::Insert { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), position: InsertPosition::First, html: value.into(), } } pub fn replace_html(self, key: K, value: impl Into) -> Effect { let key = key.to_string(); Effect::Patch { target: ResourceRef { resource: self.id, scope: Some(ScopeKey::KeyValue(key)), }, mode: PatchMode::Morph, html: value.into(), } } pub fn remove(self, key: K) -> Effect { Effect::Remove { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), } } pub fn move_before(self, key: K, before: K) -> Effect { Effect::Move { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), position: MovePosition::Before(ResourceRef::scoped( self.id, ScopeKey::KeyValue(before.to_string()), )), } } pub fn move_to_end(self, key: K) -> Effect { Effect::Move { target: ResourceRef::scoped(self.id, ScopeKey::KeyValue(key.to_string())), position: MovePosition::Last, } } } #[derive(Debug, Eq, PartialEq, Hash)] pub struct Atom { id: ResourceId, _marker: PhantomData T>, } impl Clone for Atom { fn clone(&self) -> Self { *self } } impl Copy for Atom {} impl core::fmt::Display for Atom { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.id.id.fmt(f) } } impl Atom { pub const fn new(id: u32) -> Self { Self { id: ResourceId::new(ResourceKind::Atom, id), _marker: PhantomData, } } pub const fn id(self) -> ResourceId { self.id } pub fn set(self, value: impl ToString) -> Effect { Effect::Patch { target: ResourceRef::unscoped(self.id), mode: PatchMode::Morph, html: SafeHtml::trusted(value.to_string()), } } } #[derive(Debug, Eq, PartialEq, Hash)] pub struct Handle { id: ResourceId, _marker: PhantomData, } impl Clone for Handle { fn clone(&self) -> Self { *self } } impl Copy for Handle {} impl Handle { pub const fn new(id: u32) -> Self { Self { id: ResourceId::new(ResourceKind::Handle, id), _marker: PhantomData, } } pub const fn id(self) -> ResourceId { self.id } } impl core::fmt::Display for Handle { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.id.id.fmt(f) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct FormError { message: String, } impl FormError { pub fn new(message: impl Into) -> Self { Self { message: message.into(), } } pub fn message(&self) -> &str { &self.message } } impl core::fmt::Display for FormError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(&self.message) } } impl std::error::Error for FormError {} pub trait FormValue: Sized { fn parse_form_value(value: &str) -> Result; } impl FormValue for T where T: std::str::FromStr, { fn parse_form_value(value: &str) -> Result { value.parse().map_err(|_| "invalid form value".to_owned()) } } pub trait FromForm: Sized { fn from_form_fields(fields: &[(String, String)]) -> Result; } pub trait FormModel {} #[derive(Debug, Eq, PartialEq, Hash)] pub struct Form { id: ResourceId, _marker: PhantomData T>, } impl Clone for Form { fn clone(&self) -> Self { *self } } impl Copy for Form {} impl core::fmt::Display for Form { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.id.id.fmt(f) } } impl GeneratedTarget for Form { fn __hemx_resource_id(self) -> ResourceId { self.id } } impl Form { pub const fn new(id: u32) -> Self { Self { id: ResourceId::new(ResourceKind::Form, id), _marker: PhantomData, } } pub const fn id(self) -> ResourceId { self.id } pub const fn typed(self) -> Form { Form { id: self.id, _marker: PhantomData, } } pub fn field(self, name: impl Into) -> ResourceRef { ResourceRef::scoped(self.id, ScopeKey::Field(name.into())) } pub fn clear_field(self, field: impl Into) -> Effect { Effect::Patch { target: self.field(field), mode: PatchMode::Morph, html: SafeHtml::trusted(""), } } pub fn focus(self, field: impl Into) -> Effect { Effect::Focus { target: self.field(field), prevent_scroll: false, } } } pub fn navigate(url: impl Into) -> Effect { push(url) } pub fn push(url: impl Into) -> Effect { Effect::Visit { url: url.into(), history: HistoryMode::Push, } } pub fn replace(url: impl Into) -> Effect { Effect::Visit { url: url.into(), history: HistoryMode::Replace, } } pub fn redirect(url: impl Into) -> Effect { Effect::Visit { url: url.into(), history: HistoryMode::Replace, } } pub fn event(name: impl Into, payload: impl Into) -> Effect { Effect::Dispatch { event: name.into(), payload: payload.into().into_bytes(), } }