Order displays based on logical position

This commit is contained in:
Henry Hiles 2026-04-21 22:34:41 -04:00
commit 1ba6c8a026
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 228 additions and 180 deletions

View file

@ -9,104 +9,98 @@ class Bar extends ConsumerWidget {
const Bar({super.key}); const Bar({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) => Center( Widget build(BuildContext context, WidgetRef ref) => Padding(
child: Padding( padding: EdgeInsets.symmetric(horizontal: 8),
padding: EdgeInsets.symmetric(horizontal: 8), child: Stack(
child: Stack( children:
children: [
[ ref
ref .watch(WorkspacesController.provider)
.watch(WorkspacesController.provider) .when(
.when( error: (error, stackTrace) => [Text(error.toString())],
error: (error, stackTrace) => [Text(error.toString())], loading: () => [SizedBox.shrink()],
loading: () => [SizedBox.shrink()], data: (value) => value
data: (value) => value
.map(
(group) => Row(
children: group
.map(
(workspace) => IconButton(
onPressed: () => workspace.activate(),
icon: Icon(
workspace.activated
? Icons.circle
: Icons.circle_outlined,
),
),
)
.toList(),
),
)
.toList(),
),
[
TextButton(
onPressed: () {},
child: Text(
DateFormat.Hm().format(
ref
.watch(TimeController.provider)
.when(
data: (time) => time,
loading: DateTime.now,
error: (_, _) => DateTime.now(),
),
),
),
),
],
[
Row(
children: [
IconButton(onPressed: () {}, icon: Icon(Icons.wifi)),
IconButton(
onPressed: () {},
icon: Icon(Icons.bluetooth),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.volume_off),
),
],
),
],
]
.mapIndexed(
(index, children) => Align(
alignment: [
Alignment.centerLeft,
Alignment.center,
Alignment.centerRight,
][index],
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: children
.map( .map(
(child) => Padding( (group) => Row(
padding: EdgeInsetsGeometry.directional( children: group
bottom: 6, .map(
), (workspace) => IconButton(
child: Container( onPressed: () => workspace.activate(),
height: 42, icon: Icon(
padding: EdgeInsets.symmetric(horizontal: 12), workspace.activated
decoration: BoxDecoration( ? Icons.circle
color: Theme.of( : Icons.circle_outlined,
context, ),
).colorScheme.surfaceContainerLow, ),
borderRadius: BorderRadius.circular(999), )
), .toList(),
child: child,
),
), ),
) )
.toList(), .toList(),
), ),
[
TextButton(
onPressed: () {},
child: Text(
DateFormat.Hm().format(
ref
.watch(TimeController.provider)
.when(
data: (time) => time,
loading: DateTime.now,
error: (_, _) => DateTime.now(),
),
),
),
), ),
) ],
.toList(),
), [
Row(
children: [
IconButton(onPressed: () {}, icon: Icon(Icons.wifi)),
IconButton(onPressed: () {}, icon: Icon(Icons.bluetooth)),
IconButton(
onPressed: () {},
icon: Icon(Icons.volume_off),
),
],
),
],
]
.mapIndexed(
(index, children) => Align(
alignment: [
Alignment.centerLeft,
Alignment.center,
Alignment.centerRight,
][index],
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: children
.map(
(child) => Padding(
padding: EdgeInsetsGeometry.directional(bottom: 6),
child: Container(
height: 42,
padding: EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(999),
),
child: child,
),
),
)
.toList(),
),
),
)
.toList(),
), ),
); );
} }

View file

@ -13,7 +13,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
wayland-client = "0.31" wayland-client = "0.31"
wayland-protocols = { version = "0.32.12", features = ["client", "staging"] } wayland-protocols = { version = "0.32.12", features = ["client", "staging", "unstable"] }
calloop = "0.14.4" calloop = "0.14.4"
calloop-wayland-source = "0.4.1" calloop-wayland-source = "0.4.1"

View file

@ -1,10 +1,13 @@
use crate::{frb_generated::StreamSink, internal::wayland::AppState}; use crate::{frb_generated::StreamSink, internal::wayland::AppState};
use std::{collections::HashMap, sync::{Mutex, OnceLock}};
use serde_json::Result; use serde_json::Result;
use std::{
collections::HashMap,
sync::{Mutex, OnceLock},
};
use calloop::{ use calloop::{
channel::{channel, Channel, Sender},
EventLoop, EventLoop,
channel::{Channel, Sender, channel},
}; };
use calloop_wayland_source::WaylandSource; use calloop_wayland_source::WaylandSource;
@ -25,8 +28,8 @@ pub struct Workspace {
impl Workspace { impl Workspace {
pub fn activate(&self) { pub fn activate(&self) {
if let Some(tx) = controller().lock().unwrap().as_ref() { if let Some(tx) = controller().lock().unwrap().as_ref() {
let _ = tx.send(self.id); let _ = tx.send(self.id);
} }
} }
} }
@ -39,10 +42,11 @@ pub fn listen_workspaces(sink: StreamSink<Vec<Vec<Workspace>>>) -> Result<()> {
let qh = event_queue.handle(); let qh = event_queue.handle();
let mut state = AppState { let mut state = AppState {
outputs: HashMap::new(), outputs: HashMap::new(),
workspaces: HashMap::new(), workspaces: HashMap::new(),
workspace_groups: HashMap::new(), workspace_groups: HashMap::new(),
workspace_handles: HashMap::new(), workspace_handles: HashMap::new(),
xdg_output_manager: None,
sink, sink,
}; };
@ -69,4 +73,4 @@ pub fn listen_workspaces(sink: StreamSink<Vec<Vec<Workspace>>>) -> Result<()> {
while let Ok(_) = event_loop.dispatch(None, &mut state) {} while let Ok(_) = event_loop.dispatch(None, &mut state) {}
Ok(()) Ok(())
} }

View file

@ -2,11 +2,16 @@ use crate::{frb_generated::StreamSink, workspace_api::Workspace};
use std::collections::HashMap; use std::collections::HashMap;
use wayland_client::{ use wayland_client::{
self, event_created_child, protocol::{wl_registry, wl_output}, Connection, Dispatch, Proxy, QueueHandle, self, event_created_child,
protocol::{wl_output, wl_registry},
Connection, Dispatch, Proxy, QueueHandle,
}; };
use wayland_protocols::ext::workspace::v1::client::{ use wayland_protocols::{
ext_workspace_group_handle_v1, ext_workspace_handle_v1, ext_workspace_manager_v1, ext::workspace::v1::client::{
ext_workspace_group_handle_v1, ext_workspace_handle_v1, ext_workspace_manager_v1,
},
xdg::xdg_output::zv1::client::{zxdg_output_manager_v1, zxdg_output_v1},
}; };
pub struct WorkspaceHandles { pub struct WorkspaceHandles {
@ -29,34 +34,34 @@ pub struct AppState {
pub workspace_groups: HashMap<u32, WorkspaceGroup>, pub workspace_groups: HashMap<u32, WorkspaceGroup>,
pub workspace_handles: HashMap<u32, WorkspaceHandles>, pub workspace_handles: HashMap<u32, WorkspaceHandles>,
pub sink: StreamSink<Vec<Vec<Workspace>>>, pub sink: StreamSink<Vec<Vec<Workspace>>>,
pub xdg_output_manager: Option<zxdg_output_manager_v1::ZxdgOutputManagerV1>,
} }
impl AppState { impl AppState {
fn emit(&self) { fn emit(&self) {
let mut groups: Vec<_> = self.workspace_groups.values().collect(); let mut groups: Vec<_> = self.workspace_groups.values().collect();
groups.sort_by_key(|group| { groups.sort_by_key(|group| {
group.output_id.as_ref().and_then(|o| { group
self.outputs.get(o).map(|info| info.coords.0) .output_id
}) .as_ref()
}); .and_then(|o| self.outputs.get(o).map(|info| info.coords.0))
});
let result: Vec<Vec<Workspace>> = groups let result: Vec<Vec<Workspace>> = groups
.into_iter() .into_iter()
.map(|group| { .map(|group| {
let mut workspaces: Vec<Workspace> = group let mut workspaces: Vec<Workspace> = group
.children .children
.iter() .iter()
.filter_map(|id| self.workspaces.get(id)) .filter_map(|id| self.workspaces.get(id))
.cloned() .cloned()
.collect(); .collect();
workspaces.sort_by_key(|ws| workspaces.sort_by_key(|ws| ws.coords.as_ref().map(|coords| coords.0));
ws.coords.as_ref().map(|coords| coords.0) workspaces
); })
workspaces .collect();
})
.collect();
self.sink.add(result).expect("Updating stream failed"); self.sink.add(result).expect("Updating stream failed");
} }
@ -64,7 +69,7 @@ impl AppState {
impl Dispatch<wl_registry::WlRegistry, ()> for AppState { impl Dispatch<wl_registry::WlRegistry, ()> for AppState {
fn event( fn event(
_state: &mut Self, state: &mut Self,
registry: &wl_registry::WlRegistry, registry: &wl_registry::WlRegistry,
event: wl_registry::Event, event: wl_registry::Event,
_data: &(), _data: &(),
@ -77,27 +82,34 @@ impl Dispatch<wl_registry::WlRegistry, ()> for AppState {
version, version,
} = event } = event
{ {
match interface.as_str() { match interface.as_str() {
"ext_workspace_manager_v1" => { "ext_workspace_manager_v1" => {
registry.bind::<ext_workspace_manager_v1::ExtWorkspaceManagerV1, (), AppState>( registry.bind::<ext_workspace_manager_v1::ExtWorkspaceManagerV1, (), AppState>(
name, name,
version, version,
qh, qh,
(), (),
); );
} }
"wl_output" => { "zxdg_output_manager_v1" => {
registry.bind::<wl_output::WlOutput, (), AppState>( let manager: zxdg_output_manager_v1::ZxdgOutputManagerV1 =
name, registry.bind::<zxdg_output_manager_v1::ZxdgOutputManagerV1, (), AppState>(
version, name,
qh, version,
(), qh,
); (),
} );
_ => {} state.xdg_output_manager = Some(manager);
} }
"wl_output" => {
registry.bind::<wl_output::WlOutput, (), AppState>(name, version, qh, ());
}
_ => {}
}
} }
} }
} }
@ -117,18 +129,18 @@ impl Dispatch<ext_workspace_manager_v1::ExtWorkspaceManagerV1, ()> for AppState
workspace.id().protocol_id(), workspace.id().protocol_id(),
Workspace { Workspace {
activated: false, activated: false,
id: workspace.id().protocol_id(), id: workspace.id().protocol_id(),
coords: None, coords: None,
}, },
); );
state.workspace_handles.insert( state.workspace_handles.insert(
workspace.id().protocol_id(), workspace.id().protocol_id(),
WorkspaceHandles { WorkspaceHandles {
handle: workspace, handle: workspace,
manager_handle: proxy.clone(), manager_handle: proxy.clone(),
} },
); );
} }
_ => {} _ => {}
} }
@ -207,7 +219,7 @@ impl Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, ()> for
event: ext_workspace_group_handle_v1::Event, event: ext_workspace_group_handle_v1::Event,
_data: &(), _data: &(),
_conn: &Connection, _conn: &Connection,
_qh: &QueueHandle<Self>, qh: &QueueHandle<Self>,
) { ) {
let group_id = proxy.id(); let group_id = proxy.id();
@ -215,26 +227,30 @@ impl Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, ()> for
ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => { ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => {
state state
.workspace_groups .workspace_groups
.entry(group_id.protocol_id()) .entry(group_id.protocol_id())
.or_insert_with(|| WorkspaceGroup { .or_insert_with(|| WorkspaceGroup {
output_id: None, output_id: None,
children: Vec::new(), children: Vec::new(),
}) })
.children .children
.push(workspace.id().protocol_id()); .push(workspace.id().protocol_id());
state.emit(); state.emit();
} }
ext_workspace_group_handle_v1::Event::OutputEnter { output } => { ext_workspace_group_handle_v1::Event::OutputEnter { output } => {
state if let Some(manager) = &state.xdg_output_manager {
.workspace_groups let xdg_output = manager.get_xdg_output(&output, qh, ());
.entry(group_id.protocol_id()) state
.or_insert_with(|| WorkspaceGroup { .workspace_groups
output_id: None, .entry(group_id.protocol_id())
children: Vec::new(), .or_insert_with(|| WorkspaceGroup {
}) output_id: None,
.output_id = Some(output.id().protocol_id()); children: Vec::new(),
state.emit(); })
.output_id = Some(xdg_output.id().protocol_id());
state.emit();
}
} }
ext_workspace_group_handle_v1::Event::WorkspaceLeave { workspace } => { ext_workspace_group_handle_v1::Event::WorkspaceLeave { workspace } => {
@ -247,25 +263,59 @@ impl Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, ()> for
} }
} }
impl Dispatch<wl_output::WlOutput, ()> for AppState { impl Dispatch<zxdg_output_manager_v1::ZxdgOutputManagerV1, ()> for AppState {
fn event(
_state: &mut Self,
_proxy: &zxdg_output_manager_v1::ZxdgOutputManagerV1,
_event: <zxdg_output_manager_v1::ZxdgOutputManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
// Do nothing, for now
}
event_created_child!(
AppState,
zxdg_output_manager_v1::ZxdgOutputManagerV1,
[
zxdg_output_manager_v1::REQ_GET_XDG_OUTPUT_OPCODE => (
zxdg_output_v1::ZxdgOutputV1,
()
)
]
);
}
impl Dispatch<zxdg_output_v1::ZxdgOutputV1, ()> for AppState {
fn event( fn event(
state: &mut Self, state: &mut Self,
proxy: &wl_output::WlOutput, proxy: &zxdg_output_v1::ZxdgOutputV1,
event: wl_output::Event, event: zxdg_output_v1::Event,
_data: &(), _data: &(),
_conn: &Connection, _conn: &Connection,
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
) { ) {
match event { match event {
wl_output::Event::Geometry { zxdg_output_v1::Event::LogicalPosition { x, y } => {
x, state
y, .outputs
.. .insert(proxy.id().protocol_id(), Output { coords: (x, y) });
} => {
state.outputs.insert(proxy.id().protocol_id(), Output { coords: ( x, y ) });
state.emit(); state.emit();
} }
_ => {} _ => {}
} }
} }
} }
impl Dispatch<wl_output::WlOutput, ()> for AppState {
fn event(
_state: &mut Self,
_proxy: &wl_output::WlOutput,
_event: wl_output::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
// Do nothing, just bind
}
}