chore: Remove trailing whitespace (#129)
All checks were successful
/ test (push) Successful in 3m42s
All checks were successful
/ test (push) Successful in 3m42s
Switch to the convention of using no trailing whitespace, even on blank lines. Also remove some unintended trailing whitespace on non-blank lines and fix a few typos. Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: #129 Reviewed-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
2c8c09d20d
commit
6c3a48fb52
20 changed files with 335 additions and 340 deletions
|
|
@ -48,11 +48,11 @@ impl SceneSpheres {
|
|||
highlights: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn len_i32(&self) -> i32 {
|
||||
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
|
||||
}
|
||||
|
||||
|
||||
fn push(
|
||||
&mut self, representation: DVector<f64>,
|
||||
color: ElementColor, opacity: f32, highlight: f32,
|
||||
|
|
@ -79,7 +79,7 @@ impl ScenePoints {
|
|||
selections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn push(
|
||||
&mut self, representation: DVector<f64>,
|
||||
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
|
||||
|
|
@ -107,7 +107,7 @@ impl Scene {
|
|||
|
||||
pub trait DisplayItem {
|
||||
fn show(&self, scene: &mut Scene, selected: bool);
|
||||
|
||||
|
||||
// the smallest positive depth, represented as a multiple of `dir`, where
|
||||
// the line generated by `dir` hits the element. returns `None` if the line
|
||||
// misses the element
|
||||
|
|
@ -125,14 +125,14 @@ impl DisplayItem for Sphere {
|
|||
const DEFAULT_OPACITY: f32 = 0.5;
|
||||
const GHOST_OPACITY: f32 = 0.2;
|
||||
const HIGHLIGHT: f32 = 0.2;
|
||||
|
||||
|
||||
let representation = self.representation.get_clone_untracked();
|
||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
|
||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||
scene.spheres.push(representation, color, opacity, highlight);
|
||||
}
|
||||
|
||||
|
||||
// this method should be kept synchronized with `sphere_cast` in
|
||||
// `spheres.frag`, which does essentially the same thing on the GPU side
|
||||
fn cast(
|
||||
|
|
@ -144,12 +144,12 @@ impl DisplayItem for Sphere {
|
|||
// if `a/b` is less than this threshold, we approximate
|
||||
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
||||
const DEG_THRESHOLD: f64 = 1e-9;
|
||||
|
||||
|
||||
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
||||
let a = -rep[3] * dir.norm_squared();
|
||||
let b = rep.rows_range(..3).dot(&dir);
|
||||
let c = -rep[4];
|
||||
|
||||
|
||||
let adjust = 4.0*a*c/(b*b);
|
||||
if adjust < 1.0 {
|
||||
// as long as `b` is non-zero, the linear approximation of
|
||||
|
|
@ -184,14 +184,14 @@ impl DisplayItem for Point {
|
|||
/* SCAFFOLDING */
|
||||
const GHOST_OPACITY: f32 = 0.4;
|
||||
const HIGHLIGHT: f32 = 0.5;
|
||||
|
||||
|
||||
let representation = self.representation.get_clone_untracked();
|
||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
|
||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||
scene.points.push(representation, color, opacity, highlight, selected);
|
||||
}
|
||||
|
||||
|
||||
/* SCAFFOLDING */
|
||||
fn cast(
|
||||
&self,
|
||||
|
|
@ -203,16 +203,16 @@ impl DisplayItem for Point {
|
|||
if rep[2] < 0.0 {
|
||||
// this constant should be kept synchronized with `point.frag`
|
||||
const POINT_RADIUS_PX: f64 = 4.0;
|
||||
|
||||
|
||||
// find the radius of the point in screen projection units
|
||||
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
|
||||
|
||||
|
||||
// find the squared distance between the screen projections of the
|
||||
// ray and the point
|
||||
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
|
||||
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
|
||||
let dist_sq = (dir_proj - rep_proj).norm_squared();
|
||||
|
||||
|
||||
// if the ray hits the point, return its depth
|
||||
if dist_sq < point_radius_proj * point_radius_proj {
|
||||
Some(rep[2] / dir[2])
|
||||
|
|
@ -254,13 +254,13 @@ fn set_up_program(
|
|||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||
fragment_shader_source,
|
||||
);
|
||||
|
||||
|
||||
// create the program and attach the shaders
|
||||
let program = context.create_program().unwrap();
|
||||
context.attach_shader(&program, &vertex_shader);
|
||||
context.attach_shader(&program, &fragment_shader);
|
||||
context.link_program(&program);
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// report whether linking succeeded
|
||||
let link_status = context
|
||||
|
|
@ -273,7 +273,7 @@ fn set_up_program(
|
|||
"Linking failed"
|
||||
};
|
||||
console::log_1(&JsValue::from(link_msg));
|
||||
|
||||
|
||||
program
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ fn load_new_buffer(
|
|||
// create a buffer and bind it to ARRAY_BUFFER
|
||||
let buffer = context.create_buffer();
|
||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
||||
|
||||
|
||||
// load the given data into the buffer. this block is unsafe because
|
||||
// `Float32Array::view` creates a raw view into our module's
|
||||
// `WebAssembly.Memory` buffer. allocating more memory will change the
|
||||
|
|
@ -332,7 +332,7 @@ fn load_new_buffer(
|
|||
WebGl2RenderingContext::STATIC_DRAW,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
|
|
@ -353,11 +353,11 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
|||
let width = rect.width();
|
||||
let height = rect.height();
|
||||
let shortdim = width.min(height);
|
||||
|
||||
|
||||
// this constant should be kept synchronized with `spheres.frag` and
|
||||
// `point.vert`
|
||||
const FOCAL_SLOPE: f64 = 0.3;
|
||||
|
||||
|
||||
(
|
||||
Vector3::new(
|
||||
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
||||
|
|
@ -373,13 +373,13 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
|||
#[component]
|
||||
pub fn Display() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
|
||||
// canvas
|
||||
let display = create_node_ref();
|
||||
|
||||
|
||||
// viewpoint
|
||||
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
|
||||
|
||||
|
||||
// navigation
|
||||
let pitch_up = create_signal(0.0);
|
||||
let pitch_down = create_signal(0.0);
|
||||
|
|
@ -390,7 +390,7 @@ pub fn Display() -> View {
|
|||
let zoom_in = create_signal(0.0);
|
||||
let zoom_out = create_signal(0.0);
|
||||
let turntable = create_signal(false); /* BENCHMARKING */
|
||||
|
||||
|
||||
// manipulation
|
||||
let translate_neg_x = create_signal(0.0);
|
||||
let translate_pos_x = create_signal(0.0);
|
||||
|
|
@ -400,7 +400,7 @@ pub fn Display() -> View {
|
|||
let translate_pos_z = create_signal(0.0);
|
||||
let shrink_neg = create_signal(0.0);
|
||||
let shrink_pos = create_signal(0.0);
|
||||
|
||||
|
||||
// change listener
|
||||
let scene_changed = create_signal(true);
|
||||
create_effect(move || {
|
||||
|
|
@ -413,18 +413,18 @@ pub fn Display() -> View {
|
|||
state.selection.track();
|
||||
scene_changed.set(true);
|
||||
});
|
||||
|
||||
|
||||
/* INSTRUMENTS */
|
||||
const SAMPLE_PERIOD: i32 = 60;
|
||||
let mut last_sample_time = 0.0;
|
||||
let mut frames_since_last_sample = 0;
|
||||
let mean_frame_interval = create_signal(0.0);
|
||||
|
||||
|
||||
let assembly_for_raf = state.assembly.clone();
|
||||
on_mount(move || {
|
||||
// timing
|
||||
let mut last_time = 0.0;
|
||||
|
||||
|
||||
// viewpoint
|
||||
const ROT_SPEED: f64 = 0.4; // in radians per second
|
||||
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
||||
|
|
@ -432,18 +432,18 @@ pub fn Display() -> View {
|
|||
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut location_z: f64 = 5.0;
|
||||
|
||||
|
||||
// manipulation
|
||||
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
|
||||
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
|
||||
|
||||
|
||||
// display parameters
|
||||
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||
|
||||
|
||||
/* INSTRUMENTS */
|
||||
let performance = window().unwrap().performance().unwrap();
|
||||
|
||||
|
||||
// get the display canvas
|
||||
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||
let ctx = canvas
|
||||
|
|
@ -452,28 +452,28 @@ pub fn Display() -> View {
|
|||
.unwrap()
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
|
||||
// disable depth testing
|
||||
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
|
||||
|
||||
|
||||
// set blend mode
|
||||
ctx.enable(WebGl2RenderingContext::BLEND);
|
||||
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
|
||||
// set up the sphere rendering program
|
||||
let sphere_program = set_up_program(
|
||||
&ctx,
|
||||
include_str!("identity.vert"),
|
||||
include_str!("spheres.frag"),
|
||||
);
|
||||
|
||||
|
||||
// set up the point rendering program
|
||||
let point_program = set_up_program(
|
||||
&ctx,
|
||||
include_str!("point.vert"),
|
||||
include_str!("point.frag"),
|
||||
);
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// print the maximum number of vectors that can be passed as
|
||||
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
||||
|
|
@ -490,10 +490,10 @@ pub fn Display() -> View {
|
|||
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
||||
&JsValue::from("uniform vectors available"),
|
||||
);
|
||||
|
||||
|
||||
// find the sphere program's vertex attribute
|
||||
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
|
||||
|
||||
|
||||
// find the sphere program's uniforms
|
||||
const SPHERE_MAX: usize = 200;
|
||||
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
|
||||
|
|
@ -513,7 +513,7 @@ pub fn Display() -> View {
|
|||
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
|
||||
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
|
||||
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
|
||||
|
||||
|
||||
// load the viewport vertex positions into a new vertex buffer object
|
||||
const VERTEX_CNT: usize = 6;
|
||||
let viewport_positions: [f32; 3*VERTEX_CNT] = [
|
||||
|
|
@ -527,20 +527,20 @@ pub fn Display() -> View {
|
|||
1.0, -1.0, 0.0,
|
||||
];
|
||||
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
|
||||
|
||||
|
||||
// find the point program's vertex attributes
|
||||
let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
|
||||
let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
|
||||
let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
|
||||
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
|
||||
|
||||
|
||||
// set up a repainting routine
|
||||
let (_, start_animation_loop, _) = create_raf(move || {
|
||||
// get the time step
|
||||
let time = performance.now();
|
||||
let time_step = 0.001*(time - last_time);
|
||||
last_time = time;
|
||||
|
||||
|
||||
// get the navigation state
|
||||
let pitch_up_val = pitch_up.get();
|
||||
let pitch_down_val = pitch_down.get();
|
||||
|
|
@ -551,7 +551,7 @@ pub fn Display() -> View {
|
|||
let zoom_in_val = zoom_in.get();
|
||||
let zoom_out_val = zoom_out.get();
|
||||
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||
|
||||
|
||||
// get the manipulation state
|
||||
let translate_neg_x_val = translate_neg_x.get();
|
||||
let translate_pos_x_val = translate_pos_x.get();
|
||||
|
|
@ -561,7 +561,7 @@ pub fn Display() -> View {
|
|||
let translate_pos_z_val = translate_pos_z.get();
|
||||
let shrink_neg_val = shrink_neg.get();
|
||||
let shrink_pos_val = shrink_pos.get();
|
||||
|
||||
|
||||
// update the assembly's orientation
|
||||
let ang_vel = {
|
||||
let pitch = pitch_up_val - pitch_down_val;
|
||||
|
|
@ -582,11 +582,11 @@ pub fn Display() -> View {
|
|||
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
||||
);
|
||||
orientation = &rotation * &orientation;
|
||||
|
||||
|
||||
// update the assembly's location
|
||||
let zoom = zoom_out_val - zoom_in_val;
|
||||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||
|
||||
|
||||
// manipulate the assembly
|
||||
/* KLUDGE */
|
||||
// to avoid the complexity of making tangent space projection
|
||||
|
|
@ -642,11 +642,11 @@ pub fn Display() -> View {
|
|||
scene_changed.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if scene_changed.get() {
|
||||
const SPACE_DIM: usize = 3;
|
||||
const COLOR_SIZE: usize = 3;
|
||||
|
||||
|
||||
/* INSTRUMENTS */
|
||||
// measure mean frame interval
|
||||
frames_since_last_sample += 1;
|
||||
|
|
@ -655,11 +655,11 @@ pub fn Display() -> View {
|
|||
last_sample_time = time;
|
||||
frames_since_last_sample = 0;
|
||||
}
|
||||
|
||||
|
||||
// --- get the assembly ---
|
||||
|
||||
|
||||
let mut scene = Scene::new();
|
||||
|
||||
|
||||
// find the map from assembly space to world space
|
||||
let location = {
|
||||
let u = -location_z;
|
||||
|
|
@ -672,7 +672,7 @@ pub fn Display() -> View {
|
|||
])
|
||||
};
|
||||
let asm_to_world = &location * &orientation;
|
||||
|
||||
|
||||
// set up the scene
|
||||
state.assembly.elements.with_untracked(
|
||||
|elts| for elt in elts {
|
||||
|
|
@ -681,26 +681,26 @@ pub fn Display() -> View {
|
|||
}
|
||||
);
|
||||
let sphere_cnt = scene.spheres.len_i32();
|
||||
|
||||
|
||||
// --- draw the spheres ---
|
||||
|
||||
|
||||
// use the sphere rendering program
|
||||
ctx.use_program(Some(&sphere_program));
|
||||
|
||||
|
||||
// enable the sphere program's vertex attribute
|
||||
ctx.enable_vertex_attrib_array(viewport_position_attr);
|
||||
|
||||
|
||||
// write the spheres in world coordinates
|
||||
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|
||||
|rep| (&asm_to_world * rep).cast::<f32>()
|
||||
).collect();
|
||||
|
||||
|
||||
// set the resolution
|
||||
let width = canvas.width() as f32;
|
||||
let height = canvas.height() as f32;
|
||||
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
||||
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
||||
|
||||
|
||||
// pass the scene data
|
||||
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
|
||||
for n in 0..sphere_reps_world.len() {
|
||||
|
|
@ -722,33 +722,33 @@ pub fn Display() -> View {
|
|||
scene.spheres.highlights[n],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// pass the display parameters
|
||||
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||
|
||||
|
||||
// bind the viewport vertex position buffer to the position
|
||||
// attribute in the vertex shader
|
||||
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
|
||||
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||
|
||||
|
||||
// disable the sphere program's vertex attribute
|
||||
ctx.disable_vertex_attrib_array(viewport_position_attr);
|
||||
|
||||
|
||||
// --- draw the points ---
|
||||
|
||||
|
||||
if !scene.points.representations.is_empty() {
|
||||
// use the point rendering program
|
||||
ctx.use_program(Some(&point_program));
|
||||
|
||||
|
||||
// enable the point program's vertex attributes
|
||||
ctx.enable_vertex_attrib_array(point_position_attr);
|
||||
ctx.enable_vertex_attrib_array(point_color_attr);
|
||||
ctx.enable_vertex_attrib_array(point_highlight_attr);
|
||||
ctx.enable_vertex_attrib_array(point_selection_attr);
|
||||
|
||||
|
||||
// write the points in world coordinates
|
||||
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
|
||||
let point_positions = DMatrix::from_columns(
|
||||
|
|
@ -756,7 +756,7 @@ pub fn Display() -> View {
|
|||
|rep| &asm_to_world_sp * rep
|
||||
).collect::<Vec<_>>().as_slice()
|
||||
).cast::<f32>();
|
||||
|
||||
|
||||
// load the point positions and colors into new buffers and
|
||||
// bind them to the corresponding attributes in the vertex
|
||||
// shader
|
||||
|
|
@ -764,22 +764,22 @@ pub fn Display() -> View {
|
|||
bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
|
||||
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
|
||||
|
||||
|
||||
// disable the point program's vertex attributes
|
||||
ctx.disable_vertex_attrib_array(point_position_attr);
|
||||
ctx.disable_vertex_attrib_array(point_color_attr);
|
||||
ctx.disable_vertex_attrib_array(point_highlight_attr);
|
||||
ctx.disable_vertex_attrib_array(point_selection_attr);
|
||||
}
|
||||
|
||||
|
||||
// --- update the display state ---
|
||||
|
||||
|
||||
// update the viewpoint
|
||||
assembly_to_world.set(asm_to_world);
|
||||
|
||||
|
||||
// clear the scene change flag
|
||||
scene_changed.set(
|
||||
pitch_up_val != 0.0
|
||||
|
|
@ -799,7 +799,7 @@ pub fn Display() -> View {
|
|||
});
|
||||
start_animation_loop();
|
||||
});
|
||||
|
||||
|
||||
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
|
||||
let mut navigating = true;
|
||||
let shift = event.shift_key();
|
||||
|
|
@ -819,7 +819,7 @@ pub fn Display() -> View {
|
|||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
|
||||
let mut manipulating = true;
|
||||
let shift = event.shift_key();
|
||||
|
|
@ -838,7 +838,7 @@ pub fn Display() -> View {
|
|||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
view! {
|
||||
/* TO DO */
|
||||
// switch back to integer-valued parameters when that becomes possible
|
||||
|
|
@ -860,7 +860,7 @@ pub fn Display() -> View {
|
|||
yaw_left.set(0.0);
|
||||
pitch_up.set(0.0);
|
||||
pitch_down.set(0.0);
|
||||
|
||||
|
||||
// swap manipulation inputs
|
||||
translate_pos_z.set(translate_neg_y.get());
|
||||
translate_neg_z.set(translate_pos_y.get());
|
||||
|
|
@ -886,7 +886,7 @@ pub fn Display() -> View {
|
|||
roll_ccw.set(0.0);
|
||||
zoom_in.set(0.0);
|
||||
zoom_out.set(0.0);
|
||||
|
||||
|
||||
// swap manipulation inputs
|
||||
translate_pos_y.set(translate_neg_z.get());
|
||||
translate_neg_y.set(translate_pos_z.get());
|
||||
|
|
@ -927,7 +927,7 @@ pub fn Display() -> View {
|
|||
None => (),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// if we clicked something, select it
|
||||
match clicked {
|
||||
Some((elt, _)) => state.select(&elt, event.shift_key()),
|
||||
|
|
@ -936,4 +936,4 @@ pub fn Display() -> View {
|
|||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue