mirror of
https://github.com/servo/servo.git
synced 2025-08-05 21:50:18 +01:00
Add a spinner for layout
This commit is contained in:
parent
96b9be6c33
commit
204c5b663a
9 changed files with 77 additions and 23 deletions
|
@ -24,9 +24,17 @@ pub struct LayerBufferSet {
|
||||||
buffers: ~[LayerBuffer]
|
buffers: ~[LayerBuffer]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The status of the renderer.
|
||||||
|
#[deriving(Eq)]
|
||||||
|
pub enum RenderState {
|
||||||
|
IdleRenderState,
|
||||||
|
RenderingRenderState,
|
||||||
|
}
|
||||||
|
|
||||||
/// The interface used to by the renderer to acquire draw targets for each rendered frame and
|
/// The interface used to by the renderer to acquire draw targets for each rendered frame and
|
||||||
/// submit them to be drawn to the display.
|
/// submit them to be drawn to the display.
|
||||||
pub trait Compositor {
|
pub trait Compositor {
|
||||||
fn paint(&self, layer_buffer_set: LayerBufferSet, new_size: Size2D<uint>);
|
fn paint(&self, layer_buffer_set: LayerBufferSet, new_size: Size2D<uint>);
|
||||||
|
fn set_render_state(&self, render_state: RenderState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// The task that handles all rendering/painting.
|
// The task that handles all rendering/painting.
|
||||||
|
|
||||||
use azure::AzFloat;
|
use azure::AzFloat;
|
||||||
use compositor::Compositor;
|
use compositor::{Compositor, IdleRenderState, RenderingRenderState};
|
||||||
use font_context::FontContext;
|
use font_context::FontContext;
|
||||||
use geom::matrix2d::Matrix2D;
|
use geom::matrix2d::Matrix2D;
|
||||||
use opts::Opts;
|
use opts::Opts;
|
||||||
|
@ -122,6 +122,7 @@ impl<C: Compositor + Owned> Renderer<C> {
|
||||||
|
|
||||||
fn render(&mut self, render_layer: RenderLayer) {
|
fn render(&mut self, render_layer: RenderLayer) {
|
||||||
debug!("renderer: rendering");
|
debug!("renderer: rendering");
|
||||||
|
self.compositor.set_render_state(RenderingRenderState);
|
||||||
do profile(time::RenderingCategory, self.profiler_chan.clone()) {
|
do profile(time::RenderingCategory, self.profiler_chan.clone()) {
|
||||||
let layer_buffer_set = do render_layers(&render_layer,
|
let layer_buffer_set = do render_layers(&render_layer,
|
||||||
&self.opts,
|
&self.opts,
|
||||||
|
@ -168,6 +169,7 @@ impl<C: Compositor + Owned> Renderer<C> {
|
||||||
|
|
||||||
debug!("renderer: returning surface");
|
debug!("renderer: returning surface");
|
||||||
self.compositor.paint(layer_buffer_set, render_layer.size);
|
self.compositor.paint(layer_buffer_set, render_layer.size);
|
||||||
|
self.compositor.set_render_state(IdleRenderState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use script::script_task::{LoadMsg, ScriptMsg, SendEventMsg};
|
||||||
use windowing::{ApplicationMethods, WindowMethods, WindowMouseEvent, WindowClickEvent};
|
use windowing::{ApplicationMethods, WindowMethods, WindowMouseEvent, WindowClickEvent};
|
||||||
use windowing::{WindowMouseDownEvent, WindowMouseUpEvent};
|
use windowing::{WindowMouseDownEvent, WindowMouseUpEvent};
|
||||||
|
|
||||||
|
use gfx::compositor::RenderState;
|
||||||
use script::dom::event::{Event, ClickEvent, MouseDownEvent, MouseUpEvent};
|
use script::dom::event::{Event, ClickEvent, MouseDownEvent, MouseUpEvent};
|
||||||
use script::compositor_interface::{ReadyState, CompositorInterface};
|
use script::compositor_interface::{ReadyState, CompositorInterface};
|
||||||
use script::compositor_interface;
|
use script::compositor_interface;
|
||||||
|
@ -20,7 +21,7 @@ use core::util;
|
||||||
use geom::matrix::identity;
|
use geom::matrix::identity;
|
||||||
use geom::point::Point2D;
|
use geom::point::Point2D;
|
||||||
use geom::size::Size2D;
|
use geom::size::Size2D;
|
||||||
use gfx::compositor::{Compositor, LayerBufferSet};
|
use gfx::compositor::{Compositor, LayerBufferSet, RenderState};
|
||||||
use layers::layers::{ARGB32Format, BasicImageData, ContainerLayer, ContainerLayerKind, Format};
|
use layers::layers::{ARGB32Format, BasicImageData, ContainerLayer, ContainerLayerKind, Format};
|
||||||
use layers::layers::{Image, ImageData, ImageLayer, ImageLayerKind, RGB24Format, WithDataFn};
|
use layers::layers::{Image, ImageData, ImageLayer, ImageLayerKind, RGB24Format, WithDataFn};
|
||||||
use layers::rendergl;
|
use layers::rendergl;
|
||||||
|
@ -39,8 +40,8 @@ pub struct CompositorTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompositorInterface for CompositorTask {
|
impl CompositorInterface for CompositorTask {
|
||||||
fn send_compositor_msg(&self, msg: ReadyState) {
|
fn set_ready_state(&self, ready_state: ReadyState) {
|
||||||
let msg = ChangeReadyState(msg);
|
let msg = ChangeReadyState(ready_state);
|
||||||
self.chan.send(msg);
|
self.chan.send(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,8 +49,7 @@ impl CompositorInterface for CompositorTask {
|
||||||
impl CompositorTask {
|
impl CompositorTask {
|
||||||
/// Starts the compositor. Returns an interface that can be used to communicate with the
|
/// Starts the compositor. Returns an interface that can be used to communicate with the
|
||||||
/// compositor and a port which allows notification when the compositor shuts down.
|
/// compositor and a port which allows notification when the compositor shuts down.
|
||||||
pub fn new(script_chan: SharedChan<ScriptMsg>,
|
pub fn new(script_chan: SharedChan<ScriptMsg>, profiler_chan: ProfilerChan)
|
||||||
profiler_chan: ProfilerChan)
|
|
||||||
-> (CompositorTask, Port<()>) {
|
-> (CompositorTask, Port<()>) {
|
||||||
let script_chan = Cell(script_chan);
|
let script_chan = Cell(script_chan);
|
||||||
let (shutdown_port, shutdown_chan) = stream();
|
let (shutdown_port, shutdown_chan) = stream();
|
||||||
|
@ -76,8 +76,10 @@ pub enum Msg {
|
||||||
Exit,
|
Exit,
|
||||||
/// Requests that the compositor paint the given layer buffer set for the given page size.
|
/// Requests that the compositor paint the given layer buffer set for the given page size.
|
||||||
Paint(LayerBufferSet, Size2D<uint>),
|
Paint(LayerBufferSet, Size2D<uint>),
|
||||||
/// Alerts the compositor to the current status of page loading
|
/// Alerts the compositor to the current status of page loading.
|
||||||
ChangeReadyState(ReadyState),
|
ChangeReadyState(ReadyState),
|
||||||
|
/// Alerts the compositor to the current status of rendering.
|
||||||
|
ChangeRenderState(RenderState),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Azure surface wrapping to work with the layers infrastructure.
|
/// Azure surface wrapping to work with the layers infrastructure.
|
||||||
|
@ -151,6 +153,7 @@ fn run_main_loop(port: Port<Msg>,
|
||||||
Exit => *done = true,
|
Exit => *done = true,
|
||||||
|
|
||||||
ChangeReadyState(ready_state) => window.set_ready_state(ready_state),
|
ChangeReadyState(ready_state) => window.set_ready_state(ready_state),
|
||||||
|
ChangeRenderState(render_state) => window.set_render_state(render_state),
|
||||||
|
|
||||||
Paint(new_layer_buffer_set, new_size) => {
|
Paint(new_layer_buffer_set, new_size) => {
|
||||||
debug!("osmain: received new frame");
|
debug!("osmain: received new frame");
|
||||||
|
@ -356,6 +359,9 @@ impl Compositor for CompositorTask {
|
||||||
fn paint(&self, layer_buffer_set: LayerBufferSet, new_size: Size2D<uint>) {
|
fn paint(&self, layer_buffer_set: LayerBufferSet, new_size: Size2D<uint>) {
|
||||||
self.chan.send(Paint(layer_buffer_set, new_size))
|
self.chan.send(Paint(layer_buffer_set, new_size))
|
||||||
}
|
}
|
||||||
|
fn set_render_state(&self, render_state: RenderState) {
|
||||||
|
self.chan.send(ChangeRenderState(render_state))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A function for spawning into the platform's main thread.
|
/// A function for spawning into the platform's main thread.
|
||||||
|
|
|
@ -78,7 +78,7 @@ impl Engine {
|
||||||
script_chan.take(),
|
script_chan.take(),
|
||||||
engine_chan_clone.clone(),
|
engine_chan_clone.clone(),
|
||||||
|msg: ReadyState| {
|
|msg: ReadyState| {
|
||||||
compositor_clone.send_compositor_msg(msg)
|
compositor_clone.set_ready_state(msg)
|
||||||
},
|
},
|
||||||
layout_task.clone(),
|
layout_task.clone(),
|
||||||
resource_task.clone(),
|
resource_task.clone(),
|
||||||
|
|
|
@ -38,7 +38,7 @@ use script::layout_interface::{ContentBoxesQuery, ContentBoxesResponse, ExitMsg,
|
||||||
use script::layout_interface::{LayoutResponse, LayoutTask, MatchSelectorsDocumentDamage, Msg};
|
use script::layout_interface::{LayoutResponse, LayoutTask, MatchSelectorsDocumentDamage, Msg};
|
||||||
use script::layout_interface::{QueryMsg, Reflow, ReflowDocumentDamage, ReflowForDisplay};
|
use script::layout_interface::{QueryMsg, Reflow, ReflowDocumentDamage, ReflowForDisplay};
|
||||||
use script::layout_interface::{ReflowMsg};
|
use script::layout_interface::{ReflowMsg};
|
||||||
use script::script_task::{ScriptMsg, SendEventMsg};
|
use script::script_task::{ReflowCompleteMsg, ScriptMsg, SendEventMsg};
|
||||||
use servo_net::image_cache_task::{ImageCacheTask, ImageResponseMsg};
|
use servo_net::image_cache_task::{ImageCacheTask, ImageResponseMsg};
|
||||||
use servo_net::local_image_cache::LocalImageCache;
|
use servo_net::local_image_cache::LocalImageCache;
|
||||||
use servo_util::tree::{TreeNodeRef, TreeUtils};
|
use servo_util::tree::{TreeNodeRef, TreeUtils};
|
||||||
|
@ -255,7 +255,11 @@ impl Layout {
|
||||||
debug!("%?", layout_root.dump());
|
debug!("%?", layout_root.dump());
|
||||||
|
|
||||||
// Tell script that we're done.
|
// Tell script that we're done.
|
||||||
|
//
|
||||||
|
// FIXME(pcwalton): This should probably be *one* channel, but we can't fix this without
|
||||||
|
// either select or a filtered recv() that only looks for messages of a given type.
|
||||||
data.script_join_chan.send(());
|
data.script_join_chan.send(());
|
||||||
|
data.script_chan.send(ReflowCompleteMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a query from the script task. This is the main routine that DOM functions like
|
/// Handles a query from the script task. This is the main routine that DOM functions like
|
||||||
|
|
|
@ -16,10 +16,11 @@ use core::cell::Cell;
|
||||||
use core::libc::c_int;
|
use core::libc::c_int;
|
||||||
use geom::point::Point2D;
|
use geom::point::Point2D;
|
||||||
use geom::size::Size2D;
|
use geom::size::Size2D;
|
||||||
|
use gfx::compositor::{IdleRenderState, RenderState, RenderingRenderState};
|
||||||
use glut::glut::{ACTIVE_CTRL, DOUBLE, HAVE_PRECISE_MOUSE_WHEEL, WindowHeight, WindowWidth};
|
use glut::glut::{ACTIVE_CTRL, DOUBLE, HAVE_PRECISE_MOUSE_WHEEL, WindowHeight, WindowWidth};
|
||||||
use glut::glut;
|
use glut::glut;
|
||||||
use glut::machack;
|
use glut::machack;
|
||||||
use script::compositor_interface::{FinishedLoading, Loading, Rendering, ReadyState};
|
use script::compositor_interface::{FinishedLoading, Loading, PerformingLayout, ReadyState};
|
||||||
|
|
||||||
static THROBBER: [char, ..8] = [ '⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷' ];
|
static THROBBER: [char, ..8] = [ '⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷' ];
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ pub struct Window {
|
||||||
mouse_down_point: @mut Point2D<c_int>,
|
mouse_down_point: @mut Point2D<c_int>,
|
||||||
|
|
||||||
ready_state: ReadyState,
|
ready_state: ReadyState,
|
||||||
|
render_state: RenderState,
|
||||||
throbber_frame: u8,
|
throbber_frame: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +80,7 @@ impl WindowMethods<Application> for Window {
|
||||||
mouse_down_point: @mut Point2D(0, 0),
|
mouse_down_point: @mut Point2D(0, 0),
|
||||||
|
|
||||||
ready_state: FinishedLoading,
|
ready_state: FinishedLoading,
|
||||||
|
render_state: IdleRenderState,
|
||||||
throbber_frame: 0,
|
throbber_frame: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -190,6 +193,12 @@ impl WindowMethods<Application> for Window {
|
||||||
self.ready_state = ready_state;
|
self.ready_state = ready_state;
|
||||||
self.update_window_title()
|
self.update_window_title()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the render state.
|
||||||
|
pub fn set_render_state(@mut self, render_state: RenderState) {
|
||||||
|
self.render_state = render_state;
|
||||||
|
self.update_window_title()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
|
@ -200,10 +209,19 @@ impl Window {
|
||||||
Loading => {
|
Loading => {
|
||||||
glut::set_window_title(self.glut_window, fmt!("%c Loading — Servo", throbber))
|
glut::set_window_title(self.glut_window, fmt!("%c Loading — Servo", throbber))
|
||||||
}
|
}
|
||||||
Rendering => {
|
PerformingLayout => {
|
||||||
glut::set_window_title(self.glut_window, fmt!("%c Rendering — Servo", throbber))
|
glut::set_window_title(self.glut_window,
|
||||||
|
fmt!("%c Performing Layout — Servo", throbber))
|
||||||
|
}
|
||||||
|
FinishedLoading => {
|
||||||
|
match self.render_state {
|
||||||
|
RenderingRenderState => {
|
||||||
|
glut::set_window_title(self.glut_window,
|
||||||
|
fmt!("%c Rendering — Servo", throbber))
|
||||||
|
}
|
||||||
|
IdleRenderState => glut::set_window_title(self.glut_window, "Servo"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FinishedLoading => glut::set_window_title(self.glut_window, "Servo"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
use geom::point::Point2D;
|
use geom::point::Point2D;
|
||||||
use geom::size::Size2D;
|
use geom::size::Size2D;
|
||||||
|
use gfx::compositor::RenderState;
|
||||||
use script::compositor_interface::ReadyState;
|
use script::compositor_interface::ReadyState;
|
||||||
|
|
||||||
pub enum WindowMouseEvent {
|
pub enum WindowMouseEvent {
|
||||||
|
@ -64,5 +65,7 @@ pub trait WindowMethods<A> {
|
||||||
pub fn set_needs_display(@mut self);
|
pub fn set_needs_display(@mut self);
|
||||||
/// Sets the ready state of the current page.
|
/// Sets the ready state of the current page.
|
||||||
pub fn set_ready_state(@mut self, ready_state: ReadyState);
|
pub fn set_ready_state(@mut self, ready_state: ReadyState);
|
||||||
|
/// Sets the render state of the current page.
|
||||||
|
pub fn set_render_state(@mut self, render_state: RenderState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
pub enum ReadyState {
|
pub enum ReadyState {
|
||||||
/// Informs the compositor that a page is loading. Used for setting status
|
/// Informs the compositor that a page is loading. Used for setting status
|
||||||
Loading,
|
Loading,
|
||||||
/// Informs the compositor that a page is rendering. Used for setting status
|
/// Informs the compositor that a page is performing layout. Used for setting status
|
||||||
Rendering,
|
PerformingLayout,
|
||||||
/// Informs the compositor that a page is finished loading. Used for setting status
|
/// Informs the compositor that a page is finished loading. Used for setting status
|
||||||
FinishedLoading,
|
FinishedLoading,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CompositorInterface: Clone {
|
pub trait CompositorInterface : Clone {
|
||||||
fn send_compositor_msg(&self, ReadyState);
|
fn set_ready_state(&self, ReadyState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
/// The script task is the task that owns the DOM in memory, runs JavaScript, and spawns parsing
|
/// The script task is the task that owns the DOM in memory, runs JavaScript, and spawns parsing
|
||||||
/// and layout tasks.
|
/// and layout tasks.
|
||||||
|
|
||||||
use compositor_interface::{ReadyState, Loading, Rendering, FinishedLoading};
|
use compositor_interface::{ReadyState, Loading, PerformingLayout, FinishedLoading};
|
||||||
use dom::bindings::utils::GlobalStaticData;
|
use dom::bindings::utils::GlobalStaticData;
|
||||||
use dom::document::Document;
|
use dom::document::Document;
|
||||||
use dom::element::Element;
|
use dom::element::Element;
|
||||||
|
@ -54,6 +54,8 @@ pub enum ScriptMsg {
|
||||||
SendEventMsg(Event),
|
SendEventMsg(Event),
|
||||||
/// Fires a JavaScript timeout.
|
/// Fires a JavaScript timeout.
|
||||||
FireTimerMsg(~TimerData),
|
FireTimerMsg(~TimerData),
|
||||||
|
/// Notifies script that reflow is finished.
|
||||||
|
ReflowCompleteMsg,
|
||||||
/// Exits the engine.
|
/// Exits the engine.
|
||||||
ExitMsg,
|
ExitMsg,
|
||||||
}
|
}
|
||||||
|
@ -262,6 +264,10 @@ impl ScriptContext {
|
||||||
self.handle_fire_timer_msg(timer_data);
|
self.handle_fire_timer_msg(timer_data);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
ReflowCompleteMsg => {
|
||||||
|
self.handle_reflow_complete_msg();
|
||||||
|
true
|
||||||
|
}
|
||||||
ExitMsg => {
|
ExitMsg => {
|
||||||
self.handle_exit_msg();
|
self.handle_exit_msg();
|
||||||
false
|
false
|
||||||
|
@ -306,6 +312,12 @@ impl ScriptContext {
|
||||||
self.reflow(ReflowForScriptQuery)
|
self.reflow(ReflowForScriptQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles a notification that reflow completed.
|
||||||
|
fn handle_reflow_complete_msg(&mut self) {
|
||||||
|
self.layout_join_port = None;
|
||||||
|
self.set_ready_state(FinishedLoading)
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles a request to exit the script task and shut down layout.
|
/// Handles a request to exit the script task and shut down layout.
|
||||||
fn handle_exit_msg(&mut self) {
|
fn handle_exit_msg(&mut self) {
|
||||||
self.join_layout();
|
self.join_layout();
|
||||||
|
@ -318,7 +330,7 @@ impl ScriptContext {
|
||||||
|
|
||||||
// tells the compositor when loading starts and finishes
|
// tells the compositor when loading starts and finishes
|
||||||
// FIXME ~compositor_interface doesn't work right now, which is why this is necessary
|
// FIXME ~compositor_interface doesn't work right now, which is why this is necessary
|
||||||
fn send_compositor_msg(&self, msg: ReadyState) {
|
fn set_ready_state(&self, msg: ReadyState) {
|
||||||
(self.compositor_task)(msg);
|
(self.compositor_task)(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +345,7 @@ impl ScriptContext {
|
||||||
self.bindings_initialized = true
|
self.bindings_initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_compositor_msg(Loading);
|
self.set_ready_state(Loading);
|
||||||
// Parse HTML.
|
// Parse HTML.
|
||||||
//
|
//
|
||||||
// Note: We can parse the next document in parallel with any previous documents.
|
// Note: We can parse the next document in parallel with any previous documents.
|
||||||
|
@ -374,7 +386,6 @@ impl ScriptContext {
|
||||||
url: url
|
url: url
|
||||||
});
|
});
|
||||||
|
|
||||||
self.send_compositor_msg(Rendering);
|
|
||||||
// Perform the initial reflow.
|
// Perform the initial reflow.
|
||||||
self.damage = Some(DocumentDamage {
|
self.damage = Some(DocumentDamage {
|
||||||
root: root_node,
|
root: root_node,
|
||||||
|
@ -392,7 +403,6 @@ impl ScriptContext {
|
||||||
~"???",
|
~"???",
|
||||||
1);
|
1);
|
||||||
}
|
}
|
||||||
self.send_compositor_msg(FinishedLoading);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a ping to layout and waits for the response. The response will arrive when the
|
/// Sends a ping to layout and waits for the response. The response will arrive when the
|
||||||
|
@ -426,6 +436,9 @@ impl ScriptContext {
|
||||||
// Now, join the layout so that they will see the latest changes we have made.
|
// Now, join the layout so that they will see the latest changes we have made.
|
||||||
self.join_layout();
|
self.join_layout();
|
||||||
|
|
||||||
|
// Tell the user that we're performing layout.
|
||||||
|
self.set_ready_state(PerformingLayout);
|
||||||
|
|
||||||
// Layout will let us know when it's done.
|
// Layout will let us know when it's done.
|
||||||
let (join_port, join_chan) = comm::stream();
|
let (join_port, join_chan) = comm::stream();
|
||||||
self.layout_join_port = Some(join_port);
|
self.layout_join_port = Some(join_port);
|
||||||
|
@ -438,8 +451,8 @@ impl ScriptContext {
|
||||||
document_root: root_frame.document.root,
|
document_root: root_frame.document.root,
|
||||||
url: copy root_frame.url,
|
url: copy root_frame.url,
|
||||||
goal: goal,
|
goal: goal,
|
||||||
script_chan: self.script_chan.clone(),
|
|
||||||
window_size: self.window_size,
|
window_size: self.window_size,
|
||||||
|
script_chan: self.script_chan.clone(),
|
||||||
script_join_chan: join_chan,
|
script_join_chan: join_chan,
|
||||||
damage: replace(&mut self.damage, None).unwrap(),
|
damage: replace(&mut self.damage, None).unwrap(),
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue