mirror of
https://codeberg.org/waydeer/layer-shika.git
synced 2025-12-02 19:15:54 +00:00
feat: dynamic lifecycle output management
This commit is contained in:
parent
5a1c551efb
commit
3e7bc76bb4
5 changed files with 401 additions and 10 deletions
|
|
@ -1,7 +1,8 @@
|
|||
use crate::wayland::surfaces::app_state::AppState;
|
||||
use crate::wayland::surfaces::display_metrics::DisplayMetrics;
|
||||
use crate::wayland::surfaces::surface_state::WindowState;
|
||||
use layer_shika_domain::value_objects::output_info::OutputGeometry;
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::{
|
||||
zwlr_layer_shell_v1::ZwlrLayerShellV1,
|
||||
zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1},
|
||||
|
|
@ -13,6 +14,7 @@ use wayland_client::{
|
|||
wl_compositor::WlCompositor,
|
||||
wl_output::{self, WlOutput},
|
||||
wl_pointer::{self, WlPointer},
|
||||
wl_registry::Event,
|
||||
wl_registry::WlRegistry,
|
||||
wl_seat::WlSeat,
|
||||
wl_surface::WlSurface,
|
||||
|
|
@ -76,7 +78,7 @@ impl Dispatch<WlOutput, ()> for AppState {
|
|||
event: <WlOutput as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
let output_id = proxy.id();
|
||||
let handle = state.get_handle_by_output_id(&output_id);
|
||||
|
|
@ -139,7 +141,24 @@ impl Dispatch<WlOutput, ()> for AppState {
|
|||
}
|
||||
}
|
||||
wl_output::Event::Done => {
|
||||
info!("WlOutput done");
|
||||
info!("WlOutput done for output {:?}", output_id);
|
||||
|
||||
if let Some(manager) = state.output_manager() {
|
||||
let manager_ref = manager.borrow();
|
||||
if manager_ref.has_pending_output(&output_id) {
|
||||
drop(manager_ref);
|
||||
|
||||
info!(
|
||||
"Output {:?} configuration complete, finalizing...",
|
||||
output_id
|
||||
);
|
||||
|
||||
let manager_ref = manager.borrow();
|
||||
if let Err(e) = manager_ref.finalize_output(&output_id, state, qhandle) {
|
||||
info!("Failed to finalize output {:?}: {e}", output_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -251,7 +270,6 @@ impl Dispatch<XdgWmBase, ()> for AppState {
|
|||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
if let xdg_wm_base::Event::Ping { serial } = event {
|
||||
use crate::wayland::surfaces::surface_state::WindowState;
|
||||
WindowState::handle_xdg_wm_base_ping(xdg_wm_base, serial);
|
||||
}
|
||||
}
|
||||
|
|
@ -330,6 +348,57 @@ impl Dispatch<XdgSurface, ()> for AppState {
|
|||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlRegistry, GlobalListContents> for AppState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &WlRegistry,
|
||||
event: <WlRegistry as Proxy>::Event,
|
||||
_data: &GlobalListContents,
|
||||
_conn: &Connection,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} => {
|
||||
if interface == "wl_output" {
|
||||
info!(
|
||||
"Hot-plugged output detected! Binding wl_output with name {name}, version {version}"
|
||||
);
|
||||
|
||||
let output = registry.bind::<WlOutput, _, _>(name, 4.min(version), qhandle, ());
|
||||
let output_id = output.id();
|
||||
|
||||
if let Some(manager) = state.output_manager() {
|
||||
let mut manager_ref = manager.borrow_mut();
|
||||
let handle = manager_ref.register_output(output, qhandle);
|
||||
info!("Registered hot-plugged output with handle {handle:?}");
|
||||
|
||||
state.register_registry_name(name, output_id);
|
||||
} else {
|
||||
info!("No output manager available yet (startup initialization)");
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::GlobalRemove { name } => {
|
||||
info!("Registry global removed: name {name}");
|
||||
|
||||
if let Some(output_id) = state.unregister_registry_name(name) {
|
||||
info!("Output with registry name {name} removed, cleaning up...");
|
||||
|
||||
if let Some(manager) = state.output_manager() {
|
||||
let mut manager_ref = manager.borrow_mut();
|
||||
manager_ref.remove_output(&output_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_empty_dispatch_app {
|
||||
($(($t:ty, $u:ty)),+) => {
|
||||
$(
|
||||
|
|
@ -342,7 +411,7 @@ macro_rules! impl_empty_dispatch_app {
|
|||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
info!("Implement empty dispatch event for {:?}", stringify!($t));
|
||||
debug!("Implement empty dispatch event for {:?}", stringify!($t));
|
||||
}
|
||||
}
|
||||
)+
|
||||
|
|
@ -350,7 +419,6 @@ macro_rules! impl_empty_dispatch_app {
|
|||
}
|
||||
|
||||
impl_empty_dispatch_app!(
|
||||
(WlRegistry, GlobalListContents),
|
||||
(WlCompositor, ()),
|
||||
(WlSurface, ()),
|
||||
(ZwlrLayerShellV1, ()),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ use layer_shika_domain::value_objects::output_handle::OutputHandle;
|
|||
use std::collections::HashMap;
|
||||
use wayland_client::backend::ObjectId;
|
||||
|
||||
pub struct OutputMapping {
|
||||
pub(crate) use output_manager::{OutputManager, OutputManagerContext};
|
||||
pub(crate) mod output_manager;
|
||||
|
||||
pub(crate) struct OutputMapping {
|
||||
object_to_handle: HashMap<ObjectId, OutputHandle>,
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +26,6 @@ impl OutputMapping {
|
|||
self.object_to_handle.get(object_id).copied()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn remove(&mut self, object_id: &ObjectId) -> Option<OutputHandle> {
|
||||
self.object_to_handle.remove(object_id)
|
||||
}
|
||||
|
|
|
|||
240
crates/adapters/src/wayland/outputs/output_manager.rs
Normal file
240
crates/adapters/src/wayland/outputs/output_manager.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
use crate::{
|
||||
errors::{LayerShikaError, Result},
|
||||
rendering::egl::context_factory::RenderContextFactory,
|
||||
wayland::{
|
||||
config::{LayerSurfaceConfig, WaylandWindowConfig},
|
||||
shell_adapter::WaylandWindowingSystem,
|
||||
surfaces::{
|
||||
app_state::AppState,
|
||||
event_context::SharedPointerSerial,
|
||||
layer_surface::{SurfaceCtx, SurfaceSetupParams},
|
||||
popup_manager::{PopupContext, PopupManager},
|
||||
surface_builder::WindowStateBuilder,
|
||||
surface_state::WindowState,
|
||||
},
|
||||
},
|
||||
};
|
||||
use layer_shika_domain::value_objects::{
|
||||
output_handle::OutputHandle,
|
||||
output_info::OutputInfo,
|
||||
};
|
||||
use log::{info, warn};
|
||||
use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1;
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use wayland_client::{
|
||||
backend::ObjectId,
|
||||
protocol::{wl_compositor::WlCompositor, wl_output::WlOutput, wl_pointer::WlPointer},
|
||||
Connection, Proxy, QueueHandle,
|
||||
};
|
||||
use wayland_protocols::{
|
||||
wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1,
|
||||
wp::viewporter::client::wp_viewporter::WpViewporter,
|
||||
};
|
||||
|
||||
use super::OutputMapping;
|
||||
|
||||
pub struct OutputManagerContext {
|
||||
pub compositor: WlCompositor,
|
||||
pub layer_shell: ZwlrLayerShellV1,
|
||||
pub fractional_scale_manager: Option<WpFractionalScaleManagerV1>,
|
||||
pub viewporter: Option<WpViewporter>,
|
||||
pub render_factory: Rc<RenderContextFactory>,
|
||||
pub popup_context: PopupContext,
|
||||
pub pointer: Rc<WlPointer>,
|
||||
pub shared_serial: Rc<SharedPointerSerial>,
|
||||
pub connection: Rc<Connection>,
|
||||
}
|
||||
|
||||
impl OutputManagerContext {
|
||||
pub const fn connection(&self) -> &Rc<Connection> {
|
||||
&self.connection
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingOutput {
|
||||
proxy: WlOutput,
|
||||
#[allow(dead_code)]
|
||||
output_id: ObjectId,
|
||||
info: OutputInfo,
|
||||
}
|
||||
|
||||
pub struct OutputManager {
|
||||
context: OutputManagerContext,
|
||||
config: WaylandWindowConfig,
|
||||
pub(crate) layer_surface_config: LayerSurfaceConfig,
|
||||
output_mapping: OutputMapping,
|
||||
pending_outputs: RefCell<HashMap<ObjectId, PendingOutput>>,
|
||||
}
|
||||
|
||||
impl OutputManager {
|
||||
pub(crate) fn new(
|
||||
context: OutputManagerContext,
|
||||
config: WaylandWindowConfig,
|
||||
layer_surface_config: LayerSurfaceConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
config,
|
||||
layer_surface_config,
|
||||
output_mapping: OutputMapping::new(),
|
||||
pending_outputs: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_output(
|
||||
&mut self,
|
||||
output: WlOutput,
|
||||
_queue_handle: &QueueHandle<AppState>,
|
||||
) -> OutputHandle {
|
||||
let output_id = output.id();
|
||||
let handle = self.output_mapping.insert(output_id.clone());
|
||||
|
||||
info!(
|
||||
"Registered new output with handle {handle:?}, id {:?}",
|
||||
output_id
|
||||
);
|
||||
|
||||
let info = OutputInfo::new(handle);
|
||||
|
||||
self.pending_outputs.borrow_mut().insert(
|
||||
output_id.clone(),
|
||||
PendingOutput {
|
||||
proxy: output,
|
||||
output_id,
|
||||
info,
|
||||
},
|
||||
);
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn finalize_output(
|
||||
&self,
|
||||
output_id: &ObjectId,
|
||||
app_state: &mut AppState,
|
||||
queue_handle: &QueueHandle<AppState>,
|
||||
) -> Result<()> {
|
||||
let mut pending = self.pending_outputs.borrow_mut();
|
||||
|
||||
let Some(pending_output) = pending.remove(output_id) else {
|
||||
return Err(LayerShikaError::InvalidInput {
|
||||
message: format!("No pending output found for id {output_id:?}"),
|
||||
});
|
||||
};
|
||||
|
||||
let handle = pending_output.info.handle();
|
||||
let mut info = pending_output.info;
|
||||
|
||||
let is_primary = app_state.output_registry().is_empty();
|
||||
info.set_primary(is_primary);
|
||||
|
||||
if !self.config.output_policy.should_render(&info) {
|
||||
info!(
|
||||
"Skipping output {:?} due to output policy (primary: {})",
|
||||
output_id, is_primary
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Finalizing output {:?} (handle: {handle:?}, primary: {})",
|
||||
output_id, is_primary
|
||||
);
|
||||
|
||||
let (window, main_surface_id) =
|
||||
self.create_window_for_output(&pending_output.proxy, output_id, queue_handle)?;
|
||||
|
||||
app_state.add_output(output_id.clone(), main_surface_id, window);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_window_for_output(
|
||||
&self,
|
||||
output: &WlOutput,
|
||||
_output_id: &ObjectId,
|
||||
queue_handle: &QueueHandle<AppState>,
|
||||
) -> Result<(WindowState, ObjectId)> {
|
||||
let setup_params = SurfaceSetupParams {
|
||||
compositor: &self.context.compositor,
|
||||
output,
|
||||
layer_shell: &self.context.layer_shell,
|
||||
fractional_scale_manager: self.context.fractional_scale_manager.as_ref(),
|
||||
viewporter: self.context.viewporter.as_ref(),
|
||||
queue_handle,
|
||||
layer: self.config.layer,
|
||||
namespace: self.config.namespace.clone(),
|
||||
};
|
||||
|
||||
let surface_ctx = SurfaceCtx::setup(&setup_params, &self.layer_surface_config);
|
||||
let main_surface_id = surface_ctx.surface.id();
|
||||
|
||||
let window = WaylandWindowingSystem::initialize_renderer(
|
||||
&surface_ctx.surface,
|
||||
&self.config,
|
||||
&self.context.render_factory,
|
||||
)?;
|
||||
|
||||
let mut builder = WindowStateBuilder::new()
|
||||
.with_component_definition(self.config.component_definition.clone())
|
||||
.with_compilation_result(self.config.compilation_result.clone())
|
||||
.with_surface(Rc::clone(&surface_ctx.surface))
|
||||
.with_layer_surface(Rc::clone(&surface_ctx.layer_surface))
|
||||
.with_scale_factor(self.config.scale_factor)
|
||||
.with_height(self.config.height)
|
||||
.with_exclusive_zone(self.config.exclusive_zone)
|
||||
.with_connection(Rc::clone(self.context.connection()))
|
||||
.with_pointer(Rc::clone(&self.context.pointer))
|
||||
.with_window(Rc::clone(&window));
|
||||
|
||||
if let Some(fs) = &surface_ctx.fractional_scale {
|
||||
builder = builder.with_fractional_scale(Rc::clone(fs));
|
||||
}
|
||||
|
||||
if let Some(vp) = &surface_ctx.viewport {
|
||||
builder = builder.with_viewport(Rc::clone(vp));
|
||||
}
|
||||
|
||||
let mut window_state =
|
||||
WindowState::new(builder).map_err(|e| LayerShikaError::WindowConfiguration {
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
let popup_manager = Rc::new(PopupManager::new(
|
||||
self.context.popup_context.clone(),
|
||||
Rc::clone(window_state.display_metrics()),
|
||||
));
|
||||
|
||||
window_state.set_popup_manager(Rc::clone(&popup_manager));
|
||||
window_state.set_shared_pointer_serial(Rc::clone(&self.context.shared_serial));
|
||||
|
||||
Ok((window_state, main_surface_id))
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, output_id: &ObjectId, app_state: &mut AppState) {
|
||||
if let Some(handle) = self.output_mapping.remove(output_id) {
|
||||
info!("Removing output {handle:?} (id: {output_id:?})");
|
||||
|
||||
if let Some(_window) = app_state.remove_output(handle) {
|
||||
info!("Cleaned up window for output {handle:?}");
|
||||
} else {
|
||||
warn!("No window found for output handle {handle:?}");
|
||||
}
|
||||
} else {
|
||||
self.pending_outputs.borrow_mut().remove(output_id);
|
||||
info!("Removed pending output {output_id:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_handle_by_output_id(&self, output_id: &ObjectId) -> Option<OutputHandle> {
|
||||
self.output_mapping.get(output_id)
|
||||
}
|
||||
|
||||
pub fn has_pending_output(&self, output_id: &ObjectId) -> bool {
|
||||
self.pending_outputs.borrow().contains_key(output_id)
|
||||
}
|
||||
|
||||
pub fn pending_output_count(&self) -> usize {
|
||||
self.pending_outputs.borrow().len()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ use crate::wayland::{
|
|||
config::{LayerSurfaceConfig, WaylandWindowConfig},
|
||||
globals::context::GlobalContext,
|
||||
managed_proxies::ManagedWlPointer,
|
||||
outputs::{OutputManager, OutputManagerContext},
|
||||
surfaces::layer_surface::{SurfaceCtx, SurfaceSetupParams},
|
||||
surfaces::popup_manager::{PopupContext, PopupManager},
|
||||
surfaces::{
|
||||
|
|
@ -34,6 +35,7 @@ use slint_interpreter::ComponentInstance;
|
|||
use smithay_client_toolkit::reexports::calloop::{
|
||||
EventLoop, Interest, LoopHandle, Mode, PostAction, generic::Generic,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wayland_client::{
|
||||
Connection, EventQueue, Proxy, QueueHandle,
|
||||
|
|
@ -50,6 +52,17 @@ struct OutputSetup {
|
|||
builder: WindowStateBuilder,
|
||||
}
|
||||
|
||||
struct OutputManagerParams<'a> {
|
||||
config: &'a WaylandWindowConfig,
|
||||
global_ctx: &'a GlobalContext,
|
||||
connection: &'a Connection,
|
||||
layer_surface_config: LayerSurfaceConfig,
|
||||
render_factory: &'a Rc<RenderContextFactory>,
|
||||
popup_context: &'a PopupContext,
|
||||
pointer: &'a Rc<WlPointer>,
|
||||
shared_serial: &'a Rc<SharedPointerSerial>,
|
||||
}
|
||||
|
||||
pub struct WaylandWindowingSystem {
|
||||
state: AppState,
|
||||
connection: Rc<Connection>,
|
||||
|
|
@ -266,9 +279,42 @@ impl WaylandWindowingSystem {
|
|||
&shared_serial,
|
||||
);
|
||||
|
||||
let output_manager = Self::create_output_manager(&OutputManagerParams {
|
||||
config,
|
||||
global_ctx: &global_ctx,
|
||||
connection,
|
||||
layer_surface_config,
|
||||
render_factory: &render_factory,
|
||||
popup_context: &popup_context,
|
||||
pointer: &pointer,
|
||||
shared_serial: &shared_serial,
|
||||
});
|
||||
|
||||
app_state.set_output_manager(Rc::new(RefCell::new(output_manager)));
|
||||
|
||||
Ok(app_state)
|
||||
}
|
||||
|
||||
fn create_output_manager(params: &OutputManagerParams<'_>) -> OutputManager {
|
||||
let manager_context = OutputManagerContext {
|
||||
compositor: params.global_ctx.compositor.clone(),
|
||||
layer_shell: params.global_ctx.layer_shell.clone(),
|
||||
fractional_scale_manager: params.global_ctx.fractional_scale_manager.clone(),
|
||||
viewporter: params.global_ctx.viewporter.clone(),
|
||||
render_factory: Rc::clone(params.render_factory),
|
||||
popup_context: params.popup_context.clone(),
|
||||
pointer: Rc::clone(params.pointer),
|
||||
shared_serial: Rc::clone(params.shared_serial),
|
||||
connection: Rc::new(params.connection.clone()),
|
||||
};
|
||||
|
||||
OutputManager::new(
|
||||
manager_context,
|
||||
params.config.clone(),
|
||||
params.layer_surface_config,
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_shared_popup_creator(
|
||||
popup_managers: Vec<Rc<PopupManager>>,
|
||||
layer_surfaces: Vec<Rc<ZwlrLayerSurfaceV1>>,
|
||||
|
|
@ -322,7 +368,7 @@ impl WaylandWindowingSystem {
|
|||
});
|
||||
}
|
||||
|
||||
fn initialize_renderer(
|
||||
pub(crate) fn initialize_renderer(
|
||||
surface: &Rc<WlSurface>,
|
||||
config: &WaylandWindowConfig,
|
||||
render_factory: &Rc<RenderContextFactory>,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use super::event_context::SharedPointerSerial;
|
||||
use super::surface_state::WindowState;
|
||||
use crate::wayland::managed_proxies::ManagedWlPointer;
|
||||
use crate::wayland::outputs::OutputMapping;
|
||||
use crate::wayland::outputs::{OutputManager, OutputMapping};
|
||||
use layer_shika_domain::entities::output_registry::OutputRegistry;
|
||||
use layer_shika_domain::value_objects::output_handle::OutputHandle;
|
||||
use layer_shika_domain::value_objects::output_info::OutputInfo;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use wayland_client::Proxy;
|
||||
|
|
@ -19,6 +20,8 @@ pub struct AppState {
|
|||
surface_to_output: HashMap<ObjectId, OutputHandle>,
|
||||
_pointer: ManagedWlPointer,
|
||||
shared_pointer_serial: Rc<SharedPointerSerial>,
|
||||
output_manager: Option<Rc<RefCell<OutputManager>>>,
|
||||
registry_name_to_output_id: HashMap<u32, ObjectId>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
|
|
@ -30,9 +33,31 @@ impl AppState {
|
|||
surface_to_output: HashMap::new(),
|
||||
_pointer: pointer,
|
||||
shared_pointer_serial: shared_serial,
|
||||
output_manager: None,
|
||||
registry_name_to_output_id: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_output_manager(&mut self, manager: Rc<RefCell<OutputManager>>) {
|
||||
self.output_manager = Some(manager);
|
||||
}
|
||||
|
||||
pub fn output_manager(&self) -> Option<Rc<RefCell<OutputManager>>> {
|
||||
self.output_manager.as_ref().map(Rc::clone)
|
||||
}
|
||||
|
||||
pub fn register_registry_name(&mut self, name: u32, output_id: ObjectId) {
|
||||
self.registry_name_to_output_id.insert(name, output_id);
|
||||
}
|
||||
|
||||
pub fn find_output_id_by_registry_name(&self, name: u32) -> Option<ObjectId> {
|
||||
self.registry_name_to_output_id.get(&name).cloned()
|
||||
}
|
||||
|
||||
pub fn unregister_registry_name(&mut self, name: u32) -> Option<ObjectId> {
|
||||
self.registry_name_to_output_id.remove(&name)
|
||||
}
|
||||
|
||||
pub fn add_output(
|
||||
&mut self,
|
||||
output_id: ObjectId,
|
||||
|
|
@ -51,6 +76,16 @@ impl AppState {
|
|||
self.windows.insert(handle, window);
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, handle: OutputHandle) -> Option<PerOutputWindow> {
|
||||
self.output_registry.remove(handle);
|
||||
|
||||
let window = self.windows.remove(&handle);
|
||||
|
||||
self.surface_to_output.retain(|_, h| *h != handle);
|
||||
|
||||
window
|
||||
}
|
||||
|
||||
pub fn get_output_by_handle(&self, handle: OutputHandle) -> Option<&PerOutputWindow> {
|
||||
self.windows.get(&handle)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue