Files
maps/frontend/src/lib.rs
Dongho Kim f3f1a568e2 update
2025-12-29 03:44:27 +09:00

665 lines
33 KiB
Rust

//! Frontend for the maps application
//!
//! This crate provides a WebAssembly-based map renderer using wgpu.
// Module declarations
// Module declarations
pub mod types;
pub mod geo;
pub mod labels;
pub mod pipelines;
// New architecture
pub mod domain;
pub mod repositories;
pub mod services;
// Re-exports/Imports
use std::sync::{Arc, Mutex};
use std::collections::HashSet;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use winit::{
event::{Event, WindowEvent},
event_loop::EventLoop,
window::WindowBuilder,
platform::web::WindowExtWebSys,
};
use web_sys::Window; // Ensure web_sys Window is available if needed, though usually covered by wasm_bindgen
use wgpu::util::DeviceExt;
use crate::domain::camera::Camera;
use crate::domain::state::{AppState, InputState};
use crate::services::tile_service::TileService;
use crate::services::camera_service::CameraService;
use crate::services::render_service::RenderService;
use crate::services::transit_service::TransitService;
use crate::repositories::http_client::HttpClient;
use crate::geo::project;
use crate::labels::update_labels;
use crate::types::TileData;
#[wasm_bindgen(start)]
pub async fn run() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
let _ = console_log::init_with_level(log::Level::Warn);
let event_loop = EventLoop::new().unwrap();
let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap());
// Canvas setup
let win = web_sys::window().unwrap();
let window_doc = win.document().unwrap();
let body = window_doc.body().unwrap();
if let Some(canvas) = window.canvas() {
body.append_child(&canvas).unwrap();
}
// Initialize WGPU
let instance = wgpu::Instance::default();
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}).await.unwrap();
let (device, queue) = adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
},
None,
).await.unwrap();
// Initial Config
let dpr = win.device_pixel_ratio();
let inner_width = win.inner_width().unwrap().as_f64().unwrap();
let inner_height = win.inner_height().unwrap().as_f64().unwrap();
let width = (inner_width * dpr) as u32;
let height = (inner_height * dpr) as u32;
let max_dim = device.limits().max_texture_dimension_2d.min(2048);
let width = width.max(1).min(max_dim);
let height = height.max(1).min(max_dim);
if let Some(canvas) = window.canvas() {
canvas.set_width(width);
canvas.set_height(height);
}
let mut config = surface.get_default_config(&adapter, width, height).unwrap();
surface.configure(&device, &config);
// MSAA
let mut msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Multisampled Texture"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 4,
dimension: wgpu::TextureDimension::D2,
format: config.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let mut msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
// Domain State - Initial view centered on Munich (lat 48.1351, lon 11.5820)
let camera = Arc::new(Mutex::new(Camera {
x: 0.5322, // Munich longitude in Mercator
y: 0.3195, // Munich latitude in Mercator
zoom: 4000.0, // Good city-level zoom
aspect: width as f32 / height as f32,
}));
let mut input = InputState::new();
let state = Arc::new(Mutex::new(AppState::new()));
TransitService::set_global_state(state.clone());
// Label camera tracking
let mut last_label_camera: (f32, f32, f32) = (0.0, 0.0, 0.0); // (x, y, zoom)
// Camera Buffer
let is_dark_init = win.document()
.and_then(|d| d.document_element())
.map(|e| e.get_attribute("data-theme").unwrap_or_default() == "dark")
.unwrap_or(false);
let camera_uniform = camera.lock().unwrap().to_uniform(is_dark_init, false);
let camera_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Camera Buffer"),
contents: bytemuck::cast_slice(&[camera_uniform]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
}
);
let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
],
label: Some("camera_bind_group_layout"),
});
let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &camera_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
}
],
label: Some("camera_bind_group"),
});
// Render Service (Pipelines)
let render_service = RenderService::new(&device, &config.format, &camera_bind_group_layout);
// UI Bindings (Zoom slider, buttons)
{
let camera_clone = camera.clone();
let window_clone = window.clone();
if let Some(slider) = window_doc.get_element_by_id("zoom-slider").and_then(|e| e.dyn_into::<web_sys::HtmlInputElement>().ok()) {
let closure = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |event: web_sys::Event| {
let input = event.target().unwrap().dyn_into::<web_sys::HtmlInputElement>().unwrap();
let val = input.value().parse::<f64>().unwrap_or(50.0);
let new_zoom = CameraService::slider_to_zoom(val);
camera_clone.lock().unwrap().zoom = new_zoom;
window_clone.request_redraw();
});
slider.add_event_listener_with_callback("input", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
let camera_clone = camera.clone();
let window_clone = window.clone();
if let Some(btn) = window_doc.get_element_by_id("btn-zoom-in").and_then(|e| e.dyn_into::<web_sys::HtmlButtonElement>().ok()) {
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
let mut cam = camera_clone.lock().unwrap();
let current_slider = CameraService::zoom_to_slider(cam.zoom);
let new_slider = (current_slider + 5.0).min(100.0);
cam.zoom = CameraService::slider_to_zoom(new_slider);
window_clone.request_redraw();
});
btn.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget();
}
let camera_clone = camera.clone();
let window_clone = window.clone();
if let Some(btn) = window_doc.get_element_by_id("btn-zoom-out").and_then(|e| e.dyn_into::<web_sys::HtmlButtonElement>().ok()) {
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
let mut cam = camera_clone.lock().unwrap();
let current_slider = CameraService::zoom_to_slider(cam.zoom);
let new_slider = (current_slider - 5.0).max(0.0);
cam.zoom = CameraService::slider_to_zoom(new_slider);
window_clone.request_redraw();
});
btn.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget();
}
// Transit button
let state_clone = state.clone();
let window_clone = window.clone();
if let Some(btn) = window_doc.get_element_by_id("btn-transport").and_then(|e| e.dyn_into::<web_sys::HtmlButtonElement>().ok()) {
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
let mut s = state_clone.lock().unwrap();
s.show_transit = !s.show_transit;
let new_state = s.show_transit;
drop(s);
web_sys::console::log_1(&format!("Transit mode: {}", new_state).into());
window_clone.request_redraw();
});
btn.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget();
}
// Location logic moved to closure to avoid complex refactoring right now, but using AppState
let state_clone = state.clone();
let camera_clone = camera.clone();
let window_clone = window.clone();
if let Some(btn) = window_doc.get_element_by_id("btn-location").and_then(|e| e.dyn_into::<web_sys::HtmlButtonElement>().ok()) {
// ... (geolocation logic omitted for brevity in this task, assuming safe to simplify or copy)
// For now, I'll assume users want the functionality.
// I'll copy the location logic logic from the read file to maintain feature parity.
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
let window = web_sys::window().unwrap();
let navigator = window.navigator();
let geolocation = navigator.geolocation().unwrap();
let mut state_guard = state_clone.lock().unwrap();
if let Some(id) = state_guard.watch_id {
geolocation.clear_watch(id);
state_guard.watch_id = None;
state_guard.kalman_filter = None;
state_guard.user_location = None;
window_clone.request_redraw();
return;
}
drop(state_guard);
let camera_clone2 = camera_clone.clone();
let window_clone2 = window_clone.clone();
let state_clone2 = state_clone.clone();
let success_callback = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Position)>::new(move |position: web_sys::Position| {
let coords = position.coords();
let mut state_guard = state_clone2.lock().unwrap();
state_guard.user_location = Some((coords.latitude(), coords.longitude()));
drop(state_guard);
let (x, y) = project(coords.latitude(), coords.longitude());
let mut cam = camera_clone2.lock().unwrap();
cam.x = x;
cam.y = y;
// Zoom to street level (8000.0 is good for navigation)
cam.zoom = 8000.0;
drop(cam);
web_sys::console::log_1(&format!("Location updated: lat={}, lon={}", coords.latitude(), coords.longitude()).into());
window_clone2.request_redraw();
});
let error_callback = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::PositionError)>::new(move |error: web_sys::PositionError| {
web_sys::console::error_1(&format!("Geolocation error: {:?}", error.message()).into());
});
let options = web_sys::PositionOptions::new();
let watch_id = geolocation.watch_position_with_error_callback_and_options(
success_callback.as_ref().unchecked_ref(),
Some(error_callback.as_ref().unchecked_ref()),
&options
).unwrap();
state_clone.lock().unwrap().watch_id = Some(watch_id);
success_callback.forget();
error_callback.forget();
});
btn.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget();
}
}
// Hide loading screen
if let Some(loader) = window_doc.get_element_by_id("loading-screen") {
let _ = loader.class_list().add_1("fade-out");
// Remove after transition
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
let _ = loader.set_attribute("style", "display: none;");
});
window_doc.default_view().unwrap().set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
500,
).unwrap();
closure.forget();
}
// Event Loop
event_loop.run(move |event, elwt| {
elwt.set_control_flow(winit::event_loop::ControlFlow::Wait);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(_) => {
let win = web_sys::window().unwrap();
let dpr = win.device_pixel_ratio();
let inner_width = win.inner_width().unwrap().as_f64().unwrap();
let inner_height = win.inner_height().unwrap().as_f64().unwrap();
let max_dim = device.limits().max_texture_dimension_2d.min(2048);
let width = (inner_width * dpr).max(1.0).min(max_dim as f64) as u32;
let height = (inner_height * dpr).max(1.0).min(max_dim as f64) as u32;
if let Some(canvas) = window.canvas() {
canvas.set_width(width);
canvas.set_height(height);
}
config.width = width;
config.height = height;
surface.configure(&device, &config);
// Recreate MSAA texture for the new size
msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Multisampled Texture"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 4,
dimension: wgpu::TextureDimension::D2,
format: config.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
CameraService::handle_resize(&camera, width, height);
window.request_redraw();
}
WindowEvent::MouseInput { state: button_state, button, .. } => {
CameraService::handle_mouse_input(&mut input, button_state, button);
}
WindowEvent::CursorMoved { position, .. } => {
CameraService::handle_cursor_move(&camera, &mut input, position.x, position.y, config.height as f32);
window.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
CameraService::handle_wheel(&camera, delta);
window.request_redraw();
}
WindowEvent::RedrawRequested => {
let camera_guard = camera.lock().unwrap();
let visible_tiles = TileService::get_visible_tiles(&camera_guard);
drop(camera_guard);
// Tile Fetching orchestration
let mut needs_fetch = Vec::new();
let mut tiles_to_render_set = HashSet::new();
{
let mut state_guard = state.lock().unwrap();
for &tile in &visible_tiles {
tiles_to_render_set.insert(tile);
let mut current_tile = tile;
while let Some(parent) = TileService::get_parent_tile(current_tile.0, current_tile.1, current_tile.2) {
tiles_to_render_set.insert(parent);
current_tile = parent;
}
if !state_guard.loaded_tiles.contains(&tile) && !state_guard.pending_tiles.contains(&tile) {
state_guard.pending_tiles.insert(tile);
needs_fetch.push(tile);
}
}
}
// Process Buffers (Delegated to RenderService via helper or direct call? Logic was inline)
// We need to access state to create buffers.
{
let mut state_guard = state.lock().unwrap();
let tiles_to_process: Vec<(i32, i32, i32)> = state_guard.loaded_tiles.iter()
.filter(|tile| !state_guard.buffers.contains_key(*tile))
.cloned()
.collect();
for tile in tiles_to_process {
// Call RenderService static helper? Or just logic.
// I put logic in RenderService::create_tile_buffers which takes state.
RenderService::create_tile_buffers(&device, &mut state_guard, tile, &render_service.tile_bind_group_layout);
}
}
// Fetching
for (z, x, y) in needs_fetch {
let state_clone = state.clone();
let window_clone = window.clone();
wasm_bindgen_futures::spawn_local(async move {
let url = format!("/api/tiles/{}/{}/{}/all", z, x, y);
let tile_data = if let Some(bytes) = HttpClient::fetch_cached(&url).await {
bincode::deserialize::<TileData>(&bytes).ok()
} else {
None
};
if let Some(data) = tile_data {
// Pre-compute labels from tile data (expensive, but only once per tile)
let labels = crate::labels::extract_labels(&data);
let mut guard = state_clone.lock().unwrap();
guard.nodes.insert((z, x, y), data.nodes);
guard.ways.insert((z, x, y), data.ways);
guard.buildings.insert((z, x, y), data.buildings);
guard.landuse.insert((z, x, y), data.landuse);
guard.water.insert((z, x, y), data.water);
guard.railways.insert((z, x, y), data.railways);
guard.tile_labels.insert((z, x, y), labels);
guard.loaded_tiles.insert((z, x, y));
guard.pending_tiles.remove(&(z, x, y));
window_clone.request_redraw();
} else {
let mut guard = state_clone.lock().unwrap();
guard.pending_tiles.remove(&(z, x, y));
}
});
}
// Collect Buffers
let mut tiles_to_render_vec: Vec<(i32, i32, i32)> = tiles_to_render_set.into_iter().collect();
tiles_to_render_vec.sort_by_key(|(z, _, _)| *z);
let mut tiles_to_render = Vec::new();
{
let state_guard = state.lock().unwrap();
for tile in &tiles_to_render_vec {
if let Some(buffers) = state_guard.buffers.get(tile) {
tiles_to_render.push(buffers.clone());
}
}
}
// Render
let is_dark = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.document_element())
.map(|e| e.get_attribute("data-theme").unwrap_or_default() == "dark")
.unwrap_or(false);
let show_transit = state.lock().unwrap().show_transit;
let camera_uniform_data = camera.lock().unwrap().to_uniform(is_dark, show_transit);
queue.write_buffer(&camera_buffer, 0, bytemuck::cast_slice(&[camera_uniform_data]));
let frame = surface.get_current_texture().unwrap();
let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &msaa_view,
resolve_target: Some(&view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: if is_dark { 0.05 } else { 0.961 }, // #F5F4F0 cream
g: if is_dark { 0.05 } else { 0.957 },
b: if is_dark { 0.05 } else { 0.941 },
a: 1.0,
}),
store: wgpu::StoreOp::Discard,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&render_service.landuse_residential_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.landuse_residential_index_count > 0 {
rpass.set_vertex_buffer(0, buffers.landuse_residential_vertex_buffer.slice(..));
rpass.draw(0..buffers.landuse_residential_index_count, 0..1);
}
}
rpass.set_pipeline(&render_service.landuse_green_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.landuse_green_index_count > 0 {
rpass.set_vertex_buffer(0, buffers.landuse_green_vertex_buffer.slice(..));
rpass.draw(0..buffers.landuse_green_index_count, 0..1);
}
}
rpass.set_pipeline(&render_service.water_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.water_index_count > 0 {
rpass.set_bind_group(1, &buffers.tile_bind_group, &[]);
rpass.set_vertex_buffer(0, buffers.water_vertex_buffer.slice(..));
rpass.draw(0..buffers.water_index_count, 0..1);
}
}
rpass.set_pipeline(&render_service.water_line_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.water_line_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.water_line_vertex_buffer.slice(..));
rpass.draw(0..buffers.water_line_vertex_count, 0..1);
}
}
rpass.set_pipeline(&render_service.sand_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.landuse_sand_index_count > 0 {
rpass.set_vertex_buffer(0, buffers.landuse_sand_vertex_buffer.slice(..));
rpass.draw(0..buffers.landuse_sand_index_count, 0..1);
}
}
// Roads (Fill only for now based on extracted render service logic)
rpass.set_pipeline(&render_service.residential_fill);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.road_residential_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.road_residential_vertex_buffer.slice(..));
rpass.draw(0..buffers.road_residential_vertex_count, 0..1);
}
}
rpass.set_pipeline(&render_service.secondary_fill);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.road_secondary_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.road_secondary_vertex_buffer.slice(..));
rpass.draw(0..buffers.road_secondary_vertex_count, 0..1);
}
}
rpass.set_pipeline(&render_service.primary_fill);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.road_primary_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.road_primary_vertex_buffer.slice(..));
rpass.draw(0..buffers.road_primary_vertex_count, 0..1);
}
}
rpass.set_pipeline(&render_service.motorway_fill);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.road_motorway_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.road_motorway_vertex_buffer.slice(..));
rpass.draw(0..buffers.road_motorway_vertex_count, 0..1);
}
}
// Buildings (rendered before railways so transit lines appear on top)
rpass.set_pipeline(&render_service.building_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.building_index_count > 0 {
rpass.set_vertex_buffer(0, buffers.building_vertex_buffer.slice(..));
rpass.draw(0..buffers.building_index_count, 0..1);
}
}
// Railways (rendered LAST so they appear on top of roads and buildings)
rpass.set_pipeline(&render_service.railway_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.railway_vertex_count > 0 {
rpass.set_bind_group(1, &buffers.tile_bind_group, &[]);
rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..));
rpass.draw(0..buffers.railway_vertex_count, 0..1);
}
}
}
queue.submit(Some(encoder.finish()));
frame.present();
// Extract minimal data to avoid RefCell conflicts in winit
// We hold the lock for update_labels to avoid cloning the massive nodes map
let state_guard = state.lock().unwrap();
let camera_guard = camera.lock().unwrap();
// Helper struct to represent camera for update_labels
let temp_camera = Camera {
x: camera_guard.x,
y: camera_guard.y,
zoom: camera_guard.zoom,
aspect: camera_guard.aspect
};
// Update labels every frame for smooth dragging
// Performance is maintained through reduced label limits (20/100/300/500)
update_labels(
&web_sys::window().unwrap(),
&temp_camera,
&state_guard,
config.width as f64,
config.height as f64,
window.scale_factor()
);
let user_location = state_guard.user_location;
let show_transit = state_guard.show_transit;
drop(camera_guard);
drop(state_guard);
// Update user location indicator (blue dot)
if let Some((lat, lon)) = user_location {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
if let Some(marker) = document.get_element_by_id("user-location") {
let (x, y) = project(lat, lon);
let is_dark = document.document_element().map(|e| e.get_attribute("data-theme").unwrap_or_default() == "dark").unwrap_or(false);
let uniforms = temp_camera.to_uniform(is_dark, show_transit);
let cx = x * uniforms.params[0] + uniforms.params[2];
let cy = y * uniforms.params[1] + uniforms.params[3];
// Convert NDC to CSS pixels
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(config.width as f64);
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(config.height as f64);
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
// Update marker position and visibility
let _ = marker.set_attribute("style", &format!(
"display: block; left: {}px; top: {}px; transform: translate(-50%, -50%);",
css_x, css_y
));
}
} else {
// Hide marker if no location
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
if let Some(marker) = document.get_element_by_id("user-location") {
let _ = marker.set_attribute("style", "display: none;");
}
}
}
_ => {}
},
_ => {}
}
}).unwrap();
}