diff --git a/flake.lock b/flake.lock index 507f6a8..5cfe9ee 100644 --- a/flake.lock +++ b/flake.lock @@ -34,21 +34,6 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1751984180, - "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-unstable", - "type": "indirect" - } - }, "root": { "inputs": { "flake-utils": "flake-utils", @@ -73,7 +58,9 @@ }, "wlroots-lunar": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { "lastModified": 1754816070, diff --git a/flake.nix b/flake.nix index a194d5d..6c6253e 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,10 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; - wlroots-lunar.url = "github:slendidev/wlroots-lunar"; + wlroots-lunar = { + url = "github:slendidev/wlroots-lunar"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = diff --git a/src/LunarWM.c b/src/LunarWM.c index 8e2f551..aec7e77 100644 --- a/src/LunarWM.c +++ b/src/LunarWM.c @@ -46,6 +46,16 @@ #include "vec.h" +static inline SphericalCoord get_forward_spherical_with_nearest( + Vector3 fwd, float r) +{ + if (fabs(fwd.y) < 0.2f) { + fwd.y = 0; + } + auto vec = Vector3Scale(Vector3Normalize(fwd), r); + return Vector3ToSpherical(vec); +} + void toplevel_commit_notify(struct wl_listener *l, void *) { auto *tl = wl_container_of(l, (LunarWM_Toplevel *)(NULL), commit); @@ -63,6 +73,22 @@ void toplevel_commit_notify(struct wl_listener *l, void *) LunarWM_Toplevel_update(tl); } +static void remove_windows_for_tl(LunarWM *wm, LunarWM_Toplevel *tl) +{ + if (!wm || !tl) + return; + for (size_t ws = 0; ws < ARRAY_SZ(wm->wm.workspaces); ++ws) { + auto *vec = &wm->wm.workspaces[ws].v_windows; + for (size_t i = 0; i < vector_size(*vec);) { + if ((*vec)[i].tl == tl) { + vector_remove(*vec, i); + } else { + i++; + } + } + } +} + static void focus_fallback(LunarWM *wm); void toplevel_destroy_notify(struct wl_listener *l, void *) { @@ -75,6 +101,8 @@ void toplevel_destroy_notify(struct wl_listener *l, void *) } } + remove_windows_for_tl(tl->server, tl); + focus_fallback(tl->server); LunarWM_Toplevel_destroy(tl); @@ -137,7 +165,7 @@ static void toplevel_unmap_notify(struct wl_listener *l, void *data) bool LunarWM_Toplevel_init_xdg( LunarWM_Toplevel *tl, LunarWM *wm, struct wlr_xdg_toplevel *xdg) { - tl->id = vector_size(wm->wayland.v_toplevels) + 1; + tl->id = LunarWM_get_new_id(wm); tl->server = wm; tl->is_xwayland = false; tl->u.xdg = xdg; @@ -163,7 +191,7 @@ bool LunarWM_Toplevel_init_xdg( bool LunarWM_Toplevel_init_xwayland( LunarWM_Toplevel *tl, LunarWM *wm, struct wlr_xwayland_surface *xwl) { - tl->id = vector_size(wm->wayland.v_toplevels) + 1; + tl->id = LunarWM_get_new_id(wm); tl->server = wm; tl->is_xwayland = true; tl->u.xwl = xwl; @@ -309,6 +337,13 @@ static void handle_associate(struct wl_listener *ll, void *d) if (LunarWM_Toplevel_init_xwayland(tl, wm2, x)) { vector_add(&wm2->wayland.v_toplevels, tl); + LunarWM_Window window = { + .tl = tl, + .coord = get_forward_spherical_with_nearest( + wm2->renderer.camera.target, wm2->cman->cfg.space.radius), + }; + vector_add( + &wm2->wm.workspaces[wm2->wm.active_workspace].v_windows, window); } else { free(tl); } @@ -323,6 +358,7 @@ static void handle_dissociate(struct wl_listener *ll, void *d) LunarWM_Toplevel *tl = wm2->wayland.v_toplevels[i]; if (tl->is_xwayland && tl->u.xwl == x) { vector_remove(wm2->wayland.v_toplevels, i); + remove_windows_for_tl(wm2, tl); LunarWM_Toplevel_destroy(tl); free(tl); break; @@ -484,6 +520,7 @@ static void xwl_on_destroy(struct wl_listener *ll, void *data) LunarWM_Toplevel *tl = wm->wayland.v_toplevels[i]; if (tl->is_xwayland && tl->u.xwl == x) { vector_remove(wm->wayland.v_toplevels, i); + remove_windows_for_tl(wm, tl); free(tl); break; } @@ -729,9 +766,59 @@ static void Keyboard_key_notify(struct wl_listener *listener, void *data) static void Keyboard_destroy_notify(struct wl_listener *listener, void *) { - auto *kbd = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), destroy); - wl_list_remove(&kbd->modifiers.link); - wl_list_remove(&kbd->key.link); + auto *p = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), destroy); + wl_list_remove(&p->modifiers.link); + wl_list_remove(&p->key.link); + wl_list_remove(&p->destroy.link); + wl_list_remove(&p->link); + free(p); +} + +static inline float wrap_pi(float a) +{ + a = fmodf(a + PI, 2.0f * PI); + if (a < 0.0f) + a += 2.0f * PI; + return a - PI; +} + +static void Pointer_motion_notify(struct wl_listener *listener, void *data) +{ + LunarWM_Pointer *p + = wl_container_of(listener, (LunarWM_Pointer *)NULL, motion); + if (!p->server || !p->server->wayland.seat) + return; + + struct wlr_pointer_motion_event *ev = data; + + float const dx = (float)ev->delta_x; + float const dy = -(float)ev->delta_y; + float const R = p->server->cman->cfg.space.radius; + float const g = 0.0005f; // meters per pixel + + float const POLE_GUARD = 6.0f * (float)M_PI / 180.0f; + float const MIN_SIN = sinf(POLE_GUARD); + + float phi = p->server->wm.pointer.phi; + float theta = p->server->wm.pointer.theta; + + float dphi = (-g * dy) / R; + + float s = fmaxf(sinf(phi), MIN_SIN); + float dtheta = (g * dx) / (R * s); + + phi = Clamp(phi + dphi, POLE_GUARD, (float)M_PI - POLE_GUARD); + theta = wrap_pi(theta + dtheta); + + p->server->wm.pointer.phi = phi; + p->server->wm.pointer.theta = theta; + p->server->wm.pointer.r = R; +} + +static void Pointer_destroy_notify(struct wl_listener *listener, void *) +{ + auto *kbd = wl_container_of(listener, (LunarWM_Pointer *)(NULL), destroy); + wl_list_remove(&kbd->motion.link); wl_list_remove(&kbd->destroy.link); wl_list_remove(&kbd->link); free(kbd); @@ -777,7 +864,19 @@ static void new_input_listener_notify(struct wl_listener *listener, void *data) wl_list_insert(&wm->wayland.keyboards, &keyboard->link); } else if (dev->type == WLR_INPUT_DEVICE_POINTER) { - wlr_cursor_attach_input_device(wm->wayland.cursor, dev); + struct wlr_pointer *wlr_pointer = wlr_pointer_from_input_device(dev); + + LunarWM_Pointer *pointer = calloc(1, sizeof(*pointer)); + pointer->server = wm; + pointer->wlr_pointer = wlr_pointer; + + pointer->destroy.notify = Pointer_destroy_notify; + wl_signal_add(&dev->events.destroy, &pointer->destroy); + + pointer->motion.notify = Pointer_motion_notify; + wl_signal_add(&wlr_pointer->events.motion, &pointer->motion); + + wl_list_insert(&wm->wayland.pointers, &pointer->link); } uint32_t caps = WL_SEAT_CAPABILITY_POINTER; @@ -808,6 +907,13 @@ static void new_xdg_toplevel_listener_notify( if (LunarWM_Toplevel_init_xdg(tl, wm, xdg_tl)) { vector_add(&wm->wayland.v_toplevels, tl); + LunarWM_Window window = { + .tl = tl, + .coord = get_forward_spherical_with_nearest( + wm->renderer.camera.target, wm->cman->cfg.space.radius), + }; + vector_add( + &wm->wm.workspaces[wm->wm.active_workspace].v_windows, window); } else { wlr_log(WLR_ERROR, "Failed to initialize Toplevel."); free(tl); @@ -1044,9 +1150,8 @@ static bool init_wayland(LunarWM *this) wlr_log(WLR_ERROR, "Failed to create wlroots data device manager"); } - this->wayland.cursor = wlr_cursor_create(); - wl_list_init(&this->wayland.keyboards); + wl_list_init(&this->wayland.pointers); this->wayland.new_input_listener.notify = new_input_listener_notify; wl_signal_add(&this->wayland.backend->events.new_input, @@ -1851,6 +1956,12 @@ bool LunarWM_init(LunarWM *this) this->xr.recenter_rot = (Quaternion) { 0, 0, 0, 1 }; this->xr.recenter_trans = (Vector3) { 0, 0, 0 }; this->xr.recenter_active = false; + this->counter = 0; + } + + this->wm.active_workspace = 0; + for (size_t i = 0; i < ARRAY_SZ(this->wm.workspaces); i++) { + this->wm.workspaces[i].v_windows = vector_create(); } this->cman = config_manager_create(get_config_path()); @@ -2049,11 +2160,6 @@ static void cleanup_wayland(LunarWM *this) this->wayland.v_toplevels = NULL; } - if (this->wayland.cursor) { - wlr_cursor_destroy(this->wayland.cursor); - this->wayland.cursor = NULL; - } - if (this->wayland.backend) { wlr_backend_destroy(this->wayland.backend); this->wayland.backend = NULL; @@ -2369,6 +2475,103 @@ void DrawTextureCyl( rlEnableBackfaceCulling(); } +static void DrawTextureCyl2(Texture2D tex, Vector3 sphere_center, + SphericalCoord coord, float rad, float scale, bool y_flip) +{ + if (!tex.id || scale <= 0.0f || rad == 0.0f) + return; + + // midpoint on the sphere where the panel should sit (its center) + Vector3 fwd = SphericalToVector3(coord); + if (Vector3Length(fwd) < 1e-6f) + fwd = (Vector3) { 0, 0, 1 }; + fwd = Vector3Normalize(fwd); + + // build a local tangent frame at that point (right, up, forward) + Vector3 worldUp = (Vector3) { 0, 1, 0 }; + if (fabsf(Vector3DotProduct(worldUp, fwd)) > 0.99f) + worldUp = (Vector3) { 1, 0, 0 }; + Vector3 right = Vector3Normalize(Vector3CrossProduct(worldUp, fwd)); + Vector3 up = Vector3Normalize(Vector3CrossProduct(fwd, right)); + + float r = fabsf(rad); + float arc_len = (float)tex.width * scale; // world units across + float theta = arc_len / r; // total horizontal FOV in radians + float half_t = 0.5f * theta; + float half_h = 0.5f * (float)tex.height * scale; + + // shift so cylinder's surface midpoint lands exactly at coord.r from + // sphere_center + Vector3 delta = Vector3Add(sphere_center, + Vector3Add(Vector3Scale(fwd, coord.r - r), (Vector3) { 0, 0, 0 })); + + // tessellation: about 3° per slice (min 8, max 1024) + int slices = (int)ceilf(fmaxf(theta * (180.0f / PI) / 3.0f, 8.0f)); + if (slices > 1024) + slices = 1024; + + float vt = y_flip ? 1.0f : 0.0f; + float vb = y_flip ? 0.0f : 1.0f; + + rlDrawRenderBatchActive(); + rlSetTexture(tex.id); + rlDisableBackfaceCulling(); + rlColor4ub(255, 255, 255, 255); + rlBegin(RL_QUADS); + + for (int i = 0; i < slices; ++i) { + float u0 = (float)i / (float)slices; + float u1 = (float)(i + 1) / (float)slices; + + float aL = -half_t + theta * u0; + float aR = -half_t + theta * u1; + + // local outward directions on the cylindrical surface + Vector3 nL = Vector3Add( + Vector3Scale(right, sinf(aL)), Vector3Scale(fwd, cosf(aL))); + Vector3 nR = Vector3Add( + Vector3Scale(right, sinf(aR)), Vector3Scale(fwd, cosf(aR))); + + if (rad < 0.0f) { + nL = Vector3Negate(nL); + nR = Vector3Negate(nR); + } + + // surface points (center band), then top/bottom by +/- up*half_h + Vector3 cL = Vector3Add(delta, Vector3Scale(nL, r)); + Vector3 cR = Vector3Add(delta, Vector3Scale(nR, r)); + + Vector3 pLT = Vector3Add(cL, Vector3Scale(up, half_h)); + Vector3 pLB = Vector3Add(cL, Vector3Scale(up, -half_h)); + Vector3 pRT = Vector3Add(cR, Vector3Scale(up, half_h)); + Vector3 pRB = Vector3Add(cR, Vector3Scale(up, -half_h)); + + // match the original horizontal flip so Wayland textures look correct + float U0 = 1.0f - u0; + float U1 = 1.0f - u1; + + rlNormal3f(nL.x, nL.y, nL.z); + rlTexCoord2f(U0, vt); + rlVertex3f(pLT.x, pLT.y, pLT.z); + + rlNormal3f(nR.x, nR.y, nR.z); + rlTexCoord2f(U1, vt); + rlVertex3f(pRT.x, pRT.y, pRT.z); + + rlNormal3f(nR.x, nR.y, nR.z); + rlTexCoord2f(U1, vb); + rlVertex3f(pRB.x, pRB.y, pRB.z); + + rlNormal3f(nL.x, nL.y, nL.z); + rlTexCoord2f(U0, vb); + rlVertex3f(pLB.x, pLB.y, pLB.z); + } + + rlEnd(); + rlSetTexture(0); + rlEnableBackfaceCulling(); +} + static inline Vector3 RecenterPoint(LunarWM *wm, Vector3 p) { if (!wm->xr.recenter_active) @@ -2384,6 +2587,17 @@ static inline Quaternion RecenterOrient(LunarWM *wm, Quaternion q) return QuaternionMultiply(wm->xr.recenter_rot, q); } +static LunarWM_Toplevel *find_toplevel(LunarWM *this, int id) +{ + for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) { + auto *tl = this->wayland.v_toplevels[i]; + if (tl->id == id) + return tl; + } + + return NULL; +} + void render_hud(LunarWM *this, float /*dt*/, int hud_size) { ClearBackground((Color) { 0, 0, 0, 0 }); @@ -2413,18 +2627,31 @@ void render_hud(LunarWM *this, float /*dt*/, int hud_size) } } -void render_3d(LunarWM *this, float /*dt*/) +void render_windows(LunarWM *this, bool alpha_check) { - for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) { - auto *tl = this->wayland.v_toplevels[i]; - if (!tl || !tl->surface) + for (size_t i = 0; i + < vector_size(this->wm.workspaces[this->wm.active_workspace].v_windows); + i++) { + auto *window + = &this->wm.workspaces[this->wm.active_workspace].v_windows[i]; + auto *tl = window->tl; + if (!tl || !tl->surface) { continue; - if (tl->gles_texture && !tl->gles_texture->has_alpha) { - float rad = this->cman->cfg.space.radius - 0.01f * (float)i; - DrawTextureCyl(tl->rl_texture, (Vector3) { 0, 0, rad }, rad, + } + if (tl->gles_texture) { + if (alpha_check && tl->gles_texture->has_alpha) { + continue; + } + float rad = window->coord.r - 0.01f * (float)i; + DrawTextureCyl2(tl->rl_texture, Vector3Zero(), window->coord, rad, this->cman->cfg.space.window_scale, false); } } +} + +void render_3d(LunarWM *this, float /*dt*/) +{ + render_windows(this, true); for (int h = 0; h < 2; ++h) { auto *hand_info = &this->xr.hands[h]; @@ -2444,15 +2671,35 @@ void render_3d(LunarWM *this, float /*dt*/) rlEnableColorBlend(); rlDisableDepthMask(); // don't write depth - for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) { - auto *tl = this->wayland.v_toplevels[i]; - if (!tl || !tl->surface) - continue; - if (tl->gles_texture && tl->gles_texture->has_alpha) { - float rad = this->cman->cfg.space.radius - 0.01f * (float)i; - DrawTextureCyl(tl->rl_texture, (Vector3) { 0, 0, rad }, rad, - this->cman->cfg.space.window_scale, false); + render_windows(this, false); + // TODO: Replace with actual cursor texture. + { // Cursor + Vector3 tip = SphericalToVector3(this->wm.pointer); + + Vector3 n = Vector3Normalize( + Vector3Subtract(this->renderer.camera.position, tip)); + Vector3 up_hint = (Vector3) { 0, 1, 0 }; + if (fabsf(Vector3DotProduct(up_hint, n)) > 0.98f) + up_hint = (Vector3) { 1, 0, 0 }; + Vector3 right = Vector3Normalize(Vector3CrossProduct(up_hint, n)); + Vector3 up = Vector3Normalize(Vector3CrossProduct(n, right)); + Vector3 down = Vector3Negate(up); + + float const s = 0.03f; + + Vector3 v1 = tip; + Vector3 v2 = Vector3Add(tip, Vector3Scale(down, s)); + Vector3 v3 = Vector3Add(tip, Vector3Scale(right, s)); + + Vector3 normal = Vector3CrossProduct( + Vector3Subtract(v2, v1), Vector3Subtract(v3, v1)); + if (Vector3DotProduct(normal, n) < 0.0f) { + Vector3 tmp = v2; + v2 = v3; + v3 = tmp; } + + DrawTriangle3D(v1, v2, v3, RED); } rlEnableDepthMask(); @@ -2714,6 +2961,8 @@ static bool render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt) l_recenter(this->cman->L); lua_pop(this->cman->L, 1); this->renderer.first_frame = false; + this->wm.pointer = get_forward_spherical_with_nearest( + this->renderer.camera.target, this->cman->cfg.space.radius); } return true; diff --git a/src/LunarWM.h b/src/LunarWM.h index d85814d..2d71d78 100644 --- a/src/LunarWM.h +++ b/src/LunarWM.h @@ -37,6 +37,34 @@ struct LunarWM; +typedef struct { + float r, theta, phi; +} SphericalCoord; + +static inline SphericalCoord Vector3ToSpherical(Vector3 v) +{ + SphericalCoord s; + s.r = sqrtf(v.x * v.x + v.y * v.y + v.z * v.z); + if (s.r > 0.0f) { + s.theta = atan2f(v.z, v.x); // azimuth around Y axis + s.phi = acosf(v.y / s.r); // polar angle from Y+ + } else { + s.theta = 0.0f; + s.phi = 0.0f; + } + return s; +} + +static inline Vector3 SphericalToVector3(SphericalCoord s) +{ + Vector3 v; + float sin_phi = sinf(s.phi); + v.x = s.r * cosf(s.theta) * sin_phi; + v.y = s.r * cosf(s.phi); + v.z = s.r * sinf(s.theta) * sin_phi; + return v; +} + typedef struct virtual_output { struct wl_global *global; struct wl_display *display; @@ -65,6 +93,16 @@ typedef struct { struct wl_listener destroy; } LunarWM_Keyboard; +typedef struct { + struct LunarWM *server; + + struct wl_list link; + struct wlr_pointer *wlr_pointer; + + struct wl_listener motion; + struct wl_listener destroy; +} LunarWM_Pointer; + typedef struct { uint32_t id; @@ -91,6 +129,11 @@ typedef struct { Texture2D rl_texture; } LunarWM_Toplevel; +typedef struct { + LunarWM_Toplevel *tl; + SphericalCoord coord; +} LunarWM_Window; + bool LunarWM_Toplevel_init_xdg( LunarWM_Toplevel *tl, struct LunarWM *wm, struct wlr_xdg_toplevel *xdg); bool LunarWM_Toplevel_init_xwayland( @@ -148,14 +191,13 @@ typedef struct LunarWM { struct wlr_seat *seat; struct wl_list keyboards; + struct wl_list pointers; struct wl_listener new_input_listener; struct wlr_xdg_shell *xdg_shell; struct wl_listener new_xdg_toplevel_listener; struct wl_listener new_xdg_popup_listener; - struct wlr_cursor *cursor; - struct wlr_xwayland *xwayland; struct wl_listener xwayland_ready; @@ -212,8 +254,18 @@ typedef struct LunarWM { bool first_frame; } renderer; + struct { + SphericalCoord pointer; + int active_workspace; + struct { + LunarWM_Window *v_windows; + } workspaces[10]; + } wm; + ConfigManager *cman; + _Atomic(int) counter; + bool initialized; bool running; } LunarWM; @@ -224,6 +276,8 @@ void LunarWM_destroy(LunarWM *this); void LunarWM_terminate(LunarWM *this); void LunarWM_run(LunarWM *this); +static inline bool LunarWM_get_new_id(LunarWM *this) { return ++this->counter; } + extern LunarWM g_wm; #endif // LUNAR_WM_H