diff --git a/crates/chattyness-user-ui/src/components.rs b/crates/chattyness-user-ui/src/components.rs index 5dca02f..2b77631 100644 --- a/crates/chattyness-user-ui/src/components.rs +++ b/crates/chattyness-user-ui/src/components.rs @@ -1,5 +1,6 @@ //! Reusable UI components. +pub mod constants; pub mod avatar; pub mod avatar_editor; pub mod canvas_utils; diff --git a/crates/chattyness-user-ui/src/components/avatar.rs b/crates/chattyness-user-ui/src/components/avatar.rs index dd0adea..d03e4eb 100644 --- a/crates/chattyness-user-ui/src/components/avatar.rs +++ b/crates/chattyness-user-ui/src/components/avatar.rs @@ -14,6 +14,12 @@ use uuid::Uuid; use chattyness_db::models::{ChannelMemberWithAvatar, EmotionState}; use super::chat_types::{ActiveBubble, emotion_bubble_colors}; +use super::constants::{ + BASE_TEXT_SCALE, BUBBLE_BORDER_RADIUS_BASE, BUBBLE_BORDER_WIDTH, BUBBLE_FONT_SIZE_BASE, + BUBBLE_GAP_BASE, BUBBLE_LINE_HEIGHT, BUBBLE_MAX_WIDTH_BASE, BUBBLE_PADDING_BASE, + BUBBLE_TAIL_SIZE_BASE, LABEL_FONT_SIZE_BASE, LABEL_Y_OFFSET_BASE, Z_SPEECH_BUBBLE, + Z_USERNAME_LABEL, +}; #[cfg(feature = "hydrate")] pub use super::canvas_utils::hit_test_canvas; @@ -116,8 +122,9 @@ impl SceneTransform { } } -/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4 -const BASE_TEXT_SCALE: f64 = 1.4; +// ============================================================================= +// Content Bounds - Avatar grid content analysis +// ============================================================================= /// Content bounds for a 3x3 avatar grid. /// Tracks which rows/columns contain actual content for centering calculations. @@ -232,6 +239,10 @@ impl ContentBounds { } } +// ============================================================================= +// Canvas Layout - Unified layout context +// ============================================================================= + /// Unified layout context for avatar canvas rendering. /// /// This struct computes all derived layout values once from the inputs, @@ -240,39 +251,39 @@ impl ContentBounds { /// - Avatar positioning within the canvas /// - Coordinate transformations between canvas-local and screen space /// - Username label and speech bubble positioning -#[derive(Clone, Copy)] -struct CanvasLayout { +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct CanvasLayout { // Core dimensions - prop_size: f64, - avatar_size: f64, + pub prop_size: f64, + pub avatar_size: f64, // Content offset from grid center - content_x_offset: f64, + pub content_x_offset: f64, // Text scaling - text_scale: f64, + pub text_scale: f64, // Canvas dimensions - canvas_width: f64, - canvas_height: f64, + pub canvas_width: f64, + pub canvas_height: f64, // Canvas position in screen space - canvas_screen_x: f64, - canvas_screen_y: f64, + pub canvas_screen_x: f64, + pub canvas_screen_y: f64, // Avatar center within canvas (canvas-local coordinates) - avatar_cx: f64, - avatar_cy: f64, + pub avatar_cx: f64, + pub avatar_cy: f64, // Content bounds info for positioning - empty_top_rows: usize, - empty_bottom_rows: usize, + pub empty_top_rows: usize, + pub empty_bottom_rows: usize, // Scene bounds for bubble clamping (screen coordinates) - scene_min_x: f64, - scene_max_x: f64, - scene_min_y: f64, - scene_max_y: f64, + pub scene_min_x: f64, + pub scene_max_x: f64, + pub scene_min_y: f64, + pub scene_max_y: f64, } impl CanvasLayout { @@ -338,19 +349,29 @@ impl CanvasLayout { } /// Content center X in canvas-local coordinates. - fn content_center_x(&self) -> f64 { + pub fn content_center_x(&self) -> f64 { self.avatar_cx + self.content_x_offset } /// Top of avatar in canvas-local coordinates. - fn avatar_top_y(&self) -> f64 { + pub fn avatar_top_y(&self) -> f64 { self.avatar_cy - self.avatar_size / 2.0 } /// Bottom of avatar in canvas-local coordinates. - fn avatar_bottom_y(&self) -> f64 { + pub fn avatar_bottom_y(&self) -> f64 { self.avatar_cy + self.avatar_size / 2.0 } + + /// Top of actual content (accounting for empty rows). + pub fn content_top_y(&self) -> f64 { + self.avatar_top_y() + self.empty_top_rows as f64 * self.prop_size + } + + /// Bottom of actual content (accounting for empty rows). + pub fn content_bottom_y(&self) -> f64 { + self.avatar_bottom_y() - self.empty_bottom_rows as f64 * self.prop_size + } } /// Get a unique key for a member (for Leptos For keying). @@ -359,13 +380,141 @@ pub fn member_key(m: &ChannelMemberWithAvatar) -> Uuid { m.member.user_id } +// ============================================================================= +// Bubble Position +// ============================================================================= + /// Position of speech bubble relative to avatar. -#[derive(Clone, Copy, PartialEq, Debug)] +#[derive(Clone, Copy, PartialEq, Debug, Default)] enum BubblePosition { + #[default] Above, Below, } +// ============================================================================= +// Bubble Style Helper +// ============================================================================= + +/// Parameters for bubble style generation. +struct BubbleStyleParams { + position_type: &'static str, + left: f64, + top: f64, + width_style: String, + opacity: f64, + bg_color: &'static str, + border_color: &'static str, + text_color: &'static str, + tail_offset: f64, + text_scale: f64, +} + +/// Generate the CSS style string for a speech bubble. +fn build_bubble_style(params: &BubbleStyleParams) -> String { + let font_size = BUBBLE_FONT_SIZE_BASE * params.text_scale; + let padding = BUBBLE_PADDING_BASE * params.text_scale; + let tail_size = BUBBLE_TAIL_SIZE_BASE * params.text_scale; + let border_radius = BUBBLE_BORDER_RADIUS_BASE * params.text_scale; + + format!( + "position: {}; \ + left: {}px; \ + top: {}px; \ + {} \ + opacity: {}; \ + --bubble-bg: {}; \ + --bubble-border: {}; \ + --bubble-text: {}; \ + --tail-offset: {}%; \ + --font-size: {}px; \ + --padding: {}px; \ + --tail-size: {}px; \ + --border-radius: {}px; \ + z-index: {}; \ + pointer-events: none;", + params.position_type, + params.left, + params.top, + params.width_style, + params.opacity, + params.bg_color, + params.border_color, + params.text_color, + params.tail_offset, + font_size, + padding, + tail_size, + border_radius, + Z_SPEECH_BUBBLE, + ) +} + +/// Calculate bubble position based on layout and measured dimensions. +/// +/// Returns (left, top, tail_offset, position) in wrapper-relative coordinates, +/// or None if not yet measured. +fn calculate_bubble_position( + layout: &CanvasLayout, + bubble_width: f64, + bubble_height: f64, +) -> (f64, f64, f64, BubblePosition) { + let tail_size = BUBBLE_TAIL_SIZE_BASE * layout.text_scale; + let gap = BUBBLE_GAP_BASE * layout.text_scale; + + // Avatar bounds in canvas/wrapper-local coordinates + let avatar_center_x = layout.content_center_x(); + let avatar_top = layout.content_top_y(); + let avatar_bottom = layout.content_bottom_y(); + + // Convert to screen coordinates for clamping + let avatar_screen_center_x = layout.canvas_screen_x + avatar_center_x; + let avatar_screen_top = layout.canvas_screen_y + avatar_top; + let avatar_screen_bottom = layout.canvas_screen_y + avatar_bottom; + + // Horizontal positioning (in screen coordinates) + let ideal_bubble_screen_left = avatar_screen_center_x - bubble_width / 2.0; + let clamped_bubble_screen_left = ideal_bubble_screen_left + .max(layout.scene_min_x) + .min((layout.scene_max_x - bubble_width).max(layout.scene_min_x)); + + // Vertical positioning - determine above or below + let space_above = avatar_screen_top - layout.scene_min_y; + let space_needed = bubble_height + tail_size + gap; + + let position = if space_above >= space_needed { + BubblePosition::Above + } else { + BubblePosition::Below + }; + + let ideal_bubble_screen_top = match position { + BubblePosition::Above => avatar_screen_top - gap - tail_size - bubble_height, + BubblePosition::Below => avatar_screen_bottom + gap + tail_size, + }; + let clamped_bubble_screen_top = ideal_bubble_screen_top + .max(layout.scene_min_y) + .min((layout.scene_max_y - bubble_height).max(layout.scene_min_y)); + + // Convert back to wrapper-relative coordinates + let bubble_left = clamped_bubble_screen_left - layout.canvas_screen_x; + let bubble_top = clamped_bubble_screen_top - layout.canvas_screen_y; + + // Calculate tail offset (where avatar is relative to bubble) + let tail_offset = if bubble_width > 0.0 { + let avatar_rel_to_bubble = avatar_center_x - bubble_left; + (avatar_rel_to_bubble / bubble_width * 100.0).clamp(10.0, 90.0) + } else { + 50.0 + }; + + (bubble_left, bubble_top, tail_offset, position) +} + +// ============================================================================= +// Avatar Component +// ============================================================================= + /// Individual avatar component. /// /// Renders a single avatar with: @@ -401,17 +550,16 @@ pub fn Avatar( // Disable pointer-events when fading (opacity < 1.0) so fading avatars aren't clickable let pointer_events = if opacity < 1.0 { "none" } else { "auto" }; - // Signal to store computed layout for use by label and bubble - let (layout_signal, set_layout_signal) = signal(None::); - // Bubble measurement state - only SIZE is stored, position is calculated reactively let (bubble_measured_width, set_bubble_measured_width) = signal(0.0_f64); let (bubble_measured_height, set_bubble_measured_height) = signal(0.0_f64); let (bubble_is_measured, set_bubble_is_measured) = signal(false); let (bubble_position, set_bubble_position) = signal(BubblePosition::Above); - // Compute layout reactively - let compute_layout = move || { + // ========================================================================== + // Unified layout computation - single Memo used by all parts of the component + // ========================================================================== + let layout_memo: Memo = Memo::new(move |_| { let m = member.get(); let ps = prop_size.get(); let t = transform.get(); @@ -439,11 +587,13 @@ pub fn Avatar( avatar_screen_y, boundaries, ) - }; + }); + // ========================================================================== // Wrapper style (positions the entire avatar container) + // ========================================================================== let wrapper_style = move || { - let layout = compute_layout(); + let layout = layout_memo.get(); format!( "position: absolute; \ left: 0; top: 0; \ @@ -464,18 +614,15 @@ pub fn Avatar( ) }; - // Canvas style (fills the wrapper) - let canvas_style = "width: 100%; height: 100%;"; - + // ========================================================================== // Label style (positioned relative to wrapper, below avatar) + // ========================================================================== let label_style = move || { - let layout = compute_layout(); - let font_size = 12.0 * layout.text_scale; + let layout = layout_memo.get(); + let font_size = LABEL_FONT_SIZE_BASE * layout.text_scale; // Position at content center X, below content bottom let label_x = layout.content_center_x(); - let label_y = layout.avatar_bottom_y() - - layout.empty_bottom_rows as f64 * layout.prop_size - + 15.0 * layout.text_scale; + let label_y = layout.content_bottom_y() + LABEL_Y_OFFSET_BASE * layout.text_scale; format!( "position: absolute; \ @@ -484,8 +631,8 @@ pub fn Avatar( transform: translateX(-50%); \ font-size: {}px; \ white-space: nowrap; \ - z-index: 99998;", - label_x, label_y, font_size + z-index: {};", + label_x, label_y, font_size, Z_USERNAME_LABEL ) }; @@ -493,12 +640,11 @@ pub fn Avatar( let display_name = move || member.get().member.display_name.clone(); // Compute data-member-id reactively - let data_member_id = move || { - let m = member.get(); - m.member.user_id.to_string() - }; + let data_member_id = move || member.get().member.user_id.to_string(); - // Store references for the canvas drawing effect + // ========================================================================== + // Canvas drawing effect + // ========================================================================== #[cfg(feature = "hydrate")] { use std::cell::RefCell; @@ -521,40 +667,12 @@ pub fn Avatar( // Get current values from signals let m = member.get(); - - let ps = prop_size.get(); - let te = text_em_size.get(); + let layout = layout_memo.get(); let Some(canvas) = canvas_ref.get() else { return; }; - // Calculate content bounds for the avatar - let content_bounds = ContentBounds::from_layers( - &m.avatar.skin_layer, - &m.avatar.clothes_layer, - &m.avatar.accessories_layer, - &m.avatar.emotion_layer, - ); - - // Use passed-in transform and screen bounds (computed once at scene level) - let t = transform.get(); - let boundaries = screen_bounds.get(); - let (avatar_screen_x, avatar_screen_y) = - t.to_screen(m.member.position_x, m.member.position_y); - - let layout = CanvasLayout::new( - &content_bounds, - ps, - te, - avatar_screen_x, - avatar_screen_y, - boundaries, - ); - - // Store layout for use by label and bubble - set_layout_signal.set(Some(layout)); - let canvas_el: &web_sys::HtmlCanvasElement = &canvas; // Set canvas resolution from layout @@ -705,6 +823,10 @@ pub fn Avatar( } }); + // ========================================================================== + // Bubble measurement effect + // ========================================================================== + // Track the message ID we've measured to detect new messages let (last_measured_message_id, set_last_measured_message_id) = signal(Option::::None); @@ -768,77 +890,10 @@ pub fn Avatar( }); } - // Calculate bubble position reactively based on current layout - // This runs whenever the avatar moves, keeping the bubble following the avatar - let calc_bubble_position = move || -> Option<(f64, f64, f64, BubblePosition)> { - let layout = layout_signal.get()?; - let measured = bubble_is_measured.get(); - if !measured { - return None; - } + // ========================================================================== + // Bubble visibility and style + // ========================================================================== - let bubble_width = bubble_measured_width.get(); - let bubble_height = bubble_measured_height.get(); - - if bubble_width <= 0.0 || bubble_height <= 0.0 { - return None; - } - - let tail_size = 8.0 * layout.text_scale; - let gap = 5.0 * layout.text_scale; - - // === AVATAR BOUNDS (in canvas/wrapper-local coordinates) === - let avatar_center_x = layout.content_center_x(); - let avatar_top = - layout.avatar_top_y() + layout.empty_top_rows as f64 * layout.prop_size; - let avatar_bottom = - layout.avatar_bottom_y() - layout.empty_bottom_rows as f64 * layout.prop_size; - - // === CONVERT TO SCENE COORDINATES FOR CLAMPING === - let avatar_screen_center_x = layout.canvas_screen_x + avatar_center_x; - let avatar_screen_top = layout.canvas_screen_y + avatar_top; - let avatar_screen_bottom = layout.canvas_screen_y + avatar_bottom; - - // === HORIZONTAL POSITIONING (in scene coordinates) === - let ideal_bubble_screen_left = avatar_screen_center_x - bubble_width / 2.0; - let clamped_bubble_screen_left = ideal_bubble_screen_left - .max(layout.scene_min_x) - .min(layout.scene_max_x - bubble_width); - - // === VERTICAL POSITIONING === - let space_above = avatar_screen_top - layout.scene_min_y; - let space_needed = bubble_height + tail_size + gap; - - let position = if space_above >= space_needed { - BubblePosition::Above - } else { - BubblePosition::Below - }; - - let ideal_bubble_screen_top = match position { - BubblePosition::Above => avatar_screen_top - gap - tail_size - bubble_height, - BubblePosition::Below => avatar_screen_bottom + gap + tail_size, - }; - let clamped_bubble_screen_top = ideal_bubble_screen_top - .max(layout.scene_min_y) - .min(layout.scene_max_y - bubble_height); - - // === CONVERT BACK TO WRAPPER-RELATIVE COORDINATES === - let bubble_left = clamped_bubble_screen_left - layout.canvas_screen_x; - let bubble_top = clamped_bubble_screen_top - layout.canvas_screen_y; - - // === CALCULATE TAIL OFFSET === - let tail_offset = if bubble_width > 0.0 { - let avatar_rel_to_bubble = avatar_center_x - bubble_left; - (avatar_rel_to_bubble / bubble_width * 100.0).clamp(10.0, 90.0) - } else { - 50.0 - }; - - Some((bubble_left, bubble_top, tail_offset, position)) - }; - - // Bubble visibility check #[cfg(feature = "hydrate")] let is_bubble_visible = move || { let Some(bubble_signal) = active_bubble else { @@ -851,11 +906,8 @@ pub fn Avatar( now < bubble.expires_at }; #[cfg(not(feature = "hydrate"))] - let is_bubble_visible = move || { - active_bubble.map(|s| s.get().is_some()).unwrap_or(false) - }; + let is_bubble_visible = move || active_bubble.map(|s| s.get().is_some()).unwrap_or(false); - // Bubble style let bubble_style = move || { let Some(bubble_signal) = active_bubble else { return "display: none;".to_string(); @@ -863,29 +915,25 @@ pub fn Avatar( let Some(bubble) = bubble_signal.get() else { return "display: none;".to_string(); }; - let Some(layout) = layout_signal.get() else { - return "display: none;".to_string(); - }; + let layout = layout_memo.get(); let (bg_color, border_color, text_color) = emotion_bubble_colors(&bubble.message.emotion); - let max_width = 200.0 * layout.text_scale; - let padding = 8.0 * layout.text_scale; - let tail_size = 8.0 * layout.text_scale; - let font_size = 12.0 * layout.text_scale; - let border_width = 2.0; // CSS border width + let max_width = BUBBLE_MAX_WIDTH_BASE * layout.text_scale; + let font_size = BUBBLE_FONT_SIZE_BASE * layout.text_scale; + let padding = BUBBLE_PADDING_BASE * layout.text_scale; let measured = bubble_is_measured.get(); let m_height = bubble_measured_height.get(); + let m_width = bubble_measured_width.get(); // Detect multi-line: if measured height exceeds what a single line would be - // Single line height = font-size * line-height + padding * 2 + border * 2 - let single_line_height = font_size * 1.5 + padding * 2.0 + border_width * 2.0; + let single_line_height = + font_size * BUBBLE_LINE_HEIGHT + padding * 2.0 + BUBBLE_BORDER_WIDTH * 2.0; let is_multiline = measured && m_height > single_line_height * 1.3; // Width strategy: fit-content for single line, explicit max-width for multi-line - // This ensures long text gets full width while short text shrinks to fit let width_style = if is_multiline { format!("width: {}px;", max_width) } else { @@ -894,10 +942,12 @@ pub fn Avatar( // Get reactive position (recalculates when avatar moves) let (position_type, final_left, final_top, tail_offset, position) = - if let Some((left, top, tail, pos)) = calc_bubble_position() { + if measured && m_width > 0.0 && m_height > 0.0 { + let (left, top, tail, pos) = + calculate_bubble_position(&layout, m_width, m_height); ("absolute", left, top, tail, pos) } else { - // Not yet measured - use fixed positioning off-screen + // Not yet measured - position off-screen for measurement ("fixed", -1000.0, -1000.0, 50.0, BubblePosition::Above) }; @@ -907,36 +957,18 @@ pub fn Avatar( // Store position for bubble_class to use set_bubble_position.set(position); - format!( - "position: {}; \ - left: {}px; \ - top: {}px; \ - {} \ - opacity: {}; \ - --bubble-bg: {}; \ - --bubble-border: {}; \ - --bubble-text: {}; \ - --tail-offset: {}%; \ - --font-size: {}px; \ - --padding: {}px; \ - --tail-size: {}px; \ - --border-radius: {}px; \ - z-index: 99999; \ - pointer-events: none;", + build_bubble_style(&BubbleStyleParams { position_type, - final_left, - final_top, + left: final_left, + top: final_top, width_style, - bubble_opacity, + opacity: bubble_opacity, bg_color, border_color, text_color, tail_offset, - font_size, - padding, - tail_size, - 8.0 * layout.text_scale, - ) + text_scale: layout.text_scale, + }) }; let bubble_class = move || { @@ -961,11 +993,15 @@ pub fn Avatar( .unwrap_or(false) }; + // ========================================================================== + // View + // ========================================================================== + view! {
{display_name} diff --git a/crates/chattyness-user-ui/src/components/constants.rs b/crates/chattyness-user-ui/src/components/constants.rs new file mode 100644 index 0000000..6490bb7 --- /dev/null +++ b/crates/chattyness-user-ui/src/components/constants.rs @@ -0,0 +1,76 @@ +//! Shared constants for UI components. +//! +//! Centralizes magic numbers and configuration values used across multiple components. + +// ============================================================================= +// Text Scaling +// ============================================================================= + +/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4 +pub const BASE_TEXT_SCALE: f64 = 1.4; + +// ============================================================================= +// Z-Index Layers +// ============================================================================= + +/// Z-index for the props container. +pub const Z_PROPS: i32 = 1; + +/// Z-index for individual loose props inside the container. +pub const Z_LOOSE_PROP: i32 = 5; + +/// Z-index for the avatars container. +pub const Z_AVATARS_CONTAINER: i32 = 2; + +/// Z-index for fading avatars (below active avatars). +pub const Z_FADING_AVATAR: i32 = 5; + +/// Base z-index for active avatars (actual z-index is this + sort order). +pub const Z_AVATAR_BASE: i32 = 10; + +/// Z-index for the click overlay (captures scene interactions). +pub const Z_CLICK_OVERLAY: i32 = 100; + +/// Z-index for username labels (above avatars, below bubbles). +pub const Z_USERNAME_LABEL: i32 = 200; + +/// Z-index for speech bubbles (topmost in scene). +pub const Z_SPEECH_BUBBLE: i32 = 201; + +// ============================================================================= +// Bubble Dimensions +// ============================================================================= + +/// Maximum width of speech bubbles (before text scaling). +pub const BUBBLE_MAX_WIDTH_BASE: f64 = 200.0; + +/// Padding inside speech bubbles (before text scaling). +pub const BUBBLE_PADDING_BASE: f64 = 8.0; + +/// Size of the bubble tail/pointer (before text scaling). +pub const BUBBLE_TAIL_SIZE_BASE: f64 = 8.0; + +/// Gap between avatar and bubble (before text scaling). +pub const BUBBLE_GAP_BASE: f64 = 5.0; + +/// Border radius of speech bubbles (before text scaling). +pub const BUBBLE_BORDER_RADIUS_BASE: f64 = 8.0; + +/// Font size for bubble text (before text scaling). +pub const BUBBLE_FONT_SIZE_BASE: f64 = 12.0; + +/// CSS border width for bubbles. +pub const BUBBLE_BORDER_WIDTH: f64 = 2.0; + +/// Line height multiplier for bubble text. +pub const BUBBLE_LINE_HEIGHT: f64 = 1.5; + +// ============================================================================= +// Label Dimensions +// ============================================================================= + +/// Font size for username labels (before text scaling). +pub const LABEL_FONT_SIZE_BASE: f64 = 12.0; + +/// Vertical offset for username label below avatar content (before text scaling). +pub const LABEL_Y_OFFSET_BASE: f64 = 15.0; diff --git a/crates/chattyness-user-ui/src/components/scene_viewer.rs b/crates/chattyness-user-ui/src/components/scene_viewer.rs index 504c0a5..4c3641e 100644 --- a/crates/chattyness-user-ui/src/components/scene_viewer.rs +++ b/crates/chattyness-user-ui/src/components/scene_viewer.rs @@ -23,6 +23,7 @@ use uuid::Uuid; use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene}; use super::avatar::{Avatar, ScreenBounds, SceneTransform, member_key}; +use super::constants::{Z_AVATAR_BASE, Z_AVATARS_CONTAINER, Z_CLICK_OVERLAY, Z_FADING_AVATAR, Z_LOOSE_PROP, Z_PROPS}; #[cfg(feature = "hydrate")] use super::canvas_utils::hit_test_canvas; use super::chat_types::ActiveBubble; @@ -488,7 +489,7 @@ pub fn RealmSceneViewer( style=move || canvas_style(0) aria-hidden="true" /> -
+
{move || { loose_props.get().into_iter().map(|prop| { @@ -501,14 +502,14 @@ pub fn RealmSceneViewer( prop=prop_signal transform=scene_transform base_prop_size=prop_size - z_index=5 + z_index=Z_LOOSE_PROP /> } }).collect_view() }}
-
+
{move || { member_keys.get().into_iter().map(|key| { @@ -516,7 +517,7 @@ pub fn RealmSceneViewer( members_by_key.get().get(&key).map(|(_, m)| m.clone()).expect("member key should exist") }); let z_index_signal = Signal::derive(move || { - members_by_key.get().get(&key).map(|(idx, _)| (*idx as i32) + 10).unwrap_or(10) + members_by_key.get().get(&key).map(|(idx, _)| (*idx as i32) + Z_AVATAR_BASE).unwrap_or(Z_AVATAR_BASE) }); let z = z_index_signal.get_untracked(); // Derive bubble signal for this member @@ -557,7 +558,7 @@ pub fn RealmSceneViewer( member=member_signal transform=scene_transform prop_size=prop_size - z_index=5 + z_index=Z_FADING_AVATAR text_em_size=text_em_size opacity=opacity screen_bounds=screen_bounds @@ -570,7 +571,7 @@ pub fn RealmSceneViewer(