Cover every output from a background shell: the session-lock secondary machinery generalizes to layer surfaces
The runtime already covered outputs beyond the first, but only for `ShellMode::SessionLock`: `locked` created one `ext-session-lock` surface per output, tracked in `lock_extras`, rendered from `App::lock_secondary_view`, excluded from pointer/touch routing and repainted through the shared invalidation path. A background shell had nothing equivalent, so on a multi-monitor session its wallpaper simply did not exist on the extended screens — the compositor had to fake one by cover-scaling the primary's background, wrong scale and wrong crop included. The machinery is shared rather than duplicated. The state and the hook lose their lock-specific names — `lock_extras` becomes `secondaries`, `main_lock_output` becomes `main_output`, `App::lock_secondary_view` becomes `App::secondary_view` — and everything downstream of creation (the draw loop in frame.rs, invalidation, per-surface scale handling, the input exclusion in pointer/touch, redraw scheduling in run.rs) is the same single copy serving both kinds. What is genuinely new is the second creation site: in `new_output`, when the app's shell mode is `Layer( Background )`, every output beyond the main surface's gets its own background layer surface — anchored to all four edges with size delegated to the compositor, exclusive zone 0, no keyboard interactivity — bound to that output at creation and torn down in `output_destroyed`. The main surface's output is now recorded when it materializes so the runtime knows which output not to cover. Layer-shell events learn about secondaries too, and the `configure` branch is not optional: secondaries live outside the focus map, so their configure used to fall into the `None` arm and resize the main surface. `closed` on a secondary now just drops it instead of asking the app to exit. `secondary_view` keeps its default of `None` (surface filled with `background_color`), so nothing changes for apps that do not opt in; the session-lock behaviour is unchanged aside from the renames.
This commit is contained in:
13
src/app.rs
13
src/app.rs
@@ -794,15 +794,16 @@ pub trait App: 'static
|
|||||||
/// returning `true` from [`requested_exit`](Self::requested_exit).
|
/// returning `true` from [`requested_exit`](Self::requested_exit).
|
||||||
fn shell_mode( &self ) -> ShellMode { ShellMode::Window }
|
fn shell_mode( &self ) -> ShellMode { ShellMode::Window }
|
||||||
|
|
||||||
/// Content for the lock surfaces the runtime creates on every output
|
/// Content for the surfaces the runtime creates on every output beyond
|
||||||
/// beyond the first while in [`ShellMode::SessionLock`]. The compositor
|
/// the first. In [`ShellMode::SessionLock`] these are lock surfaces —
|
||||||
/// blanks any output without a lock surface, so the runtime covers each
|
/// the compositor blanks any output without one, so each is covered. In
|
||||||
/// one; this view is what gets painted there (typically the same
|
/// [`ShellMode::Layer`] on [`Layer::Background`] they are background
|
||||||
/// wallpaper as the main surface, without the interactive form).
|
/// layer surfaces, so a shell's wallpaper reaches extended outputs
|
||||||
|
/// (typically without the grid or any chrome). They take no input.
|
||||||
/// `width` / `height` are that output's surface size in physical pixels.
|
/// `width` / `height` are that output's surface size in physical pixels.
|
||||||
/// Default `None`: the surface is filled with
|
/// Default `None`: the surface is filled with
|
||||||
/// [`background_color`](Self::background_color).
|
/// [`background_color`](Self::background_color).
|
||||||
fn lock_secondary_view( &self, _width: u32, _height: u32 ) -> Option<Element<Self::Message>>
|
fn secondary_view( &self, _width: u32, _height: u32 ) -> Option<Element<Self::Message>>
|
||||||
{
|
{
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,15 +219,17 @@ pub struct AppData<A: App>
|
|||||||
/// Populated and reconciled by the run loop from [`App::overlays`]. Always
|
/// Populated and reconciled by the run loop from [`App::overlays`]. Always
|
||||||
/// empty until overlay diffing is wired up.
|
/// empty until overlay diffing is wired up.
|
||||||
pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>,
|
pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>,
|
||||||
/// Session-lock surfaces covering every output beyond the first, paired
|
/// Surfaces covering every output beyond the first, paired with the
|
||||||
/// with the output each one belongs to. The compositor blanks any output
|
/// output each one belongs to: session-lock surfaces while locked (the
|
||||||
/// without a lock surface, so one is created per output; these render
|
/// compositor blanks any output without one), background layer surfaces
|
||||||
/// [`App::lock_secondary_view`] and take no input (the input handlers
|
/// for a [`crate::Layer::Background`] shell (the wallpaper reaches
|
||||||
/// find no focus for them, and keyboard falls through to `Main`).
|
/// extended outputs). These render [`App::secondary_view`] and take no
|
||||||
pub lock_extras: Vec<( WlOutput, SurfaceState<A::Message> )>,
|
/// input (the input handlers find no focus for them, and keyboard falls
|
||||||
/// Output the main lock surface was created on, so a later `new_output`
|
/// through to `Main`).
|
||||||
/// for the same output does not add a duplicate lock surface.
|
pub secondaries: Vec<( WlOutput, SurfaceState<A::Message> )>,
|
||||||
pub main_lock_output: Option<WlOutput>,
|
/// Output the main surface was created on, so a later `new_output` for
|
||||||
|
/// the same output does not add a duplicate secondary surface.
|
||||||
|
pub main_output: Option<WlOutput>,
|
||||||
/// Input-transparent child surfaces keyed by their stable
|
/// Input-transparent child surfaces keyed by their stable
|
||||||
/// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`].
|
/// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`].
|
||||||
pub subsurfaces: HashMap<SubsurfaceId, SubsurfaceSlot>,
|
pub subsurfaces: HashMap<SubsurfaceId, SubsurfaceSlot>,
|
||||||
@@ -295,14 +297,14 @@ impl<A: App> AppData<A>
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `wl` is one of the secondary session-lock surfaces. Pointer and
|
/// Whether `wl` is one of the secondary per-output surfaces. Pointer and
|
||||||
/// touch events landing on them must be dropped rather than falling back
|
/// touch events landing on them must be dropped rather than falling back
|
||||||
/// to `Main` — their coordinates would hit-test the main surface's
|
/// to `Main` — their coordinates would hit-test the main surface's
|
||||||
/// widgets. Keyboard keeps the `Main` fallback so typing works wherever
|
/// widgets. Keyboard keeps the `Main` fallback so typing works wherever
|
||||||
/// the compositor puts the lock focus.
|
/// the compositor puts the focus.
|
||||||
pub( crate ) fn is_lock_extra_surface( &self, wl: &WlSurface ) -> bool
|
pub( crate ) fn is_secondary_surface( &self, wl: &WlSurface ) -> bool
|
||||||
{
|
{
|
||||||
self.lock_extras.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) )
|
self.secondaries.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus`
|
/// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus`
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for ( _, ss ) in data.lock_extras.iter_mut()
|
for ( _, ss ) in data.secondaries.iter_mut()
|
||||||
{
|
{
|
||||||
if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; }
|
if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; }
|
||||||
let scale = ss.scale_factor.max( 1 ) as u32;
|
let scale = ss.scale_factor.max( 1 ) as u32;
|
||||||
let view = data.app.lock_secondary_view( ss.width * scale, ss.height * scale )
|
let view = data.app.secondary_view( ss.width * scale, ss.height * scale )
|
||||||
.unwrap_or_else( || crate::spacer().into() );
|
.unwrap_or_else( || crate::spacer().into() );
|
||||||
// No frame callback: these surfaces are static between invalidations,
|
// No frame callback: these surfaces are static between invalidations,
|
||||||
// so there is no per-frame pacing to keep — and a callback that never
|
// so there is no per-frame pacing to keep — and a callback that never
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ impl<A: App> CompositorHandler for AppData<A>
|
|||||||
if new_factor <= 0 { return; }
|
if new_factor <= 0 { return; }
|
||||||
let Some( focus ) = self.focus_for_surface( surface ) else
|
let Some( focus ) = self.focus_for_surface( surface ) else
|
||||||
{
|
{
|
||||||
// Lock extras live outside the focus map.
|
// Secondary per-output surfaces live outside the focus map.
|
||||||
let shm = &self.shm;
|
let shm = &self.shm;
|
||||||
if let Some( ( _, ss ) ) = self.lock_extras.iter_mut()
|
if let Some( ( _, ss ) ) = self.secondaries.iter_mut()
|
||||||
.find( |( _, ss )| ss.surface.try_wl_surface() == Some( surface ) )
|
.find( |( _, ss )| ss.surface.try_wl_surface() == Some( surface ) )
|
||||||
{
|
{
|
||||||
apply_surface_scale( ss, shm, surface, new_factor );
|
apply_surface_scale( ss, shm, surface, new_factor );
|
||||||
@@ -165,6 +165,11 @@ impl<A: App> LayerShellHandler for AppData<A>
|
|||||||
layer: &LayerSurface,
|
layer: &LayerSurface,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
if self.is_secondary_surface( layer.wl_surface() )
|
||||||
|
{
|
||||||
|
self.secondaries.retain( |( _, ss )| ss.surface.try_wl_surface() != Some( layer.wl_surface() ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
match self.focus_for_surface( layer.wl_surface() )
|
match self.focus_for_surface( layer.wl_surface() )
|
||||||
{
|
{
|
||||||
Some( super::SurfaceFocus::Main ) | None =>
|
Some( super::SurfaceFocus::Main ) | None =>
|
||||||
@@ -200,6 +205,17 @@ impl<A: App> LayerShellHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
let ( w, h ) = configure.new_size;
|
let ( w, h ) = configure.new_size;
|
||||||
let ( w, h ) = ( w.max( 1 ), h.max( 1 ) );
|
let ( w, h ) = ( w.max( 1 ), h.max( 1 ) );
|
||||||
|
// Secondaries live outside the focus map; without this branch their
|
||||||
|
// configure would fall into the `None` arm and resize the main
|
||||||
|
// surface instead.
|
||||||
|
for ( _, ss ) in self.secondaries.iter_mut()
|
||||||
|
{
|
||||||
|
if ss.surface.try_wl_surface() == Some( layer.wl_surface() )
|
||||||
|
{
|
||||||
|
ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
match self.focus_for_surface( layer.wl_surface() )
|
match self.focus_for_surface( layer.wl_surface() )
|
||||||
{
|
{
|
||||||
Some( super::SurfaceFocus::Main ) | None =>
|
Some( super::SurfaceFocus::Main ) | None =>
|
||||||
@@ -290,23 +306,56 @@ impl<A: App> OutputHandler for AppData<A>
|
|||||||
// Same for any overlays that were created before an output existed.
|
// Same for any overlays that were created before an output existed.
|
||||||
if let Some( ref layer_shell ) = self.layer_shell
|
if let Some( ref layer_shell ) = self.layer_shell
|
||||||
{
|
{
|
||||||
self.main.surface.materialize( &self.compositor_state, layer_shell, qh, &output );
|
if self.main.surface.materialize( &self.compositor_state, layer_shell, qh, &output )
|
||||||
|
{
|
||||||
|
self.main_output = Some( output.clone() );
|
||||||
|
}
|
||||||
for ss in self.overlays.values_mut()
|
for ss in self.overlays.values_mut()
|
||||||
{
|
{
|
||||||
ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output );
|
ss.surface.materialize( &self.compositor_state, layer_shell, qh, &output );
|
||||||
}
|
}
|
||||||
|
// A background shell covers every output: each one beyond the
|
||||||
|
// main's gets its own background layer surface, painted with
|
||||||
|
// `App::secondary_view` — the wallpaper without any chrome.
|
||||||
|
if self.app.shell_mode() == crate::app::ShellMode::Layer( crate::app::Layer::Background )
|
||||||
|
&& self.main_output.is_some()
|
||||||
|
&& self.main_output.as_ref() != Some( &output )
|
||||||
|
&& !self.secondaries.iter().any( |( o, _ )| o == &output )
|
||||||
|
{
|
||||||
|
use smithay_client_toolkit::shell::wlr_layer;
|
||||||
|
let surface = self.compositor_state.create_surface( qh );
|
||||||
|
let layer_surface = layer_shell.create_layer_surface(
|
||||||
|
qh,
|
||||||
|
surface,
|
||||||
|
wlr_layer::Layer::Background,
|
||||||
|
Some( "ltk-secondary" ),
|
||||||
|
Some( &output ),
|
||||||
|
);
|
||||||
|
layer_surface.set_anchor(
|
||||||
|
wlr_layer::Anchor::TOP | wlr_layer::Anchor::BOTTOM
|
||||||
|
| wlr_layer::Anchor::LEFT | wlr_layer::Anchor::RIGHT,
|
||||||
|
);
|
||||||
|
layer_surface.set_exclusive_zone( 0 );
|
||||||
|
layer_surface.set_keyboard_interactivity( wlr_layer::KeyboardInteractivity::None );
|
||||||
|
layer_surface.set_size( 0, 0 );
|
||||||
|
layer_surface.commit();
|
||||||
|
self.secondaries.push( (
|
||||||
|
output.clone(),
|
||||||
|
super::SurfaceState::new( super::SurfaceKind::Layer( layer_surface ), 0.0, String::new() ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// An output hotplugged while the session is locked would be blanked by
|
// An output hotplugged while the session is locked would be blanked by
|
||||||
// the compositor until it gets a lock surface of its own.
|
// the compositor until it gets a lock surface of its own.
|
||||||
if let Some( ref lock ) = self.session_lock
|
if let Some( ref lock ) = self.session_lock
|
||||||
{
|
{
|
||||||
if matches!( self.main.surface, super::SurfaceKind::Lock( .. ) )
|
if matches!( self.main.surface, super::SurfaceKind::Lock( .. ) )
|
||||||
&& self.main_lock_output.as_ref() != Some( &output )
|
&& self.main_output.as_ref() != Some( &output )
|
||||||
&& !self.lock_extras.iter().any( |( o, _ )| o == &output )
|
&& !self.secondaries.iter().any( |( o, _ )| o == &output )
|
||||||
{
|
{
|
||||||
let surface = self.compositor_state.create_surface( qh );
|
let surface = self.compositor_state.create_surface( qh );
|
||||||
let lock_surface = lock.create_lock_surface( surface, &output, qh );
|
let lock_surface = lock.create_lock_surface( surface, &output, qh );
|
||||||
self.lock_extras.push( (
|
self.secondaries.push( (
|
||||||
output,
|
output,
|
||||||
super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
|
super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
|
||||||
) );
|
) );
|
||||||
@@ -318,10 +367,10 @@ impl<A: App> OutputHandler for AppData<A>
|
|||||||
|
|
||||||
fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle<Self>, output: WlOutput )
|
fn output_destroyed( &mut self, _: &Connection, _: &QueueHandle<Self>, output: WlOutput )
|
||||||
{
|
{
|
||||||
self.lock_extras.retain( |( o, _ )| o != &output );
|
self.secondaries.retain( |( o, _ )| o != &output );
|
||||||
if self.main_lock_output.as_ref() == Some( &output )
|
if self.main_output.as_ref() == Some( &output )
|
||||||
{
|
{
|
||||||
self.main_lock_output = None;
|
self.main_output = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,21 +533,21 @@ impl<A: App> SessionLockHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
// Every output needs its own lock surface — the compositor blanks any
|
// Every output needs its own lock surface — the compositor blanks any
|
||||||
// output left without one. The first hosts the app's main surface;
|
// output left without one. The first hosts the app's main surface;
|
||||||
// the rest render `App::lock_secondary_view`.
|
// the rest render `App::secondary_view`.
|
||||||
let outputs: Vec<WlOutput> = self.output_state.outputs().collect();
|
let outputs: Vec<WlOutput> = self.output_state.outputs().collect();
|
||||||
let mut outputs = outputs.into_iter();
|
let mut outputs = outputs.into_iter();
|
||||||
if let Some( output ) = outputs.next()
|
if let Some( output ) = outputs.next()
|
||||||
{
|
{
|
||||||
let surface = self.compositor_state.create_surface( qh );
|
let surface = self.compositor_state.create_surface( qh );
|
||||||
let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
|
let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
|
||||||
self.main.surface = super::SurfaceKind::Lock( lock_surface );
|
self.main.surface = super::SurfaceKind::Lock( lock_surface );
|
||||||
self.main_lock_output = Some( output );
|
self.main_output = Some( output );
|
||||||
}
|
}
|
||||||
for output in outputs
|
for output in outputs
|
||||||
{
|
{
|
||||||
let surface = self.compositor_state.create_surface( qh );
|
let surface = self.compositor_state.create_surface( qh );
|
||||||
let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
|
let lock_surface = session_lock.create_lock_surface( surface, &output, qh );
|
||||||
self.lock_extras.push( (
|
self.secondaries.push( (
|
||||||
output,
|
output,
|
||||||
super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
|
super::SurfaceState::new( super::SurfaceKind::Lock( lock_surface ), 0.0, String::new() ),
|
||||||
) );
|
) );
|
||||||
@@ -529,7 +578,7 @@ impl<A: App> SessionLockHandler for AppData<A>
|
|||||||
self.on_configure( w, h );
|
self.on_configure( w, h );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for ( _, ss ) in self.lock_extras.iter_mut()
|
for ( _, ss ) in self.secondaries.iter_mut()
|
||||||
{
|
{
|
||||||
if ss.surface.try_wl_surface() == Some( wl )
|
if ss.surface.try_wl_surface() == Some( wl )
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub( super ) fn apply_invalidation<A: App>( data: &mut AppData<A>, scope: Invali
|
|||||||
{
|
{
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
}
|
}
|
||||||
for ( _, ss ) in data.lock_extras.iter_mut()
|
for ( _, ss ) in data.secondaries.iter_mut()
|
||||||
{
|
{
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ pub( super ) fn apply_invalidation<A: App>( data: &mut AppData<A>, scope: Invali
|
|||||||
data.main.request_redraw();
|
data.main.request_redraw();
|
||||||
// Lock extras mirror the main surface's backdrop, so
|
// Lock extras mirror the main surface's backdrop, so
|
||||||
// they follow its invalidations.
|
// they follow its invalidations.
|
||||||
for ( _, ss ) in data.lock_extras.iter_mut()
|
for ( _, ss ) in data.secondaries.iter_mut()
|
||||||
{
|
{
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,8 +320,8 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
|||||||
pending_size_hint_unpin,
|
pending_size_hint_unpin,
|
||||||
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
|
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
|
||||||
overlays: std::collections::HashMap::new(),
|
overlays: std::collections::HashMap::new(),
|
||||||
lock_extras: Vec::new(),
|
secondaries: Vec::new(),
|
||||||
main_lock_output: None,
|
main_output: None,
|
||||||
subsurfaces: std::collections::HashMap::new(),
|
subsurfaces: std::collections::HashMap::new(),
|
||||||
subsurface_gles_canvas: None,
|
subsurface_gles_canvas: None,
|
||||||
pointer_focus: SurfaceFocus::Main,
|
pointer_focus: SurfaceFocus::Main,
|
||||||
@@ -600,7 +600,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
|||||||
// surface qualifies we just loop back to `dispatch(None)` and sleep.
|
// surface qualifies we just loop back to `dispatch(None)` and sleep.
|
||||||
let any_drawable = ( data.main.configured && data.main.needs_redraw && !data.main.frame_pending )
|
let any_drawable = ( data.main.configured && data.main.needs_redraw && !data.main.frame_pending )
|
||||||
|| data.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending )
|
|| data.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending )
|
||||||
|| data.lock_extras.iter().any( |( _, ss )| ss.configured && ss.needs_redraw && !ss.frame_pending );
|
|| data.secondaries.iter().any( |( _, ss )| ss.configured && ss.needs_redraw && !ss.frame_pending );
|
||||||
if any_drawable
|
if any_drawable
|
||||||
{
|
{
|
||||||
// Rebuild while motion is in progress and on the first frame
|
// Rebuild while motion is in progress and on the first frame
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl<A: App> PointerHandler for AppData<A>
|
|||||||
{
|
{
|
||||||
for event in events
|
for event in events
|
||||||
{
|
{
|
||||||
if self.is_lock_extra_surface( &event.surface ) { continue; }
|
if self.is_secondary_surface( &event.surface ) { continue; }
|
||||||
let focus = self.focus_for_surface( &event.surface )
|
let focus = self.focus_for_surface( &event.surface )
|
||||||
.unwrap_or( SurfaceFocus::Main );
|
.unwrap_or( SurfaceFocus::Main );
|
||||||
self.pointer_focus = focus;
|
self.pointer_focus = focus;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl<A: App> TouchHandler for AppData<A>
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
self.last_input_serial = serial;
|
self.last_input_serial = serial;
|
||||||
if self.is_lock_extra_surface( &surface ) { return; }
|
if self.is_secondary_surface( &surface ) { return; }
|
||||||
let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
|
let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
|
||||||
self.touch_focus.insert( id, focus );
|
self.touch_focus.insert( id, focus );
|
||||||
let pos = self.surface( focus ).to_physical( position.0, position.1 );
|
let pos = self.surface( focus ).to_physical( position.0, position.1 );
|
||||||
|
|||||||
Reference in New Issue
Block a user