logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
[package]
name = "terminal_view"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[features]
test-support = ["editor/test-support", "gpui/test-support"]
[lib]
path = "src/terminal_view.rs"
doctest = false
[dependencies]
anyhow.workspace = true
async-recursion.workspace = true
breadcrumbs.workspace = true
collections.workspace = true
db.workspace = true
dirs.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
pretty_assertions.workspace = true
project.workspace = true
regex.workspace = true
task.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
shellexpand.workspace = true
terminal.workspace = true
theme.workspace = true
theme_settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
remote = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
rpc = { workspace = true, features = ["test-support"] }
semver.workspace = true
terminal = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
[package.metadata.cargo-machete]
ignored = ["log"]

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,23 @@
Design notes:
This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
#Input
There are currently many distinct paths for getting keystrokes to the terminal:
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a separate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Tom Hale, 2016. MIT Licence.
# Print out 256 colours, with each number printed in its corresponding colour
# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
set -eu # Fail on errors or undeclared variables
printable_colours=256
# Return a colour that contrasts with the given colour
# Bash only does integer division, so keep it integral
function contrast_colour {
local r g b luminance
colour="$1"
if (( colour < 16 )); then # Initial 16 ANSI colours
(( colour == 0 )) && printf "15" || printf "0"
return
fi
# Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
if (( colour > 231 )); then # Greyscale ramp
(( colour < 244 )) && printf "15" || printf "0"
return
fi
# All other colours:
# 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5]
# See http://stackoverflow.com/a/27165165/5353461
# r=$(( (colour-16) / 36 ))
g=$(( ((colour-16) % 36) / 6 ))
# b=$(( (colour-16) % 6 ))
# If luminance is bright, print number in black, white otherwise.
# Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
(( g > 2)) && printf "0" || printf "15"
return
# Uncomment the below for more precise luminance calculations
# # Calculate perceived brightness
# # See https://www.w3.org/TR/AERT#color-contrast
# # and http://www.itu.int/rec/R-REC-BT.601
# # Luminance is in range 0..5000 as each value is 0..5
# luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
# (( $luminance > 2500 )) && printf "0" || printf "15"
}
# Print a coloured block with the number of that colour
function print_colour {
local colour="$1" contrast
contrast=$(contrast_colour "$1")
printf "\e[48;5;%sm" "$colour" # Start block of colour
printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
printf "\e[0m " # Reset colour
}
# Starting at $1, print a run of $2 colours
function print_run {
local i
for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
print_colour "$i"
done
printf " "
}
# Print blocks of colours
function print_blocks {
local start="$1" i
local end="$2" # inclusive
local block_cols="$3"
local block_rows="$4"
local blocks_per_line="$5"
local block_length=$((block_cols * block_rows))
# Print sets of blocks
for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
printf "\n" # Space before each set of blocks
# For each block row
for (( row = 0; row < block_rows; row++ )) do
# Print block columns for all blocks on the line
for (( block = 0; block < blocks_per_line; block++ )) do
print_run $(( i + (block * block_length) )) "$block_cols"
done
(( i += block_cols )) # Prepare to print the next row
printf "\n"
done
done
}
print_run 0 16 # The first 16 colours are spread over the whole spectrum
printf "\n"
print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Copied from: https://unix.stackexchange.com/a/696756
# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
s="/\\";
total_cols=term_cols*term_lines;
for (colnum = 0; colnum<total_cols; colnum++) {
r = 255-(colnum*255/total_cols);
g = (colnum*510/total_cols);
b = (colnum*255/total_cols);
if (g>255) g = 510-g;
printf "\033[48;2;%d;%d;%dm", r,g,b;
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
printf "%s\033[0m", substr(s,colnum%2+1,1);
if (colnum%term_cols==term_cols) printf "\n";
}
printf "\n";
}'

View File

@@ -0,0 +1,507 @@
use anyhow::Result;
use async_recursion::async_recursion;
use collections::HashSet;
use futures::future::join_all;
use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity};
use project::Project;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use ui::{App, Context, Window};
use util::ResultExt as _;
use db::{
query,
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use workspace::{
ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
WorkspaceDb, WorkspaceId,
};
use crate::{
TerminalView, default_working_directory,
terminal_panel::{TerminalPanel, new_terminal_pane},
};
pub(crate) fn serialize_pane_group(
pane_group: &PaneGroup,
active_pane: &Entity<Pane>,
cx: &mut App,
) -> SerializedPaneGroup {
build_serialized_pane_group(&pane_group.root, active_pane, cx)
}
fn build_serialized_pane_group(
pane_group: &Member,
active_pane: &Entity<Pane>,
cx: &mut App,
) -> SerializedPaneGroup {
match pane_group {
Member::Axis(PaneAxis {
axis,
members,
flexes,
bounding_boxes: _,
}) => SerializedPaneGroup::Group {
axis: SerializedAxis(*axis),
children: members
.iter()
.map(|member| build_serialized_pane_group(member, active_pane, cx))
.collect::<Vec<_>>(),
flexes: Some(flexes.lock().clone()),
},
Member::Pane(pane_handle) => {
SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx))
}
}
}
fn serialize_pane(pane: &Entity<Pane>, active: bool, cx: &mut App) -> SerializedPane {
let mut items_to_serialize = HashSet::default();
let pane = pane.read(cx);
let children = pane
.items()
.filter_map(|item| {
let terminal_view = item.act_as::<TerminalView>(cx)?;
if terminal_view.read(cx).terminal().read(cx).task().is_some() {
None
} else {
let id = item.item_id().as_u64();
items_to_serialize.insert(id);
Some(id)
}
})
.collect::<Vec<_>>();
let active_item = pane
.active_item()
.map(|item| item.item_id().as_u64())
.filter(|active_id| items_to_serialize.contains(active_id));
let pinned_count = pane.pinned_count();
SerializedPane {
active,
children,
active_item,
pinned_count,
}
}
pub(crate) fn deserialize_terminal_panel(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
database_id: WorkspaceId,
serialized_panel: SerializedTerminalPanel,
window: &mut Window,
cx: &mut App,
) -> Task<anyhow::Result<Entity<TerminalPanel>>> {
window.spawn(cx, async move |cx| {
let terminal_panel = workspace.update_in(cx, |workspace, window, cx| {
cx.new(|cx| TerminalPanel::new(workspace, window, cx))
})?;
match &serialized_panel.items {
SerializedItems::NoSplits(item_ids) => {
let items = deserialize_terminal_views(
database_id,
project,
workspace,
item_ids.as_slice(),
cx,
)
.await;
let active_item = serialized_panel.active_item_id;
terminal_panel.update_in(cx, |terminal_panel, window, cx| {
terminal_panel.active_pane.update(cx, |pane, cx| {
populate_pane_items(pane, items, active_item, window, cx);
});
})?;
}
SerializedItems::WithSplits(serialized_pane_group) => {
let center_pane = deserialize_pane_group(
workspace,
project,
terminal_panel.clone(),
database_id,
serialized_pane_group,
cx,
)
.await;
if let Some((center_group, active_pane)) = center_pane {
terminal_panel.update(cx, |terminal_panel, _| {
terminal_panel.center = PaneGroup::with_root(center_group);
terminal_panel.active_pane =
active_pane.unwrap_or_else(|| terminal_panel.center.first_pane());
});
}
}
}
Ok(terminal_panel)
})
}
fn populate_pane_items(
pane: &mut Pane,
items: Vec<Entity<TerminalView>>,
active_item: Option<u64>,
window: &mut Window,
cx: &mut Context<Pane>,
) {
let mut active_item_index = None;
for (item_index, item) in (pane.items_len()..).zip(items) {
if Some(item.item_id().as_u64()) == active_item {
active_item_index = Some(item_index);
}
pane.add_item(Box::new(item), false, false, None, window, cx);
}
if let Some(index) = active_item_index {
pane.activate_item(index, false, false, window, cx);
}
}
#[async_recursion(?Send)]
async fn deserialize_pane_group(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
panel: Entity<TerminalPanel>,
workspace_id: WorkspaceId,
serialized: &SerializedPaneGroup,
cx: &mut AsyncWindowContext,
) -> Option<(Member, Option<Entity<Pane>>)> {
match serialized {
SerializedPaneGroup::Group {
axis,
flexes,
children,
} => {
let mut current_active_pane = None;
let mut members = Vec::new();
for child in children {
if let Some((new_member, active_pane)) = deserialize_pane_group(
workspace.clone(),
project.clone(),
panel.clone(),
workspace_id,
child,
cx,
)
.await
{
members.push(new_member);
current_active_pane = current_active_pane.or(active_pane);
}
}
if members.is_empty() {
return None;
}
if members.len() == 1 {
return Some((members.remove(0), current_active_pane));
}
Some((
Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())),
current_active_pane,
))
}
SerializedPaneGroup::Pane(serialized_pane) => {
let active = serialized_pane.active;
let pane = panel
.update_in(cx, |terminal_panel, window, cx| {
new_terminal_pane(
workspace.clone(),
project.clone(),
terminal_panel.active_pane.read(cx).is_zoomed(),
window,
cx,
)
})
.log_err()?;
let active_item = serialized_pane.active_item;
let pinned_count = serialized_pane.pinned_count;
let new_items = deserialize_terminal_views(
workspace_id,
project.clone(),
workspace.clone(),
serialized_pane.children.as_slice(),
cx,
);
cx.spawn({
let pane = pane.downgrade();
async move |cx| {
let new_items = new_items.await;
let items = pane.update_in(cx, |pane, window, cx| {
populate_pane_items(pane, new_items, active_item, window, cx);
pane.set_pinned_count(pinned_count.min(pane.items_len()));
pane.items_len()
});
// Avoid blank panes in splits
if items.is_ok_and(|items| items == 0) {
let working_directory = workspace
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
.ok()
.flatten();
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_shell(working_directory, cx)
})
.await
.log_err();
let Some(terminal) = terminal else {
return;
};
pane.update_in(cx, |pane, window, cx| {
let terminal_view = Box::new(cx.new(|cx| {
TerminalView::new(
terminal,
workspace.clone(),
Some(workspace_id),
project.downgrade(),
window,
cx,
)
}));
pane.add_item(terminal_view, true, false, None, window, cx);
})
.ok();
}
}
})
.await;
Some((Member::Pane(pane.clone()), active.then_some(pane)))
}
}
}
fn deserialize_terminal_views(
workspace_id: WorkspaceId,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
item_ids: &[u64],
cx: &mut AsyncWindowContext,
) -> impl Future<Output = Vec<Entity<TerminalView>>> + use<> {
let deserialized_items = join_all(item_ids.iter().filter_map(|item_id| {
cx.update(|window, cx| {
TerminalView::deserialize(
project.clone(),
workspace.clone(),
workspace_id,
*item_id,
window,
cx,
)
})
.ok()
}));
async move {
deserialized_items
.await
.into_iter()
.filter_map(|item| item.log_err())
.collect()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SerializedTerminalPanel {
pub items: SerializedItems,
// A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced.
pub active_item_id: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum SerializedItems {
// The data stored before terminal splits were introduced.
NoSplits(Vec<u64>),
WithSplits(SerializedPaneGroup),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SerializedPaneGroup {
Pane(SerializedPane),
Group {
axis: SerializedAxis,
flexes: Option<Vec<f32>>,
children: Vec<SerializedPaneGroup>,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SerializedPane {
pub active: bool,
pub children: Vec<u64>,
pub active_item: Option<u64>,
#[serde(default)]
pub pinned_count: usize,
}
#[derive(Debug)]
pub(crate) struct SerializedAxis(pub Axis);
impl Serialize for SerializedAxis {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self.0 {
Axis::Horizontal => serializer.serialize_str("horizontal"),
Axis::Vertical => serializer.serialize_str("vertical"),
}
}
}
impl<'de> Deserialize<'de> for SerializedAxis {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"horizontal" => Ok(SerializedAxis(Axis::Horizontal)),
"vertical" => Ok(SerializedAxis(Axis::Vertical)),
invalid => Err(serde::de::Error::custom(format!(
"Invalid axis value: '{invalid}'"
))),
}
}
}
pub struct TerminalDb(ThreadSafeConnection);
impl Domain for TerminalDb {
const NAME: &str = stringify!(TerminalDb);
const MIGRATIONS: &[&str] = &[
sql!(
CREATE TABLE terminals (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
),
// Remove the unique constraint on the item_id table
// SQLite doesn't have a way of doing this automatically, so
// we have to do this silly copying.
sql!(
CREATE TABLE terminals2 (
workspace_id INTEGER,
item_id INTEGER,
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
INSERT INTO terminals2 (workspace_id, item_id, working_directory)
SELECT workspace_id, item_id, working_directory FROM terminals;
DROP TABLE terminals;
ALTER TABLE terminals2 RENAME TO terminals;
),
sql! (
ALTER TABLE terminals ADD COLUMN working_directory_path TEXT;
UPDATE terminals SET working_directory_path = CAST(working_directory AS TEXT);
),
sql! (
ALTER TABLE terminals ADD COLUMN custom_title TEXT;
),
];
}
db::static_connection!(TerminalDb, [WorkspaceDb]);
impl TerminalDb {
query! {
pub async fn update_workspace_id(
new_id: WorkspaceId,
old_id: WorkspaceId,
item_id: ItemId
) -> Result<()> {
UPDATE terminals
SET workspace_id = ?
WHERE workspace_id = ? AND item_id = ?
}
}
pub async fn save_working_directory(
&self,
item_id: ItemId,
workspace_id: WorkspaceId,
working_directory: PathBuf,
) -> Result<()> {
log::debug!(
"Saving working directory {working_directory:?} for item {item_id} in workspace {workspace_id:?}"
);
let query =
"INSERT INTO terminals(item_id, workspace_id, working_directory, working_directory_path)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2,
working_directory = ?3,
working_directory_path = ?4"
;
self.write(move |conn| {
let mut statement = Statement::prepare(conn, query)?;
let mut next_index = statement.bind(&item_id, 1)?;
next_index = statement.bind(&workspace_id, next_index)?;
next_index = statement.bind(&working_directory, next_index)?;
statement.bind(
&working_directory.to_string_lossy().into_owned(),
next_index,
)?;
statement.exec()
})
.await
}
query! {
pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
SELECT working_directory
FROM terminals
WHERE item_id = ? AND workspace_id = ?
}
}
pub async fn save_custom_title(
&self,
item_id: ItemId,
workspace_id: WorkspaceId,
custom_title: Option<String>,
) -> Result<()> {
log::debug!(
"Saving custom title {:?} for item {} in workspace {:?}",
custom_title,
item_id,
workspace_id
);
self.write(move |conn| {
let query = "INSERT INTO terminals (item_id, workspace_id, custom_title)
VALUES (?1, ?2, ?3)
ON CONFLICT (workspace_id, item_id) DO UPDATE SET
custom_title = excluded.custom_title";
let mut statement = Statement::prepare(conn, query)?;
let mut next_index = statement.bind(&item_id, 1)?;
next_index = statement.bind(&workspace_id, next_index)?;
statement.bind(&custom_title, next_index)?;
statement.exec()
})
.await
}
query! {
pub fn get_custom_title(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<String>> {
SELECT custom_title
FROM terminals
WHERE item_id = ? AND workspace_id = ?
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use gpui::{Bounds, Point, point, size};
use terminal::Terminal;
use ui::{Pixels, ScrollableHandle, px};
#[derive(Debug)]
struct ScrollHandleState {
line_height: Pixels,
total_lines: usize,
viewport_lines: usize,
display_offset: usize,
}
impl ScrollHandleState {
fn new(terminal: &Terminal) -> Self {
Self {
line_height: terminal.last_content().terminal_bounds.line_height,
total_lines: terminal.total_lines(),
viewport_lines: terminal.viewport_lines(),
display_offset: terminal.last_content().display_offset,
}
}
}
#[derive(Debug, Clone)]
pub struct TerminalScrollHandle {
state: Rc<RefCell<ScrollHandleState>>,
pub future_display_offset: Rc<Cell<Option<usize>>>,
}
impl TerminalScrollHandle {
pub fn new(terminal: &Terminal) -> Self {
Self {
state: Rc::new(RefCell::new(ScrollHandleState::new(terminal))),
future_display_offset: Rc::new(Cell::new(None)),
}
}
pub fn update(&self, terminal: &Terminal) {
*self.state.borrow_mut() = ScrollHandleState::new(terminal);
}
}
impl ScrollableHandle for TerminalScrollHandle {
fn max_offset(&self) -> Point<Pixels> {
let state = self.state.borrow();
point(
Pixels::ZERO,
state.total_lines.saturating_sub(state.viewport_lines) as f32 * state.line_height,
)
}
fn offset(&self) -> Point<Pixels> {
let state = self.state.borrow();
let scroll_offset = state
.total_lines
.saturating_sub(state.viewport_lines)
.saturating_sub(state.display_offset);
Point::new(Pixels::ZERO, -(scroll_offset as f32 * state.line_height))
}
fn set_offset(&self, point: Point<Pixels>) {
let state = self.state.borrow();
let offset_delta = (point.y / state.line_height).round() as i32;
let max_offset = state.total_lines.saturating_sub(state.viewport_lines);
let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32);
self.future_display_offset
.set(Some(display_offset as usize));
}
fn viewport(&self) -> Bounds<Pixels> {
let state = self.state.borrow();
Bounds::new(
Point::new(px(0.), px(0.)),
size(
Pixels::ZERO,
state.viewport_lines as f32 * state.line_height,
),
)
}
}

File diff suppressed because it is too large Load Diff