Silence warnings, run cargo fmt
This commit is contained in:
parent
fe1c1d3655
commit
af1c767f5f
77 changed files with 1904 additions and 903 deletions
|
|
@ -9,7 +9,7 @@ use uuid::Uuid;
|
|||
|
||||
use chattyness_db::models::ChannelMemberWithAvatar;
|
||||
|
||||
use super::chat_types::{emotion_bubble_colors, ActiveBubble};
|
||||
use super::chat_types::{ActiveBubble, emotion_bubble_colors};
|
||||
|
||||
/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4
|
||||
const BASE_TEXT_SCALE: f64 = 1.4;
|
||||
|
|
@ -65,18 +65,47 @@ impl ContentBounds {
|
|||
let mid_col = [1, 4, 7].iter().any(|&p| has_content_at(p));
|
||||
let right_col = [2, 5, 8].iter().any(|&p| has_content_at(p));
|
||||
|
||||
let min_col = if left_col { 0 } else if mid_col { 1 } else { 2 };
|
||||
let max_col = if right_col { 2 } else if mid_col { 1 } else { 0 };
|
||||
let min_col = if left_col {
|
||||
0
|
||||
} else if mid_col {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let max_col = if right_col {
|
||||
2
|
||||
} else if mid_col {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Rows: 0 (top), 1 (middle), 2 (bottom)
|
||||
let top_row = [0, 1, 2].iter().any(|&p| has_content_at(p));
|
||||
let mid_row = [3, 4, 5].iter().any(|&p| has_content_at(p));
|
||||
let bot_row = [6, 7, 8].iter().any(|&p| has_content_at(p));
|
||||
|
||||
let min_row = if top_row { 0 } else if mid_row { 1 } else { 2 };
|
||||
let max_row = if bot_row { 2 } else if mid_row { 1 } else { 0 };
|
||||
let min_row = if top_row {
|
||||
0
|
||||
} else if mid_row {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let max_row = if bot_row {
|
||||
2
|
||||
} else if mid_row {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Self { min_col, max_col, min_row, max_row }
|
||||
Self {
|
||||
min_col,
|
||||
max_col,
|
||||
min_row,
|
||||
max_row,
|
||||
}
|
||||
}
|
||||
|
||||
/// Content center column (0.0 to 2.0, grid center is 1.0).
|
||||
|
|
@ -158,8 +187,12 @@ impl ScreenBoundaries {
|
|||
half_width: f64,
|
||||
half_height: f64,
|
||||
) -> (f64, f64) {
|
||||
let clamped_x = center_x.max(self.min_x + half_width).min(self.max_x - half_width);
|
||||
let clamped_y = center_y.max(self.min_y + half_height).min(self.max_y - half_height);
|
||||
let clamped_x = center_x
|
||||
.max(self.min_x + half_width)
|
||||
.min(self.max_x - half_width);
|
||||
let clamped_y = center_y
|
||||
.max(self.min_y + half_height)
|
||||
.min(self.max_y - half_height);
|
||||
(clamped_x, clamped_y)
|
||||
}
|
||||
}
|
||||
|
|
@ -310,7 +343,8 @@ pub fn AvatarCanvas(
|
|||
let avatar_half_height = avatar_size / 2.0 + y_content_offset;
|
||||
|
||||
// Calculate bubble height using actual content (includes tail + gap)
|
||||
let estimated_bubble_height = bubble.as_ref()
|
||||
let estimated_bubble_height = bubble
|
||||
.as_ref()
|
||||
.map(|b| estimate_bubble_height(&b.message.content, text_scale))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
|
|
@ -363,8 +397,8 @@ pub fn AvatarCanvas(
|
|||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
|
||||
// Image cache for this avatar (persists across re-renders)
|
||||
let image_cache: Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>> =
|
||||
|
|
@ -426,7 +460,8 @@ pub fn AvatarCanvas(
|
|||
let avatar_half_height = avatar_size / 2.0 + y_content_offset;
|
||||
|
||||
// Calculate bubble height using actual content (includes tail + gap)
|
||||
let estimated_bubble_height = bubble.as_ref()
|
||||
let estimated_bubble_height = bubble
|
||||
.as_ref()
|
||||
.map(|b| estimate_bubble_height(&b.message.content, text_scale))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
|
|
@ -470,7 +505,12 @@ pub fn AvatarCanvas(
|
|||
|
||||
// Helper to load and draw an image
|
||||
// Images are cached; when loaded, triggers a redraw via signal
|
||||
let draw_image = |path: &str, cache: &Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>>, ctx: &web_sys::CanvasRenderingContext2d, x: f64, y: f64, size: f64| {
|
||||
let draw_image = |path: &str,
|
||||
cache: &Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>>,
|
||||
ctx: &web_sys::CanvasRenderingContext2d,
|
||||
x: f64,
|
||||
y: f64,
|
||||
size: f64| {
|
||||
let normalized_path = normalize_asset_path(path);
|
||||
let mut cache_borrow = cache.borrow_mut();
|
||||
|
||||
|
|
@ -526,7 +566,14 @@ pub fn AvatarCanvas(
|
|||
let row = pos / 3;
|
||||
let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
|
||||
let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
|
||||
draw_image(clothes_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size);
|
||||
draw_image(
|
||||
clothes_path,
|
||||
&image_cache,
|
||||
&ctx,
|
||||
cell_cx,
|
||||
cell_cy,
|
||||
cell_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -537,7 +584,14 @@ pub fn AvatarCanvas(
|
|||
let row = pos / 3;
|
||||
let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
|
||||
let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
|
||||
draw_image(accessories_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size);
|
||||
draw_image(
|
||||
accessories_path,
|
||||
&image_cache,
|
||||
&ctx,
|
||||
cell_cx,
|
||||
cell_cy,
|
||||
cell_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -548,7 +602,14 @@ pub fn AvatarCanvas(
|
|||
let row = pos / 3;
|
||||
let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
|
||||
let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
|
||||
draw_image(emotion_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size);
|
||||
draw_image(
|
||||
emotion_path,
|
||||
&image_cache,
|
||||
&ctx,
|
||||
cell_cx,
|
||||
cell_cy,
|
||||
cell_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -560,7 +621,13 @@ pub fn AvatarCanvas(
|
|||
let badge_y = avatar_cy - avatar_size / 2.0 - badge_size / 2.0;
|
||||
|
||||
ctx.begin_path();
|
||||
let _ = ctx.arc(badge_x, badge_y, badge_size / 2.0, 0.0, std::f64::consts::PI * 2.0);
|
||||
let _ = ctx.arc(
|
||||
badge_x,
|
||||
badge_y,
|
||||
badge_size / 2.0,
|
||||
0.0,
|
||||
std::f64::consts::PI * 2.0,
|
||||
);
|
||||
ctx.set_fill_style_str("#f59e0b");
|
||||
ctx.fill();
|
||||
|
||||
|
|
@ -580,7 +647,8 @@ pub fn AvatarCanvas(
|
|||
ctx.set_font(&format!("{}px sans-serif", 12.0 * text_scale));
|
||||
ctx.set_text_align("center");
|
||||
ctx.set_text_baseline("alphabetic");
|
||||
let name_y = avatar_cy + avatar_size / 2.0 - (empty_bottom_rows as f64 * cell_size) + 15.0 * text_scale;
|
||||
let name_y = avatar_cy + avatar_size / 2.0 - (empty_bottom_rows as f64 * cell_size)
|
||||
+ 15.0 * text_scale;
|
||||
// Black outline
|
||||
ctx.set_stroke_style_str("#000");
|
||||
ctx.set_line_width(3.0);
|
||||
|
|
@ -630,7 +698,9 @@ pub fn AvatarCanvas(
|
|||
// Compute data-member-id reactively
|
||||
let data_member_id = move || {
|
||||
let m = member.get();
|
||||
m.member.user_id.map(|u| u.to_string())
|
||||
m.member
|
||||
.user_id
|
||||
.map(|u| u.to_string())
|
||||
.or_else(|| m.member.guest_session_id.map(|g| g.to_string()))
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
|
@ -693,11 +763,19 @@ fn draw_bubble(
|
|||
let (bg_color, border_color, text_color) = emotion_bubble_colors(&bubble.message.emotion);
|
||||
|
||||
// Use italic font for whispers
|
||||
let font_style = if bubble.message.is_whisper { "italic " } else { "" };
|
||||
let font_style = if bubble.message.is_whisper {
|
||||
"italic "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
// Measure and wrap text
|
||||
ctx.set_font(&format!("{}{}px sans-serif", font_style, font_size));
|
||||
let lines = wrap_text(ctx, &bubble.message.content, max_bubble_width - padding * 2.0);
|
||||
let lines = wrap_text(
|
||||
ctx,
|
||||
&bubble.message.content,
|
||||
max_bubble_width - padding * 2.0,
|
||||
);
|
||||
|
||||
// Calculate bubble dimensions
|
||||
let bubble_width = lines
|
||||
|
|
@ -747,7 +825,14 @@ fn draw_bubble(
|
|||
};
|
||||
|
||||
// Draw bubble background
|
||||
draw_rounded_rect(ctx, bubble_x, bubble_y, bubble_width, bubble_height, border_radius);
|
||||
draw_rounded_rect(
|
||||
ctx,
|
||||
bubble_x,
|
||||
bubble_y,
|
||||
bubble_width,
|
||||
bubble_height,
|
||||
border_radius,
|
||||
);
|
||||
ctx.set_fill_style_str(bg_color);
|
||||
ctx.fill();
|
||||
ctx.set_stroke_style_str(border_color);
|
||||
|
|
@ -782,7 +867,11 @@ fn draw_bubble(
|
|||
ctx.set_text_align("left");
|
||||
ctx.set_text_baseline("top");
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let _ = ctx.fill_text(line, bubble_x + padding, bubble_y + padding + (i as f64) * line_height);
|
||||
let _ = ctx.fill_text(
|
||||
line,
|
||||
bubble_x + padding,
|
||||
bubble_y + padding + (i as f64) * line_height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -800,7 +889,10 @@ fn wrap_text(ctx: &web_sys::CanvasRenderingContext2d, text: &str, max_width: f64
|
|||
format!("{} {}", current_line, word)
|
||||
};
|
||||
|
||||
let width = ctx.measure_text(&test_line).map(|m| m.width()).unwrap_or(0.0);
|
||||
let width = ctx
|
||||
.measure_text(&test_line)
|
||||
.map(|m| m.width())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
if width > max_width && !current_line.is_empty() {
|
||||
lines.push(current_line);
|
||||
|
|
@ -834,11 +926,7 @@ fn wrap_text(ctx: &web_sys::CanvasRenderingContext2d, text: &str, max_width: f64
|
|||
/// Returns true if the alpha channel at the clicked pixel is > 0.
|
||||
/// This enables pixel-perfect hit detection on avatar canvases.
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn hit_test_canvas(
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
client_x: f64,
|
||||
client_y: f64,
|
||||
) -> bool {
|
||||
pub fn hit_test_canvas(canvas: &web_sys::HtmlCanvasElement, client_x: f64, client_y: f64) -> bool {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
// Get the canvas bounding rect to transform client coords to canvas coords
|
||||
|
|
@ -849,7 +937,11 @@ pub fn hit_test_canvas(
|
|||
let relative_y = client_y - rect.top();
|
||||
|
||||
// Check if click is within canvas bounds
|
||||
if relative_x < 0.0 || relative_y < 0.0 || relative_x >= rect.width() || relative_y >= rect.height() {
|
||||
if relative_x < 0.0
|
||||
|| relative_y < 0.0
|
||||
|| relative_x >= rect.width()
|
||||
|| relative_y >= rect.height()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ fn RenderedPreview(#[prop(into)] avatar: Signal<Option<AvatarWithPaths>>) -> imp
|
|||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
|
||||
let image_cache: Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>> =
|
||||
Rc::new(RefCell::new(HashMap::new()));
|
||||
|
|
@ -134,36 +134,37 @@ fn RenderedPreview(#[prop(into)] avatar: Signal<Option<AvatarWithPaths>>) -> imp
|
|||
ctx.fill_rect(0.0, 0.0, canvas_size as f64, canvas_size as f64);
|
||||
|
||||
// Helper to load and draw an image at a grid position
|
||||
let draw_at_position = |path: &str,
|
||||
pos: usize,
|
||||
cache: &Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>>,
|
||||
ctx: &web_sys::CanvasRenderingContext2d| {
|
||||
let normalized_path = normalize_asset_path(path);
|
||||
let mut cache_borrow = cache.borrow_mut();
|
||||
let row = pos / 3;
|
||||
let col = pos % 3;
|
||||
let x = (col * cell_size) as f64;
|
||||
let y = (row * cell_size) as f64;
|
||||
let size = cell_size as f64;
|
||||
let draw_at_position =
|
||||
|path: &str,
|
||||
pos: usize,
|
||||
cache: &Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>>,
|
||||
ctx: &web_sys::CanvasRenderingContext2d| {
|
||||
let normalized_path = normalize_asset_path(path);
|
||||
let mut cache_borrow = cache.borrow_mut();
|
||||
let row = pos / 3;
|
||||
let col = pos % 3;
|
||||
let x = (col * cell_size) as f64;
|
||||
let y = (row * cell_size) as f64;
|
||||
let size = cell_size as f64;
|
||||
|
||||
if let Some(img) = cache_borrow.get(&normalized_path) {
|
||||
if img.complete() && img.natural_width() > 0 {
|
||||
let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh(
|
||||
img, x, y, size, size,
|
||||
);
|
||||
if let Some(img) = cache_borrow.get(&normalized_path) {
|
||||
if img.complete() && img.natural_width() > 0 {
|
||||
let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh(
|
||||
img, x, y, size, size,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let img = web_sys::HtmlImageElement::new().unwrap();
|
||||
let trigger = set_redraw_trigger;
|
||||
let onload = Closure::once(Box::new(move || {
|
||||
trigger.update(|v| *v += 1);
|
||||
}) as Box<dyn FnOnce()>);
|
||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
||||
onload.forget();
|
||||
img.set_src(&normalized_path);
|
||||
cache_borrow.insert(normalized_path, img);
|
||||
}
|
||||
} else {
|
||||
let img = web_sys::HtmlImageElement::new().unwrap();
|
||||
let trigger = set_redraw_trigger;
|
||||
let onload = Closure::once(Box::new(move || {
|
||||
trigger.update(|v| *v += 1);
|
||||
}) as Box<dyn FnOnce()>);
|
||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
||||
onload.forget();
|
||||
img.set_src(&normalized_path);
|
||||
cache_borrow.insert(normalized_path, img);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Draw layers in order: skin -> clothes -> accessories -> current emotion
|
||||
for (pos, path) in av.skin_layer.iter().enumerate() {
|
||||
|
|
@ -252,7 +253,7 @@ pub fn AvatarEditorPopup(
|
|||
let (context_menu, set_context_menu) = signal(Option::<ContextMenuState>::None);
|
||||
|
||||
// Saving state
|
||||
let (saving, set_saving) = signal(false);
|
||||
let (_saving, set_saving) = signal(false);
|
||||
|
||||
// Helper to get current layer name for API calls
|
||||
let get_current_layer_name = move || -> String {
|
||||
|
|
@ -290,8 +291,9 @@ pub fn AvatarEditorPopup(
|
|||
let response = Request::get("/api/inventory").send().await;
|
||||
match response {
|
||||
Ok(resp) if resp.ok() => {
|
||||
if let Ok(data) =
|
||||
resp.json::<chattyness_db::models::InventoryResponse>().await
|
||||
if let Ok(data) = resp
|
||||
.json::<chattyness_db::models::InventoryResponse>()
|
||||
.await
|
||||
{
|
||||
set_inventory_items.set(data.items);
|
||||
set_inventory_loaded.set(true);
|
||||
|
|
@ -353,8 +355,14 @@ pub fn AvatarEditorPopup(
|
|||
item.layer
|
||||
.map(|l| match (l, layer) {
|
||||
(chattyness_db::models::AvatarLayer::Skin, BaseLayer::Skin) => true,
|
||||
(chattyness_db::models::AvatarLayer::Clothes, BaseLayer::Clothes) => true,
|
||||
(chattyness_db::models::AvatarLayer::Accessories, BaseLayer::Accessories) => true,
|
||||
(
|
||||
chattyness_db::models::AvatarLayer::Clothes,
|
||||
BaseLayer::Clothes,
|
||||
) => true,
|
||||
(
|
||||
chattyness_db::models::AvatarLayer::Accessories,
|
||||
BaseLayer::Accessories,
|
||||
) => true,
|
||||
_ => false,
|
||||
})
|
||||
.unwrap_or(false)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use leptos::prelude::*;
|
|||
use chattyness_db::models::EmotionAvailability;
|
||||
use chattyness_db::ws_messages::ClientMessage;
|
||||
|
||||
use super::emotion_picker::{EmoteListPopup, LabelStyle, EMOTIONS};
|
||||
use super::emotion_picker::{EMOTIONS, EmoteListPopup, LabelStyle};
|
||||
use super::ws_client::WsSenderStorage;
|
||||
|
||||
/// Command mode state for the chat input.
|
||||
|
|
@ -90,13 +90,10 @@ pub fn ChatInput(
|
|||
emotion_availability: Signal<Option<EmotionAvailability>>,
|
||||
skin_preview_path: Signal<Option<String>>,
|
||||
focus_trigger: Signal<bool>,
|
||||
#[prop(default = Signal::derive(|| ':'))]
|
||||
focus_prefix: Signal<char>,
|
||||
#[prop(default = Signal::derive(|| ':'))] focus_prefix: Signal<char>,
|
||||
on_focus_change: Callback<bool>,
|
||||
#[prop(optional)]
|
||||
on_open_settings: Option<Callback<()>>,
|
||||
#[prop(optional)]
|
||||
on_open_inventory: Option<Callback<()>>,
|
||||
#[prop(optional)] on_open_settings: Option<Callback<()>>,
|
||||
#[prop(optional)] on_open_inventory: Option<Callback<()>>,
|
||||
/// Signal containing the display name to whisper to. When set, pre-fills the input.
|
||||
#[prop(optional, into)]
|
||||
whisper_target: Option<Signal<Option<String>>>,
|
||||
|
|
|
|||
|
|
@ -96,18 +96,18 @@ pub struct ActiveBubble {
|
|||
/// Returns (background_color, border_color, text_color).
|
||||
pub fn emotion_bubble_colors(emotion: &str) -> (&'static str, &'static str, &'static str) {
|
||||
match emotion {
|
||||
"neutral" => ("#374151", "#4B5563", "#F9FAFB"), // gray
|
||||
"happy" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber
|
||||
"sad" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue
|
||||
"angry" => ("#EF4444", "#DC2626", "#F9FAFB"), // red
|
||||
"neutral" => ("#374151", "#4B5563", "#F9FAFB"), // gray
|
||||
"happy" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber
|
||||
"sad" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue
|
||||
"angry" => ("#EF4444", "#DC2626", "#F9FAFB"), // red
|
||||
"surprised" => ("#A855F7", "#9333EA", "#F9FAFB"), // purple
|
||||
"thinking" => ("#6366F1", "#4F46E5", "#F9FAFB"), // indigo
|
||||
"laughing" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber
|
||||
"crying" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue
|
||||
"love" => ("#EC4899", "#DB2777", "#F9FAFB"), // pink
|
||||
"confused" => ("#8B5CF6", "#7C3AED", "#F9FAFB"), // violet
|
||||
"sleeping" => ("#1F2937", "#374151", "#9CA3AF"), // dark gray
|
||||
"wink" => ("#10B981", "#059669", "#F9FAFB"), // emerald
|
||||
_ => ("#374151", "#4B5563", "#F9FAFB"), // default - gray
|
||||
"thinking" => ("#6366F1", "#4F46E5", "#F9FAFB"), // indigo
|
||||
"laughing" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber
|
||||
"crying" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue
|
||||
"love" => ("#EC4899", "#DB2777", "#F9FAFB"), // pink
|
||||
"confused" => ("#8B5CF6", "#7C3AED", "#F9FAFB"), // violet
|
||||
"sleeping" => ("#1F2937", "#374151", "#9CA3AF"), // dark gray
|
||||
"wink" => ("#10B981", "#059669", "#F9FAFB"), // emerald
|
||||
_ => ("#374151", "#4B5563", "#F9FAFB"), // default - gray
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ pub fn ContextMenu(
|
|||
// Click outside handler
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use wasm_bindgen::{JsCast, closure::Closure};
|
||||
|
||||
Effect::new(move |_| {
|
||||
if !open.get() {
|
||||
|
|
@ -100,28 +100,35 @@ pub fn ContextMenu(
|
|||
let menu_el: web_sys::HtmlElement = menu_el.into();
|
||||
let menu_el_clone = menu_el.clone();
|
||||
|
||||
let handler = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
if let Some(target) = ev.target() {
|
||||
if let Ok(target_el) = target.dyn_into::<web_sys::Node>() {
|
||||
if !menu_el_clone.contains(Some(&target_el)) {
|
||||
on_close.run(());
|
||||
let handler =
|
||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
if let Some(target) = ev.target() {
|
||||
if let Ok(target_el) = target.dyn_into::<web_sys::Node>() {
|
||||
if !menu_el_clone.contains(Some(&target_el)) {
|
||||
on_close.run(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let _ = window.add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref());
|
||||
let _ = window
|
||||
.add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref());
|
||||
|
||||
// Escape key handler
|
||||
let on_close_esc = on_close.clone();
|
||||
let keydown_handler = Closure::<dyn Fn(web_sys::KeyboardEvent)>::new(move |ev: web_sys::KeyboardEvent| {
|
||||
if ev.key() == "Escape" {
|
||||
on_close_esc.run(());
|
||||
ev.prevent_default();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback("keydown", keydown_handler.as_ref().unchecked_ref());
|
||||
let keydown_handler = Closure::<dyn Fn(web_sys::KeyboardEvent)>::new(
|
||||
move |ev: web_sys::KeyboardEvent| {
|
||||
if ev.key() == "Escape" {
|
||||
on_close_esc.run(());
|
||||
ev.prevent_default();
|
||||
}
|
||||
},
|
||||
);
|
||||
let _ = window.add_event_listener_with_callback(
|
||||
"keydown",
|
||||
keydown_handler.as_ref().unchecked_ref(),
|
||||
);
|
||||
|
||||
// Store handlers to clean up (they get cleaned up when Effect reruns)
|
||||
handler.forget();
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ pub fn ConversationModal(
|
|||
if let Some(input) = input_ref.get() {
|
||||
let _ = input.focus();
|
||||
}
|
||||
}) as Box<dyn FnOnce()>);
|
||||
})
|
||||
as Box<dyn FnOnce()>);
|
||||
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
|
|
|
|||
|
|
@ -85,7 +85,9 @@ pub fn SceneCanvas(
|
|||
let canvas_style = Signal::derive(move || {
|
||||
let w = width.get();
|
||||
let h = height.get();
|
||||
let bg_color = background_color.get().unwrap_or_else(|| "#1a1a2e".to_string());
|
||||
let bg_color = background_color
|
||||
.get()
|
||||
.unwrap_or_else(|| "#1a1a2e".to_string());
|
||||
|
||||
if let Some(img) = background_image.get() {
|
||||
format!(
|
||||
|
|
@ -93,7 +95,10 @@ pub fn SceneCanvas(
|
|||
w, h, img
|
||||
)
|
||||
} else {
|
||||
format!("width: {}px; height: {}px; background-color: {};", w, h, bg_color)
|
||||
format!(
|
||||
"width: {}px; height: {}px; background-color: {};",
|
||||
w, h, bg_color
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -134,7 +139,6 @@ pub fn SceneCanvas(
|
|||
|
||||
/// Canvas for drawing new spots.
|
||||
#[component]
|
||||
#[allow(unused_variables)]
|
||||
pub fn SpotDrawer(
|
||||
#[prop(into)] width: Signal<u32>,
|
||||
#[prop(into)] height: Signal<u32>,
|
||||
|
|
@ -144,17 +148,16 @@ pub fn SpotDrawer(
|
|||
#[prop(into)] background_image: Signal<Option<String>>,
|
||||
#[prop(into)] existing_spots_wkt: Signal<Vec<String>>,
|
||||
) -> impl IntoView {
|
||||
let (drawing_points, _set_drawing_points) = signal(Vec::<(f64, f64)>::new());
|
||||
let (is_drawing, _set_is_drawing) = signal(false);
|
||||
let (start_point, _set_start_point) = signal(Option::<(f64, f64)>::None);
|
||||
#[cfg(feature = "hydrate")]
|
||||
let (set_drawing_points, set_is_drawing, set_start_point) =
|
||||
(_set_drawing_points, _set_is_drawing, _set_start_point);
|
||||
let (drawing_points, set_drawing_points) = signal(Vec::<(f64, f64)>::new());
|
||||
let (is_drawing, set_is_drawing) = signal(false);
|
||||
let (start_point, set_start_point) = signal(Option::<(f64, f64)>::None);
|
||||
|
||||
let canvas_style = Signal::derive(move || {
|
||||
let w = width.get();
|
||||
let h = height.get();
|
||||
let bg_color = background_color.get().unwrap_or_else(|| "#1a1a2e".to_string());
|
||||
let bg_color = background_color
|
||||
.get()
|
||||
.unwrap_or_else(|| "#1a1a2e".to_string());
|
||||
|
||||
if let Some(img) = background_image.get() {
|
||||
format!(
|
||||
|
|
@ -162,20 +165,21 @@ pub fn SpotDrawer(
|
|||
w, h, img
|
||||
)
|
||||
} else {
|
||||
format!("width: {}px; height: {}px; background-color: {}; cursor: crosshair;", w, h, bg_color)
|
||||
format!(
|
||||
"width: {}px; height: {}px; background-color: {}; cursor: crosshair;",
|
||||
w, h, bg_color
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let on_mouse_down = move |ev: leptos::ev::MouseEvent| {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
let rect = ev
|
||||
.target()
|
||||
.and_then(|t| {
|
||||
use wasm_bindgen::JsCast;
|
||||
t.dyn_ref::<web_sys::HtmlElement>()
|
||||
.map(|el| el.get_bounding_client_rect())
|
||||
});
|
||||
let rect = ev.target().and_then(|t| {
|
||||
use wasm_bindgen::JsCast;
|
||||
t.dyn_ref::<web_sys::HtmlElement>()
|
||||
.map(|el| el.get_bounding_client_rect())
|
||||
});
|
||||
|
||||
if let Some(rect) = rect {
|
||||
let x = ev.client_x() as f64 - rect.left();
|
||||
|
|
@ -202,13 +206,11 @@ pub fn SpotDrawer(
|
|||
{
|
||||
if mode.get() == DrawingMode::Rectangle && is_drawing.get() {
|
||||
if let Some((start_x, start_y)) = start_point.get() {
|
||||
let rect = ev
|
||||
.target()
|
||||
.and_then(|t| {
|
||||
use wasm_bindgen::JsCast;
|
||||
t.dyn_ref::<web_sys::HtmlElement>()
|
||||
.map(|el| el.get_bounding_client_rect())
|
||||
});
|
||||
let rect = ev.target().and_then(|t| {
|
||||
use wasm_bindgen::JsCast;
|
||||
t.dyn_ref::<web_sys::HtmlElement>()
|
||||
.map(|el| el.get_bounding_client_rect())
|
||||
});
|
||||
|
||||
if let Some(rect) = rect {
|
||||
let end_x = ev.client_x() as f64 - rect.left();
|
||||
|
|
@ -222,7 +224,16 @@ pub fn SpotDrawer(
|
|||
if (max_x - min_x) > 10.0 && (max_y - min_y) > 10.0 {
|
||||
let wkt = format!(
|
||||
"POLYGON(({} {}, {} {}, {} {}, {} {}, {} {}))",
|
||||
min_x, min_y, max_x, min_y, max_x, max_y, min_x, max_y, min_x, min_y
|
||||
min_x,
|
||||
min_y,
|
||||
max_x,
|
||||
min_y,
|
||||
max_x,
|
||||
max_y,
|
||||
min_x,
|
||||
max_y,
|
||||
min_x,
|
||||
min_y
|
||||
);
|
||||
on_complete.run(wkt);
|
||||
}
|
||||
|
|
@ -324,8 +335,14 @@ fn parse_wkt_to_style(wkt: &str) -> String {
|
|||
if points.len() >= 4 {
|
||||
let min_x = points.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
|
||||
let min_y = points.iter().map(|(_, y)| *y).fold(f64::INFINITY, f64::min);
|
||||
let max_x = points.iter().map(|(x, _)| *x).fold(f64::NEG_INFINITY, f64::max);
|
||||
let max_y = points.iter().map(|(_, y)| *y).fold(f64::NEG_INFINITY, f64::max);
|
||||
let max_x = points
|
||||
.iter()
|
||||
.map(|(x, _)| *x)
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let max_y = points
|
||||
.iter()
|
||||
.map(|(_, y)| *y)
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
|
||||
return format!(
|
||||
"left: {}px; top: {}px; width: {}px; height: {}px;",
|
||||
|
|
|
|||
|
|
@ -104,9 +104,7 @@ pub fn EmoteListPopup(
|
|||
if show_all_emotions {
|
||||
EMOTIONS
|
||||
.iter()
|
||||
.filter(|name| {
|
||||
filter_text.is_empty() || name.starts_with(&filter_text)
|
||||
})
|
||||
.filter(|name| filter_text.is_empty() || name.starts_with(&filter_text))
|
||||
.map(|name| ((*name).to_string(), None, true))
|
||||
.collect()
|
||||
} else {
|
||||
|
|
@ -223,7 +221,7 @@ pub fn EmotionPreview(skin_path: Option<String>, emotion_path: Option<String>) -
|
|||
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use wasm_bindgen::{JsCast, closure::Closure};
|
||||
|
||||
let skin_path_clone = skin_path.clone();
|
||||
let emotion_path_clone = emotion_path.clone();
|
||||
|
|
|
|||
|
|
@ -295,7 +295,10 @@ pub fn ColorPicker(
|
|||
|
||||
/// Color palette component.
|
||||
#[component]
|
||||
pub fn ColorPalette(#[prop(into)] value: Signal<String>, on_change: Callback<String>) -> impl IntoView {
|
||||
pub fn ColorPalette(
|
||||
#[prop(into)] value: Signal<String>,
|
||||
on_change: Callback<String>,
|
||||
) -> impl IntoView {
|
||||
let colors = [
|
||||
"#1a1a2e", "#16213e", "#0f3460", "#e94560", "#533483", "#2c3e50", "#1e8449", "#d35400",
|
||||
];
|
||||
|
|
@ -334,7 +337,10 @@ pub fn ColorPalette(#[prop(into)] value: Signal<String>, on_change: Callback<Str
|
|||
fn event_target_checked(ev: &leptos::ev::Event) -> bool {
|
||||
use wasm_bindgen::JsCast;
|
||||
ev.target()
|
||||
.and_then(|t| t.dyn_ref::<web_sys::HtmlInputElement>().map(|el| el.checked()))
|
||||
.and_then(|t| {
|
||||
t.dyn_ref::<web_sys::HtmlInputElement>()
|
||||
.map(|el| el.checked())
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,8 +84,9 @@ pub fn InventoryPopup(
|
|||
let response = Request::get("/api/inventory").send().await;
|
||||
match response {
|
||||
Ok(resp) if resp.ok() => {
|
||||
if let Ok(data) =
|
||||
resp.json::<chattyness_db::models::InventoryResponse>().await
|
||||
if let Ok(data) = resp
|
||||
.json::<chattyness_db::models::InventoryResponse>()
|
||||
.await
|
||||
{
|
||||
set_items.set(data.items);
|
||||
set_my_inventory_loaded.set(true);
|
||||
|
|
@ -123,8 +124,9 @@ pub fn InventoryPopup(
|
|||
let response = Request::get("/api/inventory/server").send().await;
|
||||
match response {
|
||||
Ok(resp) if resp.ok() => {
|
||||
if let Ok(data) =
|
||||
resp.json::<chattyness_db::models::PublicPropsResponse>().await
|
||||
if let Ok(data) = resp
|
||||
.json::<chattyness_db::models::PublicPropsResponse>()
|
||||
.await
|
||||
{
|
||||
set_server_props.set(data.props);
|
||||
set_server_loaded.set(true);
|
||||
|
|
@ -133,8 +135,10 @@ pub fn InventoryPopup(
|
|||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
set_server_error
|
||||
.set(Some(format!("Failed to load server props: {}", resp.status())));
|
||||
set_server_error.set(Some(format!(
|
||||
"Failed to load server props: {}",
|
||||
resp.status()
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
set_server_error.set(Some(format!("Network error: {}", e)));
|
||||
|
|
@ -171,8 +175,9 @@ pub fn InventoryPopup(
|
|||
.await;
|
||||
match response {
|
||||
Ok(resp) if resp.ok() => {
|
||||
if let Ok(data) =
|
||||
resp.json::<chattyness_db::models::PublicPropsResponse>().await
|
||||
if let Ok(data) = resp
|
||||
.json::<chattyness_db::models::PublicPropsResponse>()
|
||||
.await
|
||||
{
|
||||
set_realm_props.set(data.props);
|
||||
set_realm_loaded.set(true);
|
||||
|
|
@ -181,8 +186,10 @@ pub fn InventoryPopup(
|
|||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
set_realm_error
|
||||
.set(Some(format!("Failed to load realm props: {}", resp.status())));
|
||||
set_realm_error.set(Some(format!(
|
||||
"Failed to load realm props: {}",
|
||||
resp.status()
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
set_realm_error.set(Some(format!("Network error: {}", e)));
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ use crate::utils::LocalStoragePersist;
|
|||
|
||||
/// Key slot names for the 12 emotion keybindings.
|
||||
/// Maps to e1, e2, ..., e9, e0, eq, ew
|
||||
pub const KEYBINDING_SLOTS: [&str; 12] = [
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "q", "w",
|
||||
];
|
||||
pub const KEYBINDING_SLOTS: [&str; 12] =
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "q", "w"];
|
||||
|
||||
/// Default emotion order for keybinding slots.
|
||||
/// Slot 0 (e1) is always Happy (locked).
|
||||
|
|
|
|||
|
|
@ -189,7 +189,10 @@ pub fn Card(#[prop(optional)] class: &'static str, children: Children) -> impl I
|
|||
#[component]
|
||||
pub fn SceneThumbnail(scene: chattyness_db::models::SceneSummary) -> impl IntoView {
|
||||
let background_style = match (&scene.background_image_path, &scene.background_color) {
|
||||
(Some(path), _) => format!("background-image: url('{}'); background-size: cover; background-position: center;", path),
|
||||
(Some(path), _) => format!(
|
||||
"background-image: url('{}'); background-size: cover; background-position: center;",
|
||||
path
|
||||
),
|
||||
(None, Some(color)) => format!("background-color: {};", color),
|
||||
(None, None) => "background-color: #1a1a2e;".to_string(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -75,9 +75,7 @@ pub fn NotificationToast(
|
|||
|
||||
// Clear any existing timer
|
||||
if let Some(handle) = timer_handle_effect.borrow_mut().take() {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.clear_timeout_with_handle(handle);
|
||||
web_sys::window().unwrap().clear_timeout_with_handle(handle);
|
||||
}
|
||||
|
||||
// Start new timer if notification is present
|
||||
|
|
@ -89,13 +87,16 @@ pub fn NotificationToast(
|
|||
let closure = wasm_bindgen::closure::Closure::once(Box::new(move || {
|
||||
on_dismiss.run(id);
|
||||
timer_handle_inner.borrow_mut().take();
|
||||
}) as Box<dyn FnOnce()>);
|
||||
})
|
||||
as Box<dyn FnOnce()>);
|
||||
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Ok(handle) = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
closure.as_ref().unchecked_ref(),
|
||||
5000, // 5 second auto-dismiss
|
||||
) {
|
||||
if let Ok(handle) = window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
closure.as_ref().unchecked_ref(),
|
||||
5000, // 5 second auto-dismiss
|
||||
)
|
||||
{
|
||||
*timer_handle_effect.borrow_mut() = Some(handle);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,8 +111,9 @@ pub fn NotificationToast(
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
let closure_holder: Rc<RefCell<Option<wasm_bindgen::closure::Closure<dyn Fn(web_sys::KeyboardEvent)>>>> =
|
||||
Rc::new(RefCell::new(None));
|
||||
let closure_holder: Rc<
|
||||
RefCell<Option<wasm_bindgen::closure::Closure<dyn Fn(web_sys::KeyboardEvent)>>>,
|
||||
> = Rc::new(RefCell::new(None));
|
||||
let closure_holder_clone = closure_holder.clone();
|
||||
|
||||
Effect::new(move |_| {
|
||||
|
|
@ -140,37 +142,37 @@ pub fn NotificationToast(
|
|||
let on_history = on_history.clone();
|
||||
let on_dismiss = on_dismiss.clone();
|
||||
|
||||
let closure = wasm_bindgen::closure::Closure::<dyn Fn(web_sys::KeyboardEvent)>::new(move |ev: web_sys::KeyboardEvent| {
|
||||
let key = ev.key();
|
||||
match key.as_str() {
|
||||
"r" | "R" => {
|
||||
ev.prevent_default();
|
||||
on_reply.run(display_name.clone());
|
||||
on_dismiss.run(notif_id);
|
||||
let closure = wasm_bindgen::closure::Closure::<dyn Fn(web_sys::KeyboardEvent)>::new(
|
||||
move |ev: web_sys::KeyboardEvent| {
|
||||
let key = ev.key();
|
||||
match key.as_str() {
|
||||
"r" | "R" => {
|
||||
ev.prevent_default();
|
||||
on_reply.run(display_name.clone());
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
"c" | "C" => {
|
||||
ev.prevent_default();
|
||||
on_context.run(display_name.clone());
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
"h" | "H" => {
|
||||
ev.prevent_default();
|
||||
on_history.run(());
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
"Escape" => {
|
||||
ev.prevent_default();
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"c" | "C" => {
|
||||
ev.prevent_default();
|
||||
on_context.run(display_name.clone());
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
"h" | "H" => {
|
||||
ev.prevent_default();
|
||||
on_history.run(());
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
"Escape" => {
|
||||
ev.prevent_default();
|
||||
on_dismiss.run(notif_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.add_event_listener_with_callback(
|
||||
"keydown",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
);
|
||||
let _ = window
|
||||
.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
// Store closure for cleanup on next change
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene};
|
|||
|
||||
#[cfg(feature = "hydrate")]
|
||||
use super::avatar_canvas::hit_test_canvas;
|
||||
use super::avatar_canvas::{member_key, AvatarCanvas};
|
||||
use super::avatar_canvas::{AvatarCanvas, member_key};
|
||||
use super::chat_types::ActiveBubble;
|
||||
use super::context_menu::{ContextMenu, ContextMenuItem};
|
||||
use super::settings::{
|
||||
calculate_min_zoom, ViewerSettings, BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH,
|
||||
BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH, ViewerSettings, calculate_min_zoom,
|
||||
};
|
||||
use super::ws_client::FadingMember;
|
||||
use crate::utils::parse_bounds_dimensions;
|
||||
|
|
@ -35,18 +35,12 @@ use crate::utils::parse_bounds_dimensions;
|
|||
#[component]
|
||||
pub fn RealmSceneViewer(
|
||||
scene: Scene,
|
||||
#[allow(unused)]
|
||||
realm_slug: String,
|
||||
#[prop(into)]
|
||||
members: Signal<Vec<ChannelMemberWithAvatar>>,
|
||||
#[prop(into)]
|
||||
active_bubbles: Signal<HashMap<(Option<Uuid>, Option<Uuid>), ActiveBubble>>,
|
||||
#[prop(into)]
|
||||
loose_props: Signal<Vec<LooseProp>>,
|
||||
#[prop(into)]
|
||||
on_move: Callback<(f64, f64)>,
|
||||
#[prop(into)]
|
||||
on_prop_click: Callback<Uuid>,
|
||||
#[prop(into)] members: Signal<Vec<ChannelMemberWithAvatar>>,
|
||||
#[prop(into)] active_bubbles: Signal<HashMap<(Option<Uuid>, Option<Uuid>), ActiveBubble>>,
|
||||
#[prop(into)] loose_props: Signal<Vec<LooseProp>>,
|
||||
#[prop(into)] on_move: Callback<(f64, f64)>,
|
||||
#[prop(into)] on_prop_click: Callback<Uuid>,
|
||||
/// Viewer settings for pan/zoom/enlarge modes.
|
||||
#[prop(optional)]
|
||||
settings: Option<Signal<ViewerSettings>>,
|
||||
|
|
@ -104,9 +98,7 @@ pub fn RealmSceneViewer(
|
|||
.clone()
|
||||
.unwrap_or_else(|| "#1a1a2e".to_string());
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let has_background_image = scene.background_image_path.is_some();
|
||||
#[allow(unused_variables)]
|
||||
let image_path = scene.background_image_path.clone().unwrap_or_default();
|
||||
|
||||
// Canvas refs for background and props layers
|
||||
|
|
@ -217,15 +209,18 @@ pub fn RealmSceneViewer(
|
|||
// Parse the member ID to determine if it's a user_id or guest_session_id
|
||||
if let Ok(member_id) = member_id_str.parse::<Uuid>() {
|
||||
// Check if this is the current user's avatar
|
||||
let is_current_user = my_user_id == Some(member_id) ||
|
||||
my_guest_session_id == Some(member_id);
|
||||
let is_current_user = my_user_id == Some(member_id)
|
||||
|| my_guest_session_id == Some(member_id);
|
||||
|
||||
if !is_current_user {
|
||||
// Find the display name for this member
|
||||
let display_name = members.get().iter()
|
||||
let display_name = members
|
||||
.get()
|
||||
.iter()
|
||||
.find(|m| {
|
||||
m.member.user_id == Some(member_id) ||
|
||||
m.member.guest_session_id == Some(member_id)
|
||||
m.member.user_id == Some(member_id)
|
||||
|| m.member.guest_session_id
|
||||
== Some(member_id)
|
||||
})
|
||||
.map(|m| m.member.display_name.clone());
|
||||
|
||||
|
|
@ -256,7 +251,7 @@ pub fn RealmSceneViewer(
|
|||
{
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use wasm_bindgen::{JsCast, closure::Closure};
|
||||
|
||||
let image_path_clone = image_path.clone();
|
||||
let bg_color_clone = bg_color.clone();
|
||||
|
|
@ -395,7 +390,8 @@ pub fn RealmSceneViewer(
|
|||
canvas_width as f64,
|
||||
canvas_height as f64,
|
||||
);
|
||||
}) as Box<dyn FnOnce()>);
|
||||
})
|
||||
as Box<dyn FnOnce()>);
|
||||
|
||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
||||
onload.forget();
|
||||
|
|
@ -419,7 +415,8 @@ pub fn RealmSceneViewer(
|
|||
let canvas_aspect = display_width as f64 / display_height as f64;
|
||||
let scene_aspect = scene_width_f / scene_height_f;
|
||||
|
||||
let (draw_width, draw_height, draw_x, draw_y) = if canvas_aspect > scene_aspect {
|
||||
let (draw_width, draw_height, draw_x, draw_y) = if canvas_aspect > scene_aspect
|
||||
{
|
||||
let h = display_height as f64;
|
||||
let w = h * scene_aspect;
|
||||
let x = (display_width as f64 - w) / 2.0;
|
||||
|
|
@ -462,9 +459,14 @@ pub fn RealmSceneViewer(
|
|||
|
||||
let onload = Closure::once(Box::new(move || {
|
||||
let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
|
||||
&img_clone, draw_x, draw_y, draw_width, draw_height,
|
||||
&img_clone,
|
||||
draw_x,
|
||||
draw_y,
|
||||
draw_width,
|
||||
draw_height,
|
||||
);
|
||||
}) as Box<dyn FnOnce()>);
|
||||
})
|
||||
as Box<dyn FnOnce()>);
|
||||
|
||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
||||
onload.forget();
|
||||
|
|
@ -498,6 +500,12 @@ pub fn RealmSceneViewer(
|
|||
return;
|
||||
}
|
||||
|
||||
// Read scale factors inside the Effect (reactive context) before the closure
|
||||
let sx = scale_x.get();
|
||||
let sy = scale_y.get();
|
||||
let ox = offset_x.get();
|
||||
let oy = offset_y.get();
|
||||
|
||||
let Some(canvas) = props_canvas_ref.get() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -520,12 +528,6 @@ pub fn RealmSceneViewer(
|
|||
// Clear with transparency
|
||||
ctx.clear_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64);
|
||||
|
||||
// Get stored scale factors
|
||||
let sx = scale_x.get();
|
||||
let sy = scale_y.get();
|
||||
let ox = offset_x.get();
|
||||
let oy = offset_y.get();
|
||||
|
||||
// Calculate prop size based on mode
|
||||
let prop_size = calculate_prop_size(
|
||||
current_pan_mode,
|
||||
|
|
@ -582,7 +584,9 @@ pub fn RealmSceneViewer(
|
|||
if canvas_width > 0 && canvas_height > 0 {
|
||||
if let Some(canvas) = props_canvas_ref.get() {
|
||||
let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
|
||||
if canvas_el.width() != canvas_width || canvas_el.height() != canvas_height {
|
||||
if canvas_el.width() != canvas_width
|
||||
|| canvas_el.height() != canvas_height
|
||||
{
|
||||
canvas_el.set_width(canvas_width);
|
||||
canvas_el.set_height(canvas_height);
|
||||
}
|
||||
|
|
@ -631,39 +635,44 @@ pub fn RealmSceneViewer(
|
|||
let last_y_down = last_y.clone();
|
||||
|
||||
// Middle mouse down - start drag
|
||||
let onmousedown = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
// Button 1 is middle mouse button
|
||||
if ev.button() == 1 {
|
||||
is_dragging_down.set(true);
|
||||
last_x_down.set(ev.client_x());
|
||||
last_y_down.set(ev.client_y());
|
||||
let _ = container_for_down.style().set_property("cursor", "grabbing");
|
||||
ev.prevent_default();
|
||||
}
|
||||
});
|
||||
let onmousedown =
|
||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
// Button 1 is middle mouse button
|
||||
if ev.button() == 1 {
|
||||
is_dragging_down.set(true);
|
||||
last_x_down.set(ev.client_x());
|
||||
last_y_down.set(ev.client_y());
|
||||
let _ = container_for_down
|
||||
.style()
|
||||
.set_property("cursor", "grabbing");
|
||||
ev.prevent_default();
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse move - drag scroll
|
||||
let onmousemove = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
if is_dragging_move.get() {
|
||||
let dx = last_x_move.get() - ev.client_x();
|
||||
let dy = last_y_move.get() - ev.client_y();
|
||||
last_x_move.set(ev.client_x());
|
||||
last_y_move.set(ev.client_y());
|
||||
container_for_move.set_scroll_left(container_for_move.scroll_left() + dx);
|
||||
container_for_move.set_scroll_top(container_for_move.scroll_top() + dy);
|
||||
}
|
||||
});
|
||||
let onmousemove =
|
||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
if is_dragging_move.get() {
|
||||
let dx = last_x_move.get() - ev.client_x();
|
||||
let dy = last_y_move.get() - ev.client_y();
|
||||
last_x_move.set(ev.client_x());
|
||||
last_y_move.set(ev.client_y());
|
||||
container_for_move.set_scroll_left(container_for_move.scroll_left() + dx);
|
||||
container_for_move.set_scroll_top(container_for_move.scroll_top() + dy);
|
||||
}
|
||||
});
|
||||
|
||||
let container_for_up = container_el.clone();
|
||||
let is_dragging_up = is_dragging.clone();
|
||||
|
||||
// Mouse up - stop drag
|
||||
let onmouseup = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
|
||||
if is_dragging_up.get() {
|
||||
is_dragging_up.set(false);
|
||||
let _ = container_for_up.style().set_property("cursor", "");
|
||||
}
|
||||
});
|
||||
let onmouseup =
|
||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
|
||||
if is_dragging_up.get() {
|
||||
is_dragging_up.set(false);
|
||||
let _ = container_for_up.style().set_property("cursor", "");
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners
|
||||
let _ = container_el.add_event_listener_with_callback(
|
||||
|
|
@ -674,21 +683,20 @@ pub fn RealmSceneViewer(
|
|||
"mousemove",
|
||||
onmousemove.as_ref().unchecked_ref(),
|
||||
);
|
||||
let _ = container_el.add_event_listener_with_callback(
|
||||
"mouseup",
|
||||
onmouseup.as_ref().unchecked_ref(),
|
||||
);
|
||||
let _ = container_el
|
||||
.add_event_listener_with_callback("mouseup", onmouseup.as_ref().unchecked_ref());
|
||||
|
||||
// Also listen for mouseup on window (in case mouse released outside container)
|
||||
if let Some(window) = web_sys::window() {
|
||||
let is_dragging_window = is_dragging.clone();
|
||||
let container_for_window = container_el.clone();
|
||||
let onmouseup_window = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
|
||||
if is_dragging_window.get() {
|
||||
is_dragging_window.set(false);
|
||||
let _ = container_for_window.style().set_property("cursor", "");
|
||||
}
|
||||
});
|
||||
let onmouseup_window =
|
||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
|
||||
if is_dragging_window.get() {
|
||||
is_dragging_window.set(false);
|
||||
let _ = container_for_window.style().set_property("cursor", "");
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback(
|
||||
"mouseup",
|
||||
onmouseup_window.as_ref().unchecked_ref(),
|
||||
|
|
@ -697,11 +705,12 @@ pub fn RealmSceneViewer(
|
|||
}
|
||||
|
||||
// Prevent context menu on middle click
|
||||
let oncontextmenu = Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
if ev.button() == 1 {
|
||||
ev.prevent_default();
|
||||
}
|
||||
});
|
||||
let oncontextmenu =
|
||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
||||
if ev.button() == 1 {
|
||||
ev.prevent_default();
|
||||
}
|
||||
});
|
||||
let _ = container_el.add_event_listener_with_callback(
|
||||
"auxclick",
|
||||
oncontextmenu.as_ref().unchecked_ref(),
|
||||
|
|
@ -713,7 +722,6 @@ pub fn RealmSceneViewer(
|
|||
onmouseup.forget();
|
||||
oncontextmenu.forget();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Create wheel handler closure for use in view
|
||||
|
|
@ -861,7 +869,8 @@ pub fn RealmSceneViewer(
|
|||
// Create a map of members by key for efficient lookup
|
||||
let members_by_key = Signal::derive(move || {
|
||||
use std::collections::HashMap;
|
||||
sorted_members.get()
|
||||
sorted_members
|
||||
.get()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, m)| (member_key(&m), (idx, m)))
|
||||
|
|
@ -871,7 +880,8 @@ pub fn RealmSceneViewer(
|
|||
// Get the list of member keys - use Memo so it only updates when keys actually change
|
||||
// (not when member data like position changes)
|
||||
let member_keys = Memo::new(move |_| {
|
||||
sorted_members.get()
|
||||
sorted_members
|
||||
.get()
|
||||
.iter()
|
||||
.map(member_key)
|
||||
.collect::<Vec<_>>()
|
||||
|
|
@ -1111,7 +1121,6 @@ fn draw_loose_props(
|
|||
offset_y: f64,
|
||||
prop_size: f64,
|
||||
) {
|
||||
|
||||
for prop in props {
|
||||
let x = prop.position_x * scale_x + offset_x;
|
||||
let y = prop.position_y * scale_y + offset_y;
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ pub fn calculate_min_zoom(
|
|||
viewport_width: f64,
|
||||
viewport_height: f64,
|
||||
) -> f64 {
|
||||
if scene_width <= 0.0
|
||||
|| scene_height <= 0.0
|
||||
|| viewport_width <= 0.0
|
||||
|| viewport_height <= 0.0
|
||||
if scene_width <= 0.0 || scene_height <= 0.0 || viewport_width <= 0.0 || viewport_height <= 0.0
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ use leptos::ev::MouseEvent;
|
|||
use leptos::prelude::*;
|
||||
|
||||
use super::modals::Modal;
|
||||
use super::settings::{calculate_min_zoom, ViewerSettings, ZOOM_MAX, ZOOM_STEP, TEXT_EM_MIN, TEXT_EM_MAX, TEXT_EM_STEP};
|
||||
use super::settings::{
|
||||
TEXT_EM_MAX, TEXT_EM_MIN, TEXT_EM_STEP, ViewerSettings, ZOOM_MAX, ZOOM_STEP, calculate_min_zoom,
|
||||
};
|
||||
use crate::utils::LocalStoragePersist;
|
||||
|
||||
/// Settings popup component for scene viewer configuration.
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
use leptos::prelude::*;
|
||||
use leptos::reactive::owner::LocalStorage;
|
||||
|
||||
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp};
|
||||
#[cfg(feature = "hydrate")]
|
||||
use chattyness_db::models::EmotionState;
|
||||
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp};
|
||||
use chattyness_db::ws_messages::ClientMessage;
|
||||
#[cfg(feature = "hydrate")]
|
||||
use chattyness_db::ws_messages::{DisconnectReason, ServerMessage, WsConfig};
|
||||
use chattyness_db::ws_messages::{DisconnectReason, ServerMessage};
|
||||
|
||||
use super::chat_types::ChatMessage;
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ pub fn use_channel_websocket(
|
|||
) -> (Signal<WsState>, WsSenderStorage) {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use wasm_bindgen::{JsCast, closure::Closure};
|
||||
use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
|
||||
|
||||
let (ws_state, set_ws_state) = signal(WsState::Disconnected);
|
||||
|
|
@ -100,8 +100,8 @@ pub fn use_channel_websocket(
|
|||
|
||||
// Create a stored sender function (using new_local for WASM single-threaded environment)
|
||||
let ws_ref_for_send = ws_ref.clone();
|
||||
let sender: WsSenderStorage = StoredValue::new_local(Some(Box::new(
|
||||
move |msg: ClientMessage| {
|
||||
let sender: WsSenderStorage =
|
||||
StoredValue::new_local(Some(Box::new(move |msg: ClientMessage| {
|
||||
if let Some(ws) = ws_ref_for_send.borrow().as_ref() {
|
||||
if ws.ready_state() == WebSocket::OPEN {
|
||||
if let Ok(json) = serde_json::to_string(&msg) {
|
||||
|
|
@ -111,8 +111,7 @@ pub fn use_channel_websocket(
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)));
|
||||
})));
|
||||
|
||||
// Effect to manage WebSocket lifecycle
|
||||
let ws_ref_clone = ws_ref.clone();
|
||||
|
|
@ -198,24 +197,38 @@ pub fn use_channel_websocket(
|
|||
|
||||
if let Ok(msg) = serde_json::from_str::<ServerMessage>(&text) {
|
||||
// Check for Welcome message to start heartbeat with server-provided config
|
||||
if let ServerMessage::Welcome { ref config, ref member, .. } = msg {
|
||||
if let ServerMessage::Welcome {
|
||||
ref config,
|
||||
ref member,
|
||||
..
|
||||
} = msg
|
||||
{
|
||||
if !*heartbeat_started_clone.borrow() {
|
||||
*heartbeat_started_clone.borrow_mut() = true;
|
||||
let ping_interval_ms = config.ping_interval_secs * 1000;
|
||||
let ws_ref_ping = ws_ref_for_heartbeat.clone();
|
||||
#[cfg(debug_assertions)]
|
||||
web_sys::console::log_1(
|
||||
&format!("[WS] Starting heartbeat with interval {}ms", ping_interval_ms).into(),
|
||||
&format!(
|
||||
"[WS] Starting heartbeat with interval {}ms",
|
||||
ping_interval_ms
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
let heartbeat = gloo_timers::callback::Interval::new(ping_interval_ms as u32, move || {
|
||||
if let Some(ws) = ws_ref_ping.borrow().as_ref() {
|
||||
if ws.ready_state() == WebSocket::OPEN {
|
||||
if let Ok(json) = serde_json::to_string(&ClientMessage::Ping) {
|
||||
let _ = ws.send_with_str(&json);
|
||||
let heartbeat = gloo_timers::callback::Interval::new(
|
||||
ping_interval_ms as u32,
|
||||
move || {
|
||||
if let Some(ws) = ws_ref_ping.borrow().as_ref() {
|
||||
if ws.ready_state() == WebSocket::OPEN {
|
||||
if let Ok(json) =
|
||||
serde_json::to_string(&ClientMessage::Ping)
|
||||
{
|
||||
let _ = ws.send_with_str(&json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
std::mem::forget(heartbeat);
|
||||
}
|
||||
// Call on_welcome callback with current user info
|
||||
|
|
@ -292,7 +305,7 @@ fn handle_server_message(
|
|||
ServerMessage::Welcome {
|
||||
member: _,
|
||||
members: initial_members,
|
||||
config: _, // Config is handled in the caller for heartbeat setup
|
||||
config: _, // Config is handled in the caller for heartbeat setup
|
||||
} => {
|
||||
*members_vec = initial_members;
|
||||
on_update.run(members_vec.clone());
|
||||
|
|
@ -314,7 +327,9 @@ fn handle_server_message(
|
|||
// Find the member before removing
|
||||
let leaving_member = members_vec
|
||||
.iter()
|
||||
.find(|m| m.member.user_id == user_id && m.member.guest_session_id == guest_session_id)
|
||||
.find(|m| {
|
||||
m.member.user_id == user_id && m.member.guest_session_id == guest_session_id
|
||||
})
|
||||
.cloned();
|
||||
|
||||
// Always remove from active members list
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue