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});
@override
Widget build(BuildContext context, WidgetRef ref) => Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Stack(
children:
[
ref
.watch(WorkspacesController.provider)
.when(
error: (error, stackTrace) => [Text(error.toString())],
loading: () => [SizedBox.shrink()],
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
Widget build(BuildContext context, WidgetRef ref) => Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Stack(
children:
[
ref
.watch(WorkspacesController.provider)
.when(
error: (error, stackTrace) => [Text(error.toString())],
loading: () => [SizedBox.shrink()],
data: (value) => value
.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,
),
(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(),
),
),
),
),
)
.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"
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-wayland-source = "0.4.1"

View file

@ -1,10 +1,13 @@
use crate::{frb_generated::StreamSink, internal::wayland::AppState};
use std::{collections::HashMap, sync::{Mutex, OnceLock}};
use serde_json::Result;
use std::{
collections::HashMap,
sync::{Mutex, OnceLock},
};
use calloop::{
channel::{channel, Channel, Sender},
EventLoop,
channel::{Channel, Sender, channel},
};
use calloop_wayland_source::WaylandSource;
@ -25,8 +28,8 @@ pub struct Workspace {
impl Workspace {
pub fn activate(&self) {
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 mut state = AppState {
outputs: HashMap::new(),
outputs: HashMap::new(),
workspaces: HashMap::new(),
workspace_groups: HashMap::new(),
workspace_handles: HashMap::new(),
xdg_output_manager: None,
sink,
};
@ -69,4 +73,4 @@ pub fn listen_workspaces(sink: StreamSink<Vec<Vec<Workspace>>>) -> Result<()> {
while let Ok(_) = event_loop.dispatch(None, &mut state) {}
Ok(())
}
}

View file

@ -2,11 +2,16 @@ use crate::{frb_generated::StreamSink, workspace_api::Workspace};
use std::collections::HashMap;
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::{
ext_workspace_group_handle_v1, ext_workspace_handle_v1, ext_workspace_manager_v1,
use wayland_protocols::{
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 {
@ -29,34 +34,34 @@ pub struct AppState {
pub workspace_groups: HashMap<u32, WorkspaceGroup>,
pub workspace_handles: HashMap<u32, WorkspaceHandles>,
pub sink: StreamSink<Vec<Vec<Workspace>>>,
pub xdg_output_manager: Option<zxdg_output_manager_v1::ZxdgOutputManagerV1>,
}
impl AppState {
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| {
group.output_id.as_ref().and_then(|o| {
self.outputs.get(o).map(|info| info.coords.0)
})
});
groups.sort_by_key(|group| {
group
.output_id
.as_ref()
.and_then(|o| self.outputs.get(o).map(|info| info.coords.0))
});
let result: Vec<Vec<Workspace>> = groups
.into_iter()
.map(|group| {
let mut workspaces: Vec<Workspace> = group
.children
.iter()
.filter_map(|id| self.workspaces.get(id))
.cloned()
.collect();
.into_iter()
.map(|group| {
let mut workspaces: Vec<Workspace> = group
.children
.iter()
.filter_map(|id| self.workspaces.get(id))
.cloned()
.collect();
workspaces.sort_by_key(|ws|
ws.coords.as_ref().map(|coords| coords.0)
);
workspaces
})
.collect();
workspaces.sort_by_key(|ws| ws.coords.as_ref().map(|coords| coords.0));
workspaces
})
.collect();
self.sink.add(result).expect("Updating stream failed");
}
@ -64,7 +69,7 @@ impl AppState {
impl Dispatch<wl_registry::WlRegistry, ()> for AppState {
fn event(
_state: &mut Self,
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
@ -77,27 +82,34 @@ impl Dispatch<wl_registry::WlRegistry, ()> for AppState {
version,
} = event
{
match interface.as_str() {
"ext_workspace_manager_v1" => {
registry.bind::<ext_workspace_manager_v1::ExtWorkspaceManagerV1, (), AppState>(
name,
version,
qh,
(),
);
}
match interface.as_str() {
"ext_workspace_manager_v1" => {
registry.bind::<ext_workspace_manager_v1::ExtWorkspaceManagerV1, (), AppState>(
name,
version,
qh,
(),
);
}
"wl_output" => {
registry.bind::<wl_output::WlOutput, (), AppState>(
name,
version,
qh,
(),
);
}
"zxdg_output_manager_v1" => {
let manager: zxdg_output_manager_v1::ZxdgOutputManagerV1 =
registry.bind::<zxdg_output_manager_v1::ZxdgOutputManagerV1, (), AppState>(
name,
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 {
activated: false,
id: workspace.id().protocol_id(),
id: workspace.id().protocol_id(),
coords: None,
},
);
state.workspace_handles.insert(
workspace.id().protocol_id(),
WorkspaceHandles {
handle: workspace,
manager_handle: proxy.clone(),
}
);
state.workspace_handles.insert(
workspace.id().protocol_id(),
WorkspaceHandles {
handle: workspace,
manager_handle: proxy.clone(),
},
);
}
_ => {}
}
@ -207,7 +219,7 @@ impl Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, ()> for
event: ext_workspace_group_handle_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
qh: &QueueHandle<Self>,
) {
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 } => {
state
.workspace_groups
.entry(group_id.protocol_id())
.or_insert_with(|| WorkspaceGroup {
output_id: None,
children: Vec::new(),
})
.children
.push(workspace.id().protocol_id());
.entry(group_id.protocol_id())
.or_insert_with(|| WorkspaceGroup {
output_id: None,
children: Vec::new(),
})
.children
.push(workspace.id().protocol_id());
state.emit();
}
ext_workspace_group_handle_v1::Event::OutputEnter { output } => {
state
.workspace_groups
.entry(group_id.protocol_id())
.or_insert_with(|| WorkspaceGroup {
output_id: None,
children: Vec::new(),
})
.output_id = Some(output.id().protocol_id());
state.emit();
ext_workspace_group_handle_v1::Event::OutputEnter { output } => {
if let Some(manager) = &state.xdg_output_manager {
let xdg_output = manager.get_xdg_output(&output, qh, ());
state
.workspace_groups
.entry(group_id.protocol_id())
.or_insert_with(|| WorkspaceGroup {
output_id: None,
children: Vec::new(),
})
.output_id = Some(xdg_output.id().protocol_id());
state.emit();
}
}
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(
state: &mut Self,
proxy: &wl_output::WlOutput,
event: wl_output::Event,
proxy: &zxdg_output_v1::ZxdgOutputV1,
event: zxdg_output_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
match event {
wl_output::Event::Geometry {
x,
y,
..
} => {
state.outputs.insert(proxy.id().protocol_id(), Output { coords: ( x, y ) });
zxdg_output_v1::Event::LogicalPosition { x, y } => {
state
.outputs
.insert(proxy.id().protocol_id(), Output { coords: (x, y) });
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
}
}