use eframe::egui;
use egui::{ViewportBuilder, ViewportId};
mod monitor_info;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Direction {
North,
South,
West,
East,
}
impl Direction {
fn from_key(key: egui::Key) -> Option<Self> {
match key {
egui::Key::N => Some(Direction::North),
egui::Key::S => Some(Direction::South),
egui::Key::W => Some(Direction::West),
egui::Key::E => Some(Direction::East),
_ => None,
}
}
fn vector(&self) -> (f32, f32) {
match self {
Direction::North => (0.0, -1.0), Direction::South => (0.0, 1.0), Direction::West => (-1.0, 0.0), Direction::East => (1.0, 0.0), }
}
}
struct CardinalViewport {
direction: Direction,
position: egui::Pos2,
velocity: f32,
open: bool,
monitor_rect: egui::Rect,
movement: (f32, f32),
}
pub struct CardinalViewportsApp {
viewports: Vec<CardinalViewport>,
collision_enabled: bool,
wrap_mode: WrapMode,
}
#[derive(PartialEq, Eq, Debug)]
enum WrapMode {
ParentRect,
MonitorOfSpawn,
AllMonitorsSequential,
AllMonitorsGeometric,
}
impl Default for CardinalViewportsApp {
fn default() -> Self {
Self {
viewports: Vec::new(),
collision_enabled: true,
wrap_mode: WrapMode::MonitorOfSpawn,
}
}
}
impl eframe::App for CardinalViewportsApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let mut spawn_direction: Option<Direction> = None;
egui::TopBottomPanel::top("cardinal_controls_top").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("Cardinal Viewports Example");
ui.label("Press N, S, W, E or use the buttons below to spawn animated viewports in each direction.");
});
ui.horizontal(|ui| {
if ui.button("North").clicked() {
spawn_direction = Some(Direction::North);
}
if ui.button("South").clicked() {
spawn_direction = Some(Direction::South);
}
if ui.button("West").clicked() {
spawn_direction = Some(Direction::West);
}
if ui.button("East").clicked() {
spawn_direction = Some(Direction::East);
}
ui.separator();
ui.checkbox(&mut self.collision_enabled, "Collision Detection");
ui.separator();
egui::ComboBox::from_label("Wrap Mode")
.selected_text(match self.wrap_mode {
WrapMode::ParentRect => "Parent Rect",
WrapMode::MonitorOfSpawn => "Monitor of Spawn",
WrapMode::AllMonitorsSequential => "All Monitors (Sequential)",
WrapMode::AllMonitorsGeometric => "All Monitors (Geometric)",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.wrap_mode, WrapMode::ParentRect, "Parent Rect");
ui.selectable_value(&mut self.wrap_mode, WrapMode::MonitorOfSpawn, "Monitor of Spawn");
ui.selectable_value(&mut self.wrap_mode, WrapMode::AllMonitorsSequential, "All Monitors (Sequential)");
ui.selectable_value(&mut self.wrap_mode, WrapMode::AllMonitorsGeometric, "All Monitors (Geometric)");
});
});
});
for key in [egui::Key::N, egui::Key::S, egui::Key::W, egui::Key::E] {
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, key)) {
spawn_direction = Direction::from_key(key);
}
}
if let Some(direction) = spawn_direction {
if let Some(parent_rect) = ctx.input(|i| i.viewport().outer_rect) {
let monitor_rects: Vec<egui::Rect> = unsafe {
monitor_info::MONITOR_RECTS.lock().unwrap().clone()
};
let collision_radius = 100.0;
let (dx, dy) = direction.vector();
let start_pos = egui::pos2(
parent_rect.center().x + dx * (collision_radius + 8.0),
parent_rect.center().y + dy * (collision_radius + 8.0),
);
let spawn_monitor_rect = monitor_rects
.iter()
.find(|r| r.contains(start_pos))
.cloned()
.unwrap_or_else(|| {
monitor_rects.get(0).cloned().unwrap_or(
egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::Vec2::new(1920.0, 1080.0),
),
)
});
self.viewports.push(CardinalViewport {
direction,
position: start_pos,
velocity: 8.0,
open: true,
monitor_rect: spawn_monitor_rect,
movement: (dx, dy),
});
}
}
if let Some(parent_rect) = ctx.input(|i| i.viewport().outer_rect) {
let monitor_rects =
monitor_info::MONITOR_RECTS.lock().unwrap().clone();
let all_monitors_rect = monitor_rects.iter().skip(1).fold(
monitor_rects.first().cloned().unwrap_or(
egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::Vec2::new(1920.0, 1080.0),
),
),
|acc, r| acc.union(*r),
);
#[cfg(debug_assertions)]
{
println!("[DEBUG] Monitor rects:");
for (idx, r) in monitor_rects.iter().enumerate() {
println!(
" {} Monitor {idx}: min=({:.1},{:.1}) max=({:.1},{:.1}) size=({:.1},{:.1})",
idx,
r.min.x,
r.min.y,
r.max.x,
r.max.y,
r.width(),
r.height()
);
}
println!("[DEBUG] All monitors union: min=({:.1},{:.1}) max=({:.1},{:.1}) size=({:.1},{:.1})", all_monitors_rect.min.x, all_monitors_rect.min.y, all_monitors_rect.max.x, all_monitors_rect.max.y, all_monitors_rect.width(), all_monitors_rect.height());
}
for (i, viewport) in self.viewports.iter_mut().enumerate() {
if !viewport.open {
continue;
}
let (dx, dy) = viewport.movement;
viewport.position.x += dx * viewport.velocity;
viewport.position.y += dy * viewport.velocity;
let wrap_rect = match self.wrap_mode {
WrapMode::ParentRect => parent_rect,
WrapMode::MonitorOfSpawn => viewport.monitor_rect,
WrapMode::AllMonitorsSequential
| WrapMode::AllMonitorsGeometric => {
let mut current_monitor_idx = monitor_rects
.iter()
.position(|r| r.contains(viewport.position));
if current_monitor_idx.is_none() {
current_monitor_idx = monitor_rects
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
let da =
a.center().distance(viewport.position);
let db =
b.center().distance(viewport.position);
da.partial_cmp(&db).unwrap()
})
.map(|(idx, _)| idx);
}
let (dx, dy) = viewport.movement;
let mut did_wrap = false;
let mut new_pos = viewport.position;
let mut new_idx = current_monitor_idx.unwrap_or(0);
match self.wrap_mode {
WrapMode::AllMonitorsSequential => {
if let Some(idx) = current_monitor_idx {
if let Some((pos, idx2)) =
monitor_info::wrap_sequential(
viewport.position,
idx,
dx,
dy,
&monitor_rects,
)
{
new_pos = pos;
new_idx = idx2;
did_wrap = true;
}
}
}
WrapMode::AllMonitorsGeometric => {
if let Some((pos, idx2)) =
monitor_info::wrap_geometric(
viewport.position,
dx,
dy,
&monitor_rects,
)
{
new_pos = pos;
new_idx = idx2;
did_wrap = true;
}
}
_ => {}
}
if did_wrap {
viewport.position = new_pos;
viewport.monitor_rect = monitor_rects[new_idx];
}
monitor_rects[new_idx]
}
};
#[cfg(debug_assertions)]
{
println!("[DEBUG] Viewport {i} {:?} wrap_mode={:?} wrap_rect: min=({:.1},{:.1}) max=({:.1},{:.1}) size=({:.1},{:.1}) pos=({:.1},{:.1})", viewport.direction, self.wrap_mode, wrap_rect.min.x, wrap_rect.min.y, wrap_rect.max.x, wrap_rect.max.y, wrap_rect.width(), wrap_rect.height(), viewport.position.x, viewport.position.y);
}
if viewport.position.x < wrap_rect.left() {
viewport.position.x = wrap_rect.right();
}
if viewport.position.x > wrap_rect.right() {
viewport.position.x = wrap_rect.left();
}
if viewport.position.y < wrap_rect.top() {
viewport.position.y = wrap_rect.bottom();
}
if viewport.position.y > wrap_rect.bottom() {
viewport.position.y = wrap_rect.top();
}
if self.collision_enabled {
let parent_center = parent_rect.center();
let dist = ((viewport.position.x - parent_center.x)
.powi(2)
+ (viewport.position.y - parent_center.y).powi(2))
.sqrt();
if ctx.input(|i| i.viewport().parent.is_none()) {
ctx.layer_painter(egui::LayerId::new(
egui::Order::Foreground,
egui::Id::new("cardinal_collision"),
))
.circle(
parent_center,
50.0,
egui::Color32::RED,
egui::Stroke::new(4.0, egui::Color32::RED),
);
}
if dist < 100.0 {
#[cfg(target_os = "windows")]
{
unsafe {
winapi::um::winuser::MessageBeep(0xFFFFFFFF)
};
}
#[cfg(target_os = "linux")]
println!("\x07");
#[cfg(target_os = "macos")]
println!("\x07");
viewport.open = false;
}
}
let viewport_id =
ViewportId::from_hash_of(format!("cardinal_{i}"));
ctx.show_viewport_immediate(
viewport_id,
ViewportBuilder::default()
.with_title(format!("Viewport: {:?}", viewport.direction))
.with_inner_size([200.0, 100.0])
.with_position(viewport.position)
.with_decorations(false)
.with_always_on_top(),
move |ctx, class| {
if class == egui::ViewportClass::Embedded {
egui::Window::new("Cardinal Viewport").show(ctx, |ui| {
ui.label(
"This egui integration does not support multiple viewports",
);
});
} else {
egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| {
let dir_char = match viewport.direction {
Direction::North => "N",
Direction::South => "S",
Direction::East => "E",
Direction::West => "W",
};
ui.label(egui::RichText::new(dir_char).size(96.0).strong());
});
});
}
},
);
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn main() {
let options = eframe::NativeOptions::default();
use eframe::UserEvent;
use winit::event_loop::EventLoop;
let event_loop = EventLoop::<UserEvent>::with_user_event()
.build()
.expect("Failed to build event loop");
monitor_info::fill_monitor_rects();
#[cfg(debug_assertions)]
{
let rects = monitor_info::MONITOR_RECTS.lock().unwrap();
println!("[DEBUG] Filled MONITOR_RECTS: {} monitors", rects.len());
for (i, r) in rects.iter().enumerate() {
println!(
" Monitor {i}: min=({:.1},{:.1}) max=({:.1},{:.1}) size=({:.1},{:.1})",
r.min.x,
r.min.y,
r.max.x,
r.max.y,
r.width(),
r.height()
);
}
}
let mut app = eframe::create_native(
"Cardinal Viewports",
options,
Box::new(|_cc| Ok(Box::new(CardinalViewportsApp::default()))),
&event_loop,
);
event_loop.run_app(&mut app).expect("eframe app failed");
}
#[cfg(target_arch = "wasm32")]
fn main() {
eprintln!("Cardinal Viewports is not supported on wasm32 targets.");
}