use std::sync::Arc; use crate::{ constants::common::*, window::{ WindowLabel, derive::FloatListInfo, util::{get_monitor_for_window, show_windows_by_label}, }, }; use tauri::{AppHandle, LogicalPosition, LogicalSize, Runtime, WebviewUrl, WebviewWindowBuilder, utils::config::BackgroundThrottlingPolicy}; /// 创建悬浮列表窗口(内部实现) pub async fn create_float_list_window_impl(app: Arc>, is_teacher_muted: bool) -> Result<(), String> { if show_windows_by_label(app.as_ref(), WindowLabel::FLOAT_LIST) { return Ok(()); } // 悬浮列表尺寸 let float_info = calculate_float_info_sync(); // 获取屏幕尺寸,优先为 main 窗口所在屏幕,主窗口没有打开时,才找主屏幕 let monitor = get_monitor_for_window(app.as_ref())?; // 获取屏幕缩放因子 let scale_factor = monitor.scale_factor(); let page_size: LogicalSize = monitor.work_area().size.to_logical(scale_factor); let screen_width = page_size.width; let screen_height = page_size.height; // 构建 URL 时附加状态参数 let url = format!("floating-list-window.html?isTeacherMuted={}", is_teacher_muted); let label = WindowLabel::FLOAT_LIST.as_ref(); // 创建悬浮球窗口,直接在初始化时设置好位置和尺寸 let window = WebviewWindowBuilder::new(app.as_ref(), label, WebviewUrl::App(url.into())) .title("悬浮列表") .inner_size(float_info.width as f64, float_info.height as f64) .max_inner_size(float_info.width as f64, float_info.height as f64) .min_inner_size(float_info.width as f64, float_info.height as f64) .position(screen_width - float_info.width as f64, 0.0) .transparent(true) .visible_on_all_workspaces(true) .background_throttling(BackgroundThrottlingPolicy::Disabled) .decorations(false) .shadow(false) .focusable(true) .focused(false) .accept_first_mouse(true) .skip_taskbar(true) .always_on_top(true) .devtools(true) .build(); let window = match window { Ok(w) => w, Err(e) => { log::error!("{} {}", MSG_FAILED_CREATE_FLOATING_LIST, e); return Err(format!("{} {}", MSG_FAILED_CREATE_FLOATING_LIST, e)); } }; #[cfg(not(target_os = "linux"))] { let _ = window.set_resizable(false); } #[cfg(target_os = "windows")] { let _ = window.set_size(LogicalSize::new(float_info.width as f64, float_info.height as f64)); } // 窗口显示后等待 200 毫秒再执行动画 tokio::spawn(async move { tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; let _ = animate_to_bottom_right(window, screen_width as i32, screen_height as i32).await; }); Ok(()) } /// 关闭悬浮列表窗口(内部实现) pub fn close_float_list_window_impl(app: Arc>) -> Result<(), String> { use tauri::Manager; // 尝试获取悬浮列表窗口 if let Some(window) = app.get_webview_window(WindowLabel::FLOAT_LIST.as_ref()) { // 关闭窗口 window.close().map_err(|e| format!("关闭悬浮列表窗口失败:{}", e))?; log::info!("已成功关闭悬浮列表窗口"); } else { log::warn!("悬浮列表窗口不存在,无需关闭"); } Ok(()) } /// 根据用户登录状态和课程选择情况动态计算弹出窗口高度 pub fn calculate_float_info_sync() -> FloatListInfo { // 默认基础项目数 let item_num = 4; // 每个item高度 let item_height = 64; // 总高度 = item高度 * 项目数 + 额外用于移动按钮和时间部分的高度 let popup_height = item_height * item_num + 60; let popup_width = 60; // 返回计算得到的弹出窗口尺寸 FloatListInfo { width: popup_width as u32, height: popup_height as u32, } } /// 使用抛物线动画将窗口移动到右下角 pub async fn animate_to_bottom_right(window: tauri::WebviewWindow, screen_width: i32, screen_height: i32) -> Result<(), String> { // 获取当前窗口位置和大小 let current_position_physical = window.outer_position().map_err(|e| format!("获取窗口位置失败: {}", e))?; let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?; let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?; // 转换为逻辑位置和大小 let current_position = current_position_physical.to_logical::(scale_factor); let current_size = current_size_physical.to_logical::(scale_factor); let start_x = current_position.x; let start_y = current_position.y; // 计算目标位置(右下角) // 注意:screen_width 和 screen_height 已经在调用方考虑了缩放因子 let end_x = screen_width - current_size.width; let end_y = screen_height - current_size.height - 120; // 计算移动距离 let distance_x = (end_x - start_x) as f64; let distance_y = (end_y - start_y) as f64; // 动画参数 let duration = 800; // 总动画时间(毫秒) let start_time = std::time::Instant::now(); // 抛物线控制参数 // 抛物线顶点高度偏移量(负值表示向下偏移,形成开口向上的抛物线) let apex_offset = -(distance_y.abs() * 0.3).min(200.0); // 执行动画 loop { let elapsed = start_time.elapsed().as_millis() as u64; if elapsed >= duration { break; } let progress = (elapsed as f64) / (duration as f64); // 使用抛物线轨迹计算位置 // 水平方向匀速运动 let current_x = (start_x as f64 + distance_x * progress).round() as i32; // 垂直方向抛物线运动(开口向上,波峰向下) // y = apex_offset * (4*x^2 - 4*x) 形成开口向上的抛物线,谷点在 x=0.5 处 let parabolic_factor = apex_offset * (4.0 * progress * progress - 4.0 * progress); let current_y = (start_y as f64 + distance_y * progress + parabolic_factor).round() as i32; // 更新窗口位置,使用 LogicalPosition if let Err(_e) = window.set_position(LogicalPosition::new(current_x as f64, current_y as f64)) { break; } // 短暂休眠以控制动画帧率 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } // 确保最终位置准确, 再次获取窗口大小和缩放因子 let float_info = calculate_float_info_sync(); let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?; let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?; let current_size = current_size_physical.to_logical::(scale_factor); let _ = window.set_size(LogicalSize::new(float_info.width as f64, float_info.height as f64)); let end_x = screen_width - current_size.width; let end_y = screen_height - current_size.height - 120; if let Err(e) = window.set_position(LogicalPosition::new(end_x as f64, end_y as f64)) { return Err(format!("设置最终窗口位置失败: {}", e)); } Ok(()) }