1
0
Fork 0
forked from wry/wry

implement wlr_output_management_unstable_v1

This commit is contained in:
Mostafa Ibrahim 2025-06-03 22:18:53 +03:00 committed by Julian Orth
parent a3c0631f4e
commit c6060a7389
23 changed files with 1349 additions and 32 deletions

View file

@ -35,6 +35,7 @@
responsiveness under high system load. This is described in detail in
[setup.md](docs/setup.md).
- Implement wlr-foreign-toplevel-management-v1.
- Implement wlr-output-management-v1.
# 1.10.0 (2025-04-22)

View file

@ -69,7 +69,7 @@ pub trait Backend: Any {
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
pub struct Mode {
pub width: i32,
pub height: i32,

View file

@ -29,6 +29,9 @@ use {
WlSurface,
xdg_surface::{XdgSurface, xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel},
},
wlr_output_manager::{
zwlr_output_head_v1::ZwlrOutputHeadV1, zwlr_output_mode_v1::ZwlrOutputModeV1,
},
workspace_manager::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1,
wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1,
@ -47,8 +50,8 @@ use {
WlDataSourceId, WlOutputId, WlPointerId, WlRegionId, WlRegistryId, WlSeatId,
WlSurfaceId, WpDrmLeaseConnectorV1Id, WpImageDescriptionV1Id,
WpLinuxDrmSyncobjTimelineV1Id, XdgPopupId, XdgPositionerId, XdgSurfaceId,
XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwpPrimarySelectionSourceV1Id,
ZwpTabletToolV2Id,
XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwlrOutputHeadV1Id,
ZwlrOutputModeV1Id, ZwpPrimarySelectionSourceV1Id, ZwpTabletToolV2Id,
},
},
std::{cell::RefCell, rc::Rc},
@ -76,6 +79,8 @@ pub struct Objects {
pub screencasts: CopyHashMap<JayScreencastId, Rc<JayScreencast>>,
pub timelines: CopyHashMap<WpLinuxDrmSyncobjTimelineV1Id, Rc<WpLinuxDrmSyncobjTimelineV1>>,
pub zwlr_data_sources: CopyHashMap<ZwlrDataControlSourceV1Id, Rc<ZwlrDataControlSourceV1>>,
pub zwlr_output_heads: CopyHashMap<ZwlrOutputHeadV1Id, Rc<ZwlrOutputHeadV1>>,
pub zwlr_output_modes: CopyHashMap<ZwlrOutputModeV1Id, Rc<ZwlrOutputModeV1>>,
pub jay_toplevels: CopyHashMap<JayToplevelId, Rc<JayToplevel>>,
pub drm_lease_outputs: CopyHashMap<WpDrmLeaseConnectorV1Id, Rc<WpDrmLeaseConnectorV1>>,
pub tablet_tools: CopyHashMap<ZwpTabletToolV2Id, Rc<ZwpTabletToolV2>>,
@ -119,6 +124,8 @@ impl Objects {
screencasts: Default::default(),
timelines: Default::default(),
zwlr_data_sources: Default::default(),
zwlr_output_heads: Default::default(),
zwlr_output_modes: Default::default(),
jay_toplevels: Default::default(),
drm_lease_outputs: Default::default(),
tablet_tools: Default::default(),
@ -147,6 +154,8 @@ impl Objects {
self.registry.clear();
self.registries.clear();
self.outputs.clear();
self.zwlr_output_heads.clear();
self.zwlr_output_modes.clear();
self.surfaces.clear();
self.xdg_surfaces.clear();
self.xdg_toplevel.clear();

View file

@ -36,6 +36,7 @@ use {
wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
wl_seat::handle_position_hint_requests,
wl_surface::{NoneSurfaceExt, zwp_input_popup_surface_v2::input_popup_positioning},
wlr_output_manager::wlr_output_manager_done,
workspace_manager::workspace_manager_done,
},
io_uring::{IoUring, IoUringError},
@ -246,6 +247,7 @@ fn start_compositor2(
logger: logger.clone(),
connectors: Default::default(),
outputs: Default::default(),
wlr_output_managers: Default::default(),
drm_devs: Default::default(),
status: Default::default(),
idle: IdleState {
@ -474,6 +476,10 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
Phase::PostLayout,
output_render_data(state.clone()),
),
eng.spawn(
"wlr output manager done",
wlr_output_manager_done(state.clone()),
),
eng.spawn2("float layout", Phase::Layout, float_layout(state.clone())),
eng.spawn2(
"float titles",
@ -670,6 +676,7 @@ fn create_dummy_output(state: &Rc<State>) {
handler: Cell::new(None),
connected: Cell::new(true),
name,
description: Default::default(),
drm_dev: None,
async_event: Default::default(),
damaged: Cell::new(false),
@ -678,6 +685,7 @@ fn create_dummy_output(state: &Rc<State>) {
damage_intersect: Default::default(),
state: Cell::new(backend_state),
head_managers: HeadManagers::new(head_name, head_state),
wlr_output_heads: Default::default(),
});
let schedule = Rc::new(OutputSchedule::new(
&state.ring,

View file

@ -42,6 +42,7 @@ use {
wl_shm::WlShmGlobal,
wl_subcompositor::WlSubcompositorGlobal,
wl_surface::xwayland_shell_v1::XwaylandShellV1Global,
wlr_output_manager::zwlr_output_manager_v1::ZwlrOutputManagerV1Global,
workspace_manager::ext_workspace_manager_v1::ExtWorkspaceManagerV1Global,
wp_alpha_modifier_v1::WpAlphaModifierV1Global,
wp_commit_timing_manager_v1::WpCommitTimingManagerV1Global,
@ -186,6 +187,7 @@ impl Globals {
add_singleton!(OrgKdeKwinServerDecorationManagerGlobal);
add_singleton!(ZwpPrimarySelectionDeviceManagerV1Global);
add_singleton!(ZwlrLayerShellV1Global);
add_singleton!(ZwlrOutputManagerV1Global);
add_singleton!(ZxdgOutputManagerV1Global);
add_singleton!(JayCompositorGlobal);
add_singleton!(ZwlrScreencopyManagerV1Global);

View file

@ -52,6 +52,7 @@ pub mod wl_shm;
pub mod wl_shm_pool;
pub mod wl_subcompositor;
pub mod wl_surface;
pub mod wlr_output_manager;
pub mod workspace_manager;
pub mod wp_alpha_modifier_v1;
pub mod wp_commit_timing_manager_v1;

View file

@ -3,8 +3,7 @@ use {
crate::{
backend::transaction::{ConnectorTransaction, PreparedConnectorTransaction},
client::{Client, ClientError},
ifs::{
head_management::{
ifs::head_management::{
Head, HeadCommon, HeadCommonError, HeadMgrState, HeadName, HeadOp,
HeadTransactionError,
head_management_macros::{HeadExtension, MgrExts, announce_head, bind_extension},
@ -12,8 +11,6 @@ use {
jay_head_transaction_result_v1::JayHeadTransactionResultV1,
jay_head_v1::JayHeadV1,
},
wl_output::PersistentOutputState,
},
leaks::Tracker,
object::{Object, Version},
state::{ConnectorData, State},
@ -566,22 +563,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 {
node.set_brightness(desired.brightness);
} else if let Some(mi) = &desired.monitor_info {
let pos = &self.client.state.persistent_output_states;
let pos = match pos.get(&mi.output_id) {
Some(ps) => ps,
_ => {
let ps = Rc::new(PersistentOutputState {
transform: Default::default(),
scale: Default::default(),
pos: Default::default(),
vrr_mode: Cell::new(&VrrMode::Never),
vrr_cursor_hz: Default::default(),
tearing_mode: Cell::new(&TearingMode::Never),
brightness: Default::default(),
});
pos.set(mi.output_id.clone(), ps.clone());
ps
}
};
let pos = pos.lock().entry(mi.output_id.clone()).or_default().clone();
pos.pos.set(desired.position);
pos.scale.set(desired.scale);
pos.transform.set(desired.transform);

View file

@ -125,6 +125,20 @@ pub struct PersistentOutputState {
pub brightness: Cell<Option<f64>>,
}
impl Default for PersistentOutputState {
fn default() -> Self {
Self {
transform: Default::default(),
scale: Default::default(),
pos: Default::default(),
vrr_mode: Cell::new(&VrrMode::Never),
vrr_cursor_hz: Default::default(),
tearing_mode: Cell::new(&TearingMode::Never),
brightness: Default::default(),
}
}
}
#[derive(Eq, PartialEq, Hash, Debug)]
pub struct OutputId {
pub connector: Option<String>,

View file

@ -0,0 +1,47 @@
use {
crate::{
ifs::wlr_output_manager::zwlr_output_head_v1::WlrOutputHeadIds,
state::{OutputData, State},
utils::{copyhashmap::CopyHashMap, queue::AsyncQueue},
},
std::rc::Rc,
zwlr_output_manager_v1::{WlrOutputManagerId, WlrOutputManagerIds, ZwlrOutputManagerV1},
};
pub mod zwlr_output_configuration_head;
pub mod zwlr_output_configuration_v1;
pub mod zwlr_output_head_v1;
pub mod zwlr_output_manager_v1;
pub mod zwlr_output_mode_v1;
#[derive(Default)]
pub struct WlrOutputManagerState {
queue: AsyncQueue<Rc<ZwlrOutputManagerV1>>,
ids: WlrOutputManagerIds,
head_ids: WlrOutputHeadIds,
managers: CopyHashMap<WlrOutputManagerId, Rc<ZwlrOutputManagerV1>>,
}
impl WlrOutputManagerState {
pub fn clear(&self) {
self.managers.clear();
self.queue.clear();
}
pub fn announce_head(&self, on: &Rc<OutputData>) {
for manager in self.managers.lock().values() {
manager.announce_head(on);
}
}
}
pub async fn wlr_output_manager_done(state: Rc<State>) {
loop {
let manager = state.wlr_output_managers.queue.pop().await;
if manager.destroyed.get() {
continue;
}
manager.done_scheduled.set(false);
manager.send_done();
}
}

View file

@ -0,0 +1,146 @@
use {
crate::{
backend::Mode,
client::{Client, ClientError},
fixed::Fixed,
ifs::wlr_output_manager::zwlr_output_head_v1::{
ADAPTIVE_SYNC_STATE_DISABLED, ADAPTIVE_SYNC_STATE_ENABLED, WlrOutputHeadId,
},
leaks::Tracker,
object::{Object, Version},
scale::Scale,
tree::VrrMode,
utils::transform_ext::TransformExt,
wire::{ZwlrOutputConfigurationHeadV1Id, zwlr_output_configuration_head_v1::*},
},
jay_config::video::Transform,
std::{cell::RefCell, rc::Rc},
thiserror::Error,
};
pub struct ZwlrOutputConfigurationHeadV1 {
pub(super) id: ZwlrOutputConfigurationHeadV1Id,
pub(super) head_id: WlrOutputHeadId,
pub(super) version: Version,
pub(super) client: Rc<Client>,
pub(super) config: RefCell<OutputConfig>,
pub(super) tracker: Tracker<Self>,
}
#[derive(Default, Copy, Clone)]
pub struct OutputConfig {
pub(super) transform: Option<Transform>,
pub(super) scale: Option<Scale>,
pub(super) vrr_mode: Option<&'static VrrMode>,
pub(super) pos: Option<(i32, i32)>,
pub(super) mode: Option<Mode>,
}
impl ZwlrOutputConfigurationHeadV1RequestHandler for ZwlrOutputConfigurationHeadV1 {
type Error = ZwlrOutputConfigurationHeadV1Error;
fn set_mode(&self, req: SetMode, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let config = &mut *self.config.borrow_mut();
if config.mode.is_some() {
return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet);
}
let mode = self.client.lookup(req.mode)?;
if self.head_id != mode.head_id {
return Err(ZwlrOutputConfigurationHeadV1Error::InvalidMode);
}
config.mode = Some(mode.mode);
Ok(())
}
fn set_custom_mode(&self, req: SetCustomMode, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let config = &mut *self.config.borrow_mut();
if config.mode.is_some() {
return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet);
}
config.mode = Some(Mode {
width: req.width,
height: req.height,
refresh_rate_millihz: req.refresh as u32,
});
Ok(())
}
fn set_position(&self, req: SetPosition, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let config = &mut *self.config.borrow_mut();
if config.pos.is_some() {
return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet);
}
config.pos = Some((req.x, req.y));
Ok(())
}
fn set_transform(&self, req: SetTransform, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let config = &mut *self.config.borrow_mut();
if config.transform.is_some() {
return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet);
}
let Some(transform) = Transform::from_wl(req.transform) else {
return Err(ZwlrOutputConfigurationHeadV1Error::InvalidTransform(
req.transform,
));
};
config.transform = Some(transform);
Ok(())
}
fn set_scale(&self, req: SetScale, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let config = &mut *self.config.borrow_mut();
if config.scale.is_some() {
return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet);
}
if req.scale <= 0 {
return Err(ZwlrOutputConfigurationHeadV1Error::InvalidScale(req.scale));
}
config.scale = Some(Scale::from_f64(req.scale.to_f64()));
Ok(())
}
fn set_adaptive_sync(&self, req: SetAdaptiveSync, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let config = &mut *self.config.borrow_mut();
if config.vrr_mode.is_some() {
return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet);
}
let state = match req.state {
ADAPTIVE_SYNC_STATE_DISABLED => VrrMode::NEVER,
ADAPTIVE_SYNC_STATE_ENABLED => VrrMode::ALWAYS,
_ => {
return Err(
ZwlrOutputConfigurationHeadV1Error::InvalidAdaptiveSyncState(req.state),
);
}
};
config.vrr_mode = Some(state);
Ok(())
}
}
object_base! {
self = ZwlrOutputConfigurationHeadV1;
version = self.version;
}
impl Object for ZwlrOutputConfigurationHeadV1 {}
simple_add_obj!(ZwlrOutputConfigurationHeadV1);
#[derive(Debug, Error)]
pub enum ZwlrOutputConfigurationHeadV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("Property has already been set")]
AlreadySet,
#[error("Mode doesn't belong to head")]
InvalidMode,
#[error("Unknown transform {0}")]
InvalidTransform(i32),
#[error("Invalid scale {0}")]
InvalidScale(Fixed),
#[error("Invalid adaptive sync state {0}")]
InvalidAdaptiveSyncState(u32),
}
efrom!(ZwlrOutputConfigurationHeadV1Error, ClientError);

View file

@ -0,0 +1,256 @@
use {
crate::{
backend::{
ConnectorId,
transaction::{
BackendConnectorTransactionError, ConnectorTransaction,
PreparedConnectorTransaction,
},
},
client::{Client, ClientError},
ifs::wlr_output_manager::{
zwlr_output_configuration_head::ZwlrOutputConfigurationHeadV1,
zwlr_output_manager_v1::ZwlrOutputManagerV1,
},
leaks::Tracker,
object::{Object, Version},
utils::{copyhashmap::CopyHashMap, errorfmt::ErrorFmt, hash_map_ext::HashMapExt},
wire::{ZwlrOutputConfigurationV1Id, zwlr_output_configuration_v1::*},
},
std::{cell::Cell, rc::Rc},
thiserror::Error,
};
pub struct ZwlrOutputConfigurationV1 {
pub(super) id: ZwlrOutputConfigurationV1Id,
pub(super) version: Version,
pub(super) client: Rc<Client>,
pub(super) tracker: Tracker<Self>,
pub(super) serial: u64,
pub(super) manager: Rc<ZwlrOutputManagerV1>,
pub(super) used: Cell<bool>,
pub(super) enabled_outputs: CopyHashMap<ConnectorId, Rc<ZwlrOutputConfigurationHeadV1>>,
pub(super) configured_outputs: CopyHashMap<ConnectorId, ()>,
}
#[derive(Debug, Error)]
enum ConfigError {
#[error("Serial is out of date")]
OutOfDate,
#[error("Unconfigured output {0}")]
UnconfiguredOutput(Rc<String>),
#[error("Could not add output to transaction")]
AddToTransaction(#[source] BackendConnectorTransactionError),
#[error("Could not prepare transaction")]
PrepareTransaction(#[source] BackendConnectorTransactionError),
#[error("Could not apply transaction")]
ApplyTransaction(#[source] BackendConnectorTransactionError),
}
impl ZwlrOutputConfigurationV1 {
pub fn send_succeeded(&self) {
self.client.event(Succeeded { self_id: self.id });
}
pub fn send_failed(&self) {
self.client.event(Failed { self_id: self.id });
}
pub fn send_cancelled(&self) {
self.client.event(Cancelled { self_id: self.id });
}
fn assert_unused(&self) -> Result<(), ZwlrOutputConfigurationV1Error> {
if self.used.get() {
return Err(ZwlrOutputConfigurationV1Error::AlreadyUsed);
}
Ok(())
}
fn prepare_transaction(&self) -> Result<PreparedConnectorTransaction, ConfigError> {
if self.serial < self.manager.serial.get() {
return Err(ConfigError::OutOfDate);
}
let mut tran = ConnectorTransaction::new(&self.client.state);
for output in self.client.state.outputs.lock().values() {
let mut state = output.connector.state.get();
match self.enabled_outputs.get(&output.connector.id) {
None => {
if self.configured_outputs.not_contains(&output.connector.id) {
return Err(ConfigError::UnconfiguredOutput(
output.connector.name.clone(),
));
}
state.enabled = false;
}
Some(config) => {
state.enabled = true;
let config = *config.config.borrow();
if let Some(mode) = config.mode {
state.mode = mode;
}
}
}
tran.add(&output.connector.connector, state)
.map_err(ConfigError::AddToTransaction)?;
}
tran.prepare().map_err(ConfigError::PrepareTransaction)
}
fn apply_transaction(&self) -> Result<(), ConfigError> {
self.prepare_transaction()?
.apply()
.map_err(ConfigError::ApplyTransaction)?
.commit();
for output in self.client.state.outputs.lock().values() {
let Some(config) = self.enabled_outputs.get(&output.connector.id) else {
continue;
};
let config = *config.config.borrow();
if let Some(node) = &output.node {
if let Some(v) = config.transform {
node.update_transform(v);
}
if let Some(v) = config.scale {
node.set_preferred_scale(v);
}
if let Some(v) = config.vrr_mode {
node.set_vrr_mode(v);
}
if let Some(v) = config.pos {
node.set_position(v.0, v.1);
}
} else {
let mi = &output.monitor_info;
let pos = &self.client.state.persistent_output_states;
let pos = pos.lock().entry(mi.output_id.clone()).or_default().clone();
if let Some(v) = config.transform {
pos.transform.set(v);
}
if let Some(v) = config.scale {
pos.scale.set(v);
}
if let Some(v) = config.vrr_mode {
pos.vrr_mode.set(v);
}
if let Some(v) = config.pos {
pos.pos.set(v);
}
}
}
Ok(())
}
}
impl ZwlrOutputConfigurationV1RequestHandler for ZwlrOutputConfigurationV1 {
type Error = ZwlrOutputConfigurationV1Error;
fn enable_head(&self, req: EnableHead, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.assert_unused()?;
let head = self.client.lookup(req.head)?;
if self.configured_outputs.set(head.connector_id, ()).is_some() {
return Err(ZwlrOutputConfigurationV1Error::AlreadyConfiguredHead(
head.output.connector.name.clone(),
));
}
let configuration_head = Rc::new(ZwlrOutputConfigurationHeadV1 {
id: req.id,
head_id: head.head_id,
version: self.version,
client: self.client.clone(),
config: Default::default(),
tracker: Default::default(),
});
track!(self.client, configuration_head);
self.client.add_client_obj(&configuration_head)?;
self.enabled_outputs
.set(head.connector_id, configuration_head);
Ok(())
}
fn disable_head(&self, req: DisableHead, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.assert_unused()?;
let head = self.client.lookup(req.head)?;
if self.configured_outputs.set(head.connector_id, ()).is_some() {
return Err(ZwlrOutputConfigurationV1Error::AlreadyConfiguredHead(
head.output.connector.name.clone(),
));
}
Ok(())
}
fn apply(&self, _req: Apply, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.assert_unused()?;
self.used.set(true);
let Err(e) = self.apply_transaction() else {
self.send_succeeded();
return Ok(());
};
log::error!("Could not apply output configuration: {}", ErrorFmt(&e));
match e {
ConfigError::UnconfiguredOutput(o) => {
return Err(ZwlrOutputConfigurationV1Error::UnconfiguredHead(o));
}
ConfigError::OutOfDate => {
self.send_cancelled();
return Ok(());
}
ConfigError::AddToTransaction(_)
| ConfigError::PrepareTransaction(_)
| ConfigError::ApplyTransaction(_) => {}
}
self.send_failed();
Ok(())
}
fn test(&self, _req: Test, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.assert_unused()?;
self.used.set(true);
let Err(e) = self.prepare_transaction() else {
self.send_succeeded();
return Ok(());
};
log::error!("Could not test output configuration: {}", ErrorFmt(&e));
match e {
ConfigError::UnconfiguredOutput(o) => {
return Err(ZwlrOutputConfigurationV1Error::UnconfiguredHead(o));
}
ConfigError::OutOfDate
| ConfigError::AddToTransaction(_)
| ConfigError::PrepareTransaction(_)
| ConfigError::ApplyTransaction(_) => {}
}
self.send_failed();
Ok(())
}
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
for head in self.enabled_outputs.lock().drain_values() {
self.client.remove_obj(&*head)?;
}
Ok(())
}
}
object_base! {
self = ZwlrOutputConfigurationV1;
version = self.version;
}
impl Object for ZwlrOutputConfigurationV1 {}
simple_add_obj!(ZwlrOutputConfigurationV1);
#[derive(Debug, Error)]
pub enum ZwlrOutputConfigurationV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("Head {0} has alread been configured")]
AlreadyConfiguredHead(Rc<String>),
#[error("Head {0} has not been configured")]
UnconfiguredHead(Rc<String>),
#[error("Configuration has already been tested or applied")]
AlreadyUsed,
}
efrom!(ZwlrOutputConfigurationV1Error, ClientError);

View file

@ -0,0 +1,250 @@
use {
crate::{
backend::{self, ConnectorId},
client::{Client, ClientError},
fixed::Fixed,
ifs::wlr_output_manager::{
zwlr_output_manager_v1::{WlrOutputManagerId, ZwlrOutputManagerV1},
zwlr_output_mode_v1::ZwlrOutputModeV1,
},
leaks::Tracker,
object::{Object, Version},
scale,
state::OutputData,
tree::VrrMode,
utils::transform_ext::TransformExt,
wire::{ZwlrOutputHeadV1Id, zwlr_output_head_v1::*},
},
ahash::AHashMap,
jay_config::video,
std::rc::Rc,
thiserror::Error,
};
pub const MAKE_SINCE: Version = Version(2);
pub const MODEL_SINCE: Version = Version(2);
pub const SERIAL_NUMBER_SINCE: Version = Version(2);
#[expect(dead_code)]
pub const RELEASE_SINCE: Version = Version(3);
pub const ADAPTIVE_SYNC_SINCE: Version = Version(4);
pub const HEAD_DISABLED: i32 = 0;
pub const HEAD_ENABLED: i32 = 1;
pub const ADAPTIVE_SYNC_STATE_DISABLED: u32 = 0;
pub const ADAPTIVE_SYNC_STATE_ENABLED: u32 = 1;
linear_ids!(WlrOutputHeadIds, WlrOutputHeadId, u64);
pub struct ZwlrOutputHeadV1 {
pub(super) id: ZwlrOutputHeadV1Id,
pub(super) version: Version,
pub(super) client: Rc<Client>,
pub(super) tracker: Tracker<Self>,
pub(super) output: Rc<OutputData>,
pub(super) manager_id: WlrOutputManagerId,
pub(super) manager: Rc<ZwlrOutputManagerV1>,
pub(super) head_id: WlrOutputHeadId,
pub(super) connector_id: ConnectorId,
pub(super) modes: AHashMap<backend::Mode, Rc<ZwlrOutputModeV1>>,
}
impl ZwlrOutputHeadV1 {
fn detach(&self) {
self.output
.connector
.wlr_output_heads
.remove(&self.manager_id);
}
}
impl ZwlrOutputHeadV1 {
pub fn send_name(&self, name: &str) {
self.client.event(Name {
self_id: self.id,
name,
});
}
pub fn send_description(&self, description: &str) {
self.client.event(Description {
self_id: self.id,
description,
});
}
pub fn send_physical_size(&self, width: i32, height: i32) {
self.client.event(PhysicalSize {
self_id: self.id,
width,
height,
});
}
pub fn send_mode(&self, mode: &ZwlrOutputModeV1) {
self.client.event(Mode {
self_id: self.id,
mode: mode.id,
});
}
pub fn send_enabled(&self, enabled: bool) {
let enabled = if enabled { HEAD_ENABLED } else { HEAD_DISABLED };
self.client.event(Enabled {
self_id: self.id,
enabled,
});
}
pub fn send_current_mode(&self, mode: &ZwlrOutputModeV1) {
self.client.event(CurrentMode {
self_id: self.id,
mode: mode.id,
});
}
pub fn send_position(&self, x: i32, y: i32) {
self.client.event(Position {
self_id: self.id,
x,
y,
});
}
pub fn send_transform(&self, transform: video::Transform) {
self.client.event(Transform {
self_id: self.id,
transform: transform.to_wl(),
});
}
pub fn send_scale(&self, scale: scale::Scale) {
let scale = Fixed::from_f64(scale.to_f64());
self.client.event(Scale {
self_id: self.id,
scale,
});
}
pub fn send_finished(&self) {
self.client.event(Finished { self_id: self.id })
}
pub fn send_make(&self, make: &str) {
self.client.event(Make {
self_id: self.id,
make,
});
}
pub fn send_model(&self, model: &str) {
self.client.event(Model {
self_id: self.id,
model,
});
}
pub fn send_serial_number(&self, serial_number: &str) {
self.client.event(SerialNumber {
self_id: self.id,
serial_number,
});
}
pub fn send_adaptive_sync(&self, mode: &VrrMode) {
let state = if *mode == VrrMode::Always {
ADAPTIVE_SYNC_STATE_ENABLED
} else {
ADAPTIVE_SYNC_STATE_DISABLED
};
self.client.event(AdaptiveSync {
self_id: self.id,
state,
});
}
pub fn announce_modes(&self, modes: &[Rc<ZwlrOutputModeV1>]) {
for mode in modes {
self.send_mode(mode);
mode.send();
if mode.initial_current {
self.send_current_mode(mode);
}
}
}
pub fn hande_transform_change(&self, transform: video::Transform) {
self.send_transform(transform);
self.manager.schedule_done();
}
pub fn handle_mode_change(&self, new: backend::Mode) {
let Some(mode) = self.modes.get(&new) else {
return;
};
if mode.destroyed.get() {
return;
}
self.send_current_mode(mode);
self.manager.schedule_done();
}
pub fn handle_position_change(&self, x: i32, y: i32) {
self.send_position(x, y);
self.manager.schedule_done();
}
pub fn handle_vrr_mode_change(&self, mode: &VrrMode) {
if self.version < ADAPTIVE_SYNC_SINCE {
return;
}
self.send_adaptive_sync(mode);
self.manager.schedule_done();
}
pub fn handle_new_scale(&self, scale: scale::Scale) {
self.send_scale(scale);
self.manager.schedule_done();
}
pub fn handle_disconnected(&self) {
self.send_finished();
for mode in self.modes.values() {
if !mode.destroyed.get() {
mode.send_finished();
}
}
self.manager.schedule_done();
}
}
impl ZwlrOutputHeadV1RequestHandler for ZwlrOutputHeadV1 {
type Error = ZwlrOutputHeadV1Error;
fn release(&self, _req: Release, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.send_finished();
self.detach();
self.client.remove_obj(self)?;
Ok(())
}
}
object_base! {
self = ZwlrOutputHeadV1;
version = self.version;
}
impl Object for ZwlrOutputHeadV1 {
fn break_loops(&self) {
self.detach();
}
}
dedicated_add_obj!(ZwlrOutputHeadV1, ZwlrOutputHeadV1Id, zwlr_output_heads);
#[derive(Debug, Error)]
pub enum ZwlrOutputHeadV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(ZwlrOutputHeadV1Error, ClientError);

View file

@ -0,0 +1,289 @@
use {
crate::{
client::{CAP_HEAD_MANAGER, Client, ClientCaps, ClientError},
globals::{Global, GlobalName},
ifs::wlr_output_manager::{
zwlr_output_configuration_v1::ZwlrOutputConfigurationV1,
zwlr_output_head_v1::{
ADAPTIVE_SYNC_SINCE, MAKE_SINCE, MODEL_SINCE, SERIAL_NUMBER_SINCE, ZwlrOutputHeadV1,
},
zwlr_output_mode_v1::ZwlrOutputModeV1,
},
leaks::Tracker,
object::{Object, Version},
state::OutputData,
utils::numcell::NumCell,
wire::{ZwlrOutputManagerV1Id, zwlr_output_manager_v1::*},
},
ahash::AHashMap,
isnt::std_1::string::IsntStringExt,
std::{cell::Cell, rc::Rc},
thiserror::Error,
};
linear_ids!(WlrOutputManagerIds, WlrOutputManagerId, u64);
pub struct ZwlrOutputManagerV1Global {
name: GlobalName,
}
pub struct ZwlrOutputManagerV1 {
pub(super) id: ZwlrOutputManagerV1Id,
pub(super) manager_id: WlrOutputManagerId,
pub(super) client: Rc<Client>,
pub(super) version: Version,
pub(super) tracker: Tracker<Self>,
pub(super) done_scheduled: Cell<bool>,
pub(super) serial: NumCell<u64>,
pub(super) destroyed: Cell<bool>,
}
impl ZwlrOutputManagerV1 {
fn detach(&self) {
self.client
.state
.wlr_output_managers
.managers
.remove(&self.manager_id);
}
}
impl ZwlrOutputManagerV1Global {
pub fn new(name: GlobalName) -> Self {
Self { name }
}
fn bind_(
self: Rc<Self>,
id: ZwlrOutputManagerV1Id,
client: &Rc<Client>,
version: Version,
) -> Result<(), ZwlrOutputManagerV1Error> {
let obj = Rc::new(ZwlrOutputManagerV1 {
id,
manager_id: client.state.wlr_output_managers.ids.next(),
client: client.clone(),
tracker: Default::default(),
version,
done_scheduled: Cell::new(false),
serial: Default::default(),
destroyed: Cell::new(false),
});
track!(client, obj);
client.add_client_obj(&obj)?;
client
.state
.wlr_output_managers
.managers
.set(obj.manager_id, obj.clone());
for output in client.state.outputs.lock().values() {
obj.announce_head(output);
}
Ok(())
}
}
impl ZwlrOutputManagerV1RequestHandler for ZwlrOutputManagerV1 {
type Error = ZwlrOutputManagerV1Error;
fn create_configuration(
&self,
req: CreateConfiguration,
slf: &Rc<Self>,
) -> Result<(), Self::Error> {
let last_serial = self.serial.get();
let mut serial = (last_serial >> u32::BITS << u32::BITS) | (req.serial as u64);
if serial > last_serial {
serial = serial.saturating_sub(1 << u32::BITS);
}
let configuration = Rc::new(ZwlrOutputConfigurationV1 {
id: req.id,
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
serial,
manager: slf.clone(),
used: Cell::new(false),
enabled_outputs: Default::default(),
configured_outputs: Default::default(),
});
track!(self.client, configuration);
self.client.add_client_obj(&configuration)?;
Ok(())
}
fn stop(&self, _req: Stop, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.destroyed.set(true);
self.detach();
self.send_finished();
self.client.remove_obj(self)?;
Ok(())
}
}
impl ZwlrOutputManagerV1 {
pub fn announce_head(self: &Rc<Self>, output: &Rc<OutputData>) {
let id = match self.client.new_id() {
Ok(id) => id,
Err(e) => {
self.client.error(e);
return;
}
};
let mi = &output.monitor_info;
let state = output.connector.state.get();
let head_id = self.client.state.wlr_output_managers.head_ids.next();
let mut modes_list = vec![];
let mut modes = AHashMap::new();
let mut have_current = false;
for (idx, mode) in mi.modes.iter().enumerate() {
if modes.contains_key(mode) {
continue;
}
let current = !have_current && *mode == state.mode;
if current {
have_current = true;
}
let id = match self.client.new_id() {
Ok(id) => id,
Err(e) => {
self.client.error(e);
return;
}
};
let output_mode = Rc::new(ZwlrOutputModeV1 {
id,
head_id,
client: self.client.clone(),
tracker: Default::default(),
version: self.version,
mode: *mode,
preferred: idx == 0,
initial_current: current,
destroyed: Cell::new(false),
});
track!(self.client, output_mode);
self.client.add_server_obj(&output_mode);
modes_list.push(output_mode.clone());
modes.insert(*mode, output_mode);
}
let head = Rc::new(ZwlrOutputHeadV1 {
id,
client: self.client.clone(),
tracker: Default::default(),
version: self.version,
manager_id: self.manager_id,
manager: self.clone(),
head_id,
connector_id: output.connector.id,
modes,
output: output.clone(),
});
track!(self.client, head);
self.client.add_server_obj(&head);
output
.connector
.wlr_output_heads
.set(self.manager_id, head.clone());
self.send_head(&head);
head.send_name(&output.connector.name);
let description = &*output.connector.description.borrow();
if description.is_not_empty() {
head.send_description(description);
}
head.send_enabled(!mi.non_desktop_effective);
head.announce_modes(&modes_list);
head.send_physical_size(mi.width_mm, mi.height_mm);
if mi.output_id.manufacturer.is_not_empty() && head.version >= MAKE_SINCE {
head.send_make(&mi.output_id.manufacturer);
}
if mi.output_id.model.is_not_empty() && head.version >= MODEL_SINCE {
head.send_model(&mi.output_id.model);
}
if mi.output_id.serial_number.is_not_empty() && head.version >= SERIAL_NUMBER_SINCE {
head.send_serial_number(&mi.output_id.serial_number);
}
if let Some(node) = &output.node {
let p = &node.global.persistent;
head.send_scale(p.scale.get());
head.send_position(p.pos.get().0, p.pos.get().1);
head.send_transform(p.transform.get());
if head.version >= ADAPTIVE_SYNC_SINCE {
head.send_adaptive_sync(p.vrr_mode.get());
}
}
self.schedule_done();
}
pub fn send_head(&self, head: &ZwlrOutputHeadV1) {
self.client.event(Head {
self_id: self.id,
head: head.id,
});
}
pub fn send_done(&self) {
self.client.event(Done {
self_id: self.id,
serial: self.serial.get() as u32,
});
}
pub fn send_finished(&self) {
self.client.event(Finished { self_id: self.id });
}
pub(super) fn schedule_done(self: &Rc<Self>) {
if self.done_scheduled.replace(true) {
return;
}
self.serial.fetch_add(1);
self.client
.state
.wlr_output_managers
.queue
.push(self.clone());
}
}
global_base!(
ZwlrOutputManagerV1Global,
ZwlrOutputManagerV1,
ZwlrOutputManagerV1Error
);
impl Global for ZwlrOutputManagerV1Global {
fn singleton(&self) -> bool {
true
}
fn version(&self) -> u32 {
4
}
fn required_caps(&self) -> ClientCaps {
CAP_HEAD_MANAGER
}
}
simple_add_global!(ZwlrOutputManagerV1Global);
object_base! {
self = ZwlrOutputManagerV1;
version = self.version;
}
simple_add_obj!(ZwlrOutputManagerV1);
impl Object for ZwlrOutputManagerV1 {
fn break_loops(&self) {
self.detach();
}
}
#[derive(Debug, Error)]
pub enum ZwlrOutputManagerV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(ZwlrOutputManagerV1Error, ClientError);

View file

@ -0,0 +1,83 @@
use {
crate::{
backend::Mode,
client::{Client, ClientError},
ifs::wlr_output_manager::zwlr_output_head_v1::WlrOutputHeadId,
leaks::Tracker,
object::{Object, Version},
wire::{ZwlrOutputModeV1Id, zwlr_output_mode_v1::*},
},
std::{cell::Cell, rc::Rc},
thiserror::Error,
};
pub struct ZwlrOutputModeV1 {
pub(super) id: ZwlrOutputModeV1Id,
pub(super) head_id: WlrOutputHeadId,
pub(super) version: Version,
pub(super) client: Rc<Client>,
pub(super) tracker: Tracker<Self>,
pub(super) mode: Mode,
pub(super) preferred: bool,
pub(super) initial_current: bool,
pub(super) destroyed: Cell<bool>,
}
impl ZwlrOutputModeV1 {
pub fn send(&self) {
self.send_size(self.mode.width, self.mode.height);
self.send_refresh(self.mode.refresh_rate_millihz as _);
if self.preferred {
self.send_preferred();
}
}
fn send_size(&self, width: i32, height: i32) {
self.client.event(Size {
self_id: self.id,
width,
height,
});
}
fn send_refresh(&self, refresh: i32) {
self.client.event(Refresh {
self_id: self.id,
refresh,
});
}
fn send_preferred(&self) {
self.client.event(Preferred { self_id: self.id });
}
pub fn send_finished(&self) {
self.client.event(Finished { self_id: self.id })
}
}
impl ZwlrOutputModeV1RequestHandler for ZwlrOutputModeV1 {
type Error = ZwlrOutputModeV1Error;
fn release(&self, _req: Release, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.destroyed.set(true);
self.client.remove_obj(self)?;
Ok(())
}
}
object_base! {
self = ZwlrOutputModeV1;
version = self.version;
}
impl Object for ZwlrOutputModeV1 {}
dedicated_add_obj!(ZwlrOutputModeV1, ZwlrOutputModeV1Id, zwlr_output_modes);
#[derive(Debug, Error)]
pub enum ZwlrOutputModeV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(ZwlrOutputModeV1Error, ClientError);

View file

@ -11,7 +11,6 @@ use {
};
pub const NAME_SINCE: Version = Version(2);
#[expect(dead_code)]
pub const DESCRIPTION_SINCE: Version = Version(2);
pub const NO_DONE_SINCE: Version = Version(3);
@ -53,7 +52,6 @@ impl ZxdgOutputV1 {
});
}
#[expect(dead_code)]
pub fn send_description(&self, description: &str) {
self.client.event(Description {
self_id: self.id,
@ -71,6 +69,9 @@ impl ZxdgOutputV1 {
if self.version >= NAME_SINCE {
self.send_name(&global.connector.name);
}
if self.version >= DESCRIPTION_SINCE {
self.send_description(&global.connector.description.borrow());
}
if self.version >= NO_DONE_SINCE {
if self.output.version >= SEND_DONE_SINCE {
self.output.send_done();

View file

@ -67,6 +67,10 @@ use {
zwp_idle_inhibitor_v1::{IdleInhibitorId, IdleInhibitorIds, ZwpIdleInhibitorV1},
zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2,
},
wlr_output_manager::{
WlrOutputManagerState, zwlr_output_head_v1::ZwlrOutputHeadV1,
zwlr_output_manager_v1::WlrOutputManagerId,
},
workspace_manager::WorkspaceManagerState,
wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1,
wp_drm_lease_device_v1::WpDrmLeaseDeviceV1Global,
@ -185,6 +189,7 @@ pub struct State {
pub logger: Option<Arc<Logger>>,
pub connectors: CopyHashMap<ConnectorId, Rc<ConnectorData>>,
pub outputs: CopyHashMap<ConnectorId, Rc<OutputData>>,
pub wlr_output_managers: WlrOutputManagerState,
pub drm_devs: CopyHashMap<DrmDeviceId, Rc<DrmDevData>>,
pub status: CloneCell<Rc<String>>,
pub idle: IdleState,
@ -382,6 +387,7 @@ pub struct ConnectorData {
pub handler: Cell<Option<SpawnedFuture<()>>>,
pub connected: Cell<bool>,
pub name: Rc<String>,
pub description: RefCell<String>,
pub drm_dev: Option<Rc<DrmDevData>>,
pub async_event: Rc<AsyncEvent>,
pub damaged: Cell<bool>,
@ -390,6 +396,7 @@ pub struct ConnectorData {
pub damage_intersect: Cell<Rect>,
pub state: Cell<BackendConnectorState>,
pub head_managers: HeadManagers,
pub wlr_output_heads: CopyHashMap<WlrOutputManagerId, Rc<ZwlrOutputHeadV1>>,
}
pub struct OutputData {
@ -468,6 +475,9 @@ impl ConnectorData {
}
if old.mode != s.mode {
self.head_managers.handle_mode_change(s.mode);
for head in self.wlr_output_heads.lock().values() {
head.handle_mode_change(s.mode);
}
}
if let Some(output) = state.outputs.get(&self.connector.id())
&& let Some(node) = &output.node
@ -1009,6 +1019,7 @@ impl State {
for output in self.root.outputs.lock().values() {
output.clear();
}
self.wlr_output_managers.clear();
self.dbus.clear();
self.pending_container_layout.clear();
self.pending_container_render_positions.clear();

View file

@ -15,7 +15,8 @@ use {
state::{ConnectorData, OutputData, State},
tree::{OutputNode, WsMoveConfig, move_ws_to_output},
utils::{
asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt, rc_eq::RcEq,
asyncevent::AsyncEvent, clonecell::CloneCell, debug_fn::debug_fn,
hash_map_ext::HashMapExt, rc_eq::RcEq,
},
},
jay_config::video::Transform,
@ -76,6 +77,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
handler: Default::default(),
connected: Cell::new(false),
name,
description: Default::default(),
drm_dev: drm_dev.clone(),
async_event: Rc::new(AsyncEvent::default()),
damaged: Cell::new(false),
@ -84,6 +86,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
damage_intersect: Default::default(),
state: Cell::new(backend_state),
head_managers: HeadManagers::new(state.head_names.next(), head_state),
wlr_output_heads: Default::default(),
});
if let Some(dev) = drm_dev {
dev.connectors.set(id, data.clone());
@ -143,6 +146,7 @@ impl ConnectorHandler {
log::info!("Connector {} connected", self.data.connector.kernel_id());
self.data.connected.set(true);
self.data.set_state(&self.state, info.state);
*self.data.description.borrow_mut() = create_description(&info);
let name = self.state.globals.name();
if info.non_desktop_effective {
self.handle_non_desktop_connected(info).await;
@ -151,6 +155,9 @@ impl ConnectorHandler {
}
self.data.connected.set(false);
self.data.head_managers.handle_output_disconnected();
for head in self.data.wlr_output_heads.lock().drain_values() {
head.handle_disconnected();
}
log::info!("Connector {} disconnected", self.data.connector.kernel_id());
}
@ -316,6 +323,7 @@ impl ConnectorHandler {
self.data
.head_managers
.handle_output_connected(&output_data);
self.state.wlr_output_managers.announce_head(&output_data);
'outer: loop {
while let Some(event) = self.data.connector.event() {
match event {
@ -433,6 +441,7 @@ impl ConnectorHandler {
self.data
.head_managers
.handle_output_connected(&output_data);
self.state.wlr_output_managers.announce_head(&output_data);
'outer: loop {
while let Some(event) = self.data.connector.event() {
match event {
@ -451,3 +460,22 @@ impl ConnectorHandler {
}
}
}
fn create_description(info: &MonitorInfo) -> String {
debug_fn(|f| {
let mut needs_space = false;
let id = &info.output_id;
for s in [&id.manufacturer, &id.model, &id.serial_number] {
if s.is_empty() {
continue;
}
if needs_space {
f.write_str(" ")?;
}
needs_space = true;
f.write_str(s)?;
}
Ok(())
})
.to_string()
}

View file

@ -483,6 +483,9 @@ impl OutputNode {
.connector
.head_managers
.handle_scale_change(scale);
for head in self.global.connector.wlr_output_heads.lock().values() {
head.handle_new_scale(scale);
}
}
pub fn schedule_update_render_data(self: &Rc<Self>) {
@ -778,6 +781,9 @@ impl OutputNode {
}
let rect = pos.at_point(x, y);
self.change_extents_(&rect);
for head in self.global.connector.wlr_output_heads.lock().values() {
head.handle_position_change(x, y);
}
}
pub fn update_mode(self: &Rc<Self>, mode: Mode) {
@ -817,6 +823,9 @@ impl OutputNode {
.connector
.head_managers
.handle_transform_change(transform);
for head in self.global.connector.wlr_output_heads.lock().values() {
head.hande_transform_change(transform);
}
}
}
@ -1339,6 +1348,9 @@ impl OutputNode {
.connector
.head_managers
.handle_vrr_mode_change(mode.to_config());
for head in self.global.connector.wlr_output_heads.lock().values() {
head.handle_vrr_mode_change(mode);
}
}
}

View file

@ -0,0 +1,28 @@
# requests
request set_mode {
mode: id(zwlr_output_mode_v1),
}
request set_custom_mode {
width: i32,
height: i32,
refresh: i32,
}
request set_position {
x: i32,
y: i32,
}
request set_transform {
transform: i32,
}
request set_scale {
scale: fixed,
}
request set_adaptive_sync {
state: u32,
}

View file

@ -0,0 +1,36 @@
# requests
request enable_head {
id: id(zwlr_output_configuration_head_v1),
head: id(zwlr_output_head_v1),
}
request disable_head {
head: id(zwlr_output_head_v1),
}
request apply {
}
request test {
}
request destroy {
}
# events
event succeeded {
}
event failed {
}
event cancelled {
}

View file

@ -0,0 +1,65 @@
# requests
request release (since = 3) {
}
# events
event name {
name: str,
}
event description {
description: str,
}
event physical_size {
width: i32,
height: i32
}
event mode {
mode: id(zwlr_output_mode_v1),
}
event enabled {
enabled: i32,
}
event current_mode {
mode: id(zwlr_output_mode_v1),
}
event position {
x: i32,
y: i32,
}
event transform {
transform: i32,
}
event scale {
scale: fixed,
}
event finished {
}
event make (since = 2) {
make: str
}
event model (since = 2) {
model: str
}
event serial_number (since = 2) {
serial_number: str
}
event adaptive_sync (since = 4) {
state: u32,
}

View file

@ -0,0 +1,24 @@
# requests
request create_configuration {
id: id(zwlr_output_configuration_v1),
serial: u32,
}
request stop {
}
# events
event head {
head: id(zwlr_output_head_v1)
}
event done {
serial: u32
}
event finished {
}

View file

@ -0,0 +1,24 @@
# requests
request release (since = 3) {
}
# events
event size {
width: i32,
height: i32,
}
event refresh {
refresh: i32,
}
event preferred {
}
event finished {
}