forked from NotAShelf/beer
render: cursor shapes, visibility, and focus
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Iad508cceb2c8417147ad71b5c1ffc4bc6a6a6964
This commit is contained in:
parent
2afb4875be
commit
88df7c2404
4 changed files with 213 additions and 17 deletions
40
src/grid.rs
40
src/grid.rs
|
|
@ -61,6 +61,15 @@ pub enum Underline {
|
||||||
Dashed,
|
Dashed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cursor shape (DECSCUSR).
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||||
|
pub enum CursorShape {
|
||||||
|
#[default]
|
||||||
|
Block,
|
||||||
|
Underline,
|
||||||
|
Beam,
|
||||||
|
}
|
||||||
|
|
||||||
/// One grid cell: a character plus its rendering style.
|
/// One grid cell: a character plus its rendering style.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Cell {
|
pub struct Cell {
|
||||||
|
|
@ -113,6 +122,10 @@ pub struct Grid {
|
||||||
tabs: Vec<bool>,
|
tabs: Vec<bool>,
|
||||||
/// Saved primary screen while the alternate screen is active.
|
/// Saved primary screen while the alternate screen is active.
|
||||||
alt_saved: Option<Vec<Vec<Cell>>>,
|
alt_saved: Option<Vec<Vec<Cell>>>,
|
||||||
|
cursor_shape: CursorShape,
|
||||||
|
cursor_visible: bool,
|
||||||
|
/// Cursor colour from OSC 12; `None` follows the cell under the cursor.
|
||||||
|
cursor_color: Option<(u8, u8, u8)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tabs(cols: usize) -> Vec<bool> {
|
fn default_tabs(cols: usize) -> Vec<bool> {
|
||||||
|
|
@ -138,6 +151,9 @@ impl Grid {
|
||||||
wrap_pending: false,
|
wrap_pending: false,
|
||||||
tabs: default_tabs(cols),
|
tabs: default_tabs(cols),
|
||||||
alt_saved: None,
|
alt_saved: None,
|
||||||
|
cursor_shape: CursorShape::default(),
|
||||||
|
cursor_visible: true,
|
||||||
|
cursor_color: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +226,30 @@ impl Grid {
|
||||||
self.alt_saved.is_some()
|
self.alt_saved.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_cursor_shape(&mut self, shape: CursorShape) {
|
||||||
|
self.cursor_shape = shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_shape(&self) -> CursorShape {
|
||||||
|
self.cursor_shape
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cursor_visible(&mut self, visible: bool) {
|
||||||
|
self.cursor_visible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_visible(&self) -> bool {
|
||||||
|
self.cursor_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cursor_color(&mut self, color: Option<(u8, u8, u8)>) {
|
||||||
|
self.cursor_color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_color(&self) -> Option<(u8, u8, u8)> {
|
||||||
|
self.cursor_color
|
||||||
|
}
|
||||||
|
|
||||||
// --- printing ---
|
// --- printing ---
|
||||||
|
|
||||||
/// Place a printable character at the cursor, honouring width and autowrap.
|
/// Place a printable character at the cursor, honouring width and autowrap.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
//! neighbouring cell's background fill.
|
//! neighbouring cell's background fill.
|
||||||
|
|
||||||
use crate::font::{CellMetrics, Fonts, GlyphData, Style};
|
use crate::font::{CellMetrics, Fonts, GlyphData, Style};
|
||||||
use crate::grid::{Cell, Color, Flags, Grid, Underline};
|
use crate::grid::{Cell, Color, CursorShape, Flags, Grid, Underline};
|
||||||
|
|
||||||
/// Foreground/background used for `Color::Default`.
|
/// Foreground/background used for `Color::Default`.
|
||||||
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
|
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
|
||||||
|
|
@ -94,9 +94,16 @@ impl Renderer {
|
||||||
self.fonts.metrics()
|
self.fonts.metrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compose `grid` into `pixels` (BGRA, `width`×`height` px). The cursor cell
|
/// Compose `grid` into `pixels` (BGRA, `width`×`height` px). `focused`
|
||||||
/// is drawn reversed.
|
/// selects a solid or hollow cursor.
|
||||||
pub fn render(&mut self, grid: &Grid, pixels: &mut [u8], width: usize, height: usize) {
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
grid: &Grid,
|
||||||
|
pixels: &mut [u8],
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
focused: bool,
|
||||||
|
) {
|
||||||
let mut canvas = Canvas {
|
let mut canvas = Canvas {
|
||||||
pixels,
|
pixels,
|
||||||
width,
|
width,
|
||||||
|
|
@ -105,11 +112,10 @@ impl Renderer {
|
||||||
canvas.fill_rect(0, 0, width as u32, height as u32, DEFAULT_BG);
|
canvas.fill_rect(0, 0, width as u32, height as u32, DEFAULT_BG);
|
||||||
|
|
||||||
let m = self.fonts.metrics();
|
let m = self.fonts.metrics();
|
||||||
let cursor = grid.cursor();
|
|
||||||
|
|
||||||
for y in 0..grid.rows() {
|
for y in 0..grid.rows() {
|
||||||
for x in 0..grid.cols() {
|
for x in 0..grid.cols() {
|
||||||
let (_, bg) = cell_colors(grid.cell(x, y), (x, y) == cursor);
|
let (_, bg) = cell_colors(grid.cell(x, y));
|
||||||
let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
|
let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
|
||||||
canvas.fill_rect(px, py, m.width, m.height, bg);
|
canvas.fill_rect(px, py, m.width, m.height, bg);
|
||||||
}
|
}
|
||||||
|
|
@ -121,18 +127,14 @@ impl Renderer {
|
||||||
if cell.flags.contains(Flags::WIDE_CONT) {
|
if cell.flags.contains(Flags::WIDE_CONT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (fg, _) = cell_colors(cell, (x, y) == cursor);
|
let (fg, _) = cell_colors(cell);
|
||||||
let origin_x = x as i32 * m.width as i32;
|
let origin_x = x as i32 * m.width as i32;
|
||||||
let cell_top = y as i32 * m.height as i32;
|
let cell_top = y as i32 * m.height as i32;
|
||||||
if cell.c != ' ' {
|
if cell.c != ' ' {
|
||||||
let style = Style {
|
|
||||||
bold: cell.flags.contains(Flags::BOLD),
|
|
||||||
italic: cell.flags.contains(Flags::ITALIC),
|
|
||||||
};
|
|
||||||
self.draw_glyph(
|
self.draw_glyph(
|
||||||
&mut canvas,
|
&mut canvas,
|
||||||
cell.c,
|
cell.c,
|
||||||
style,
|
cell_style(cell),
|
||||||
origin_x,
|
origin_x,
|
||||||
cell_top + m.ascent as i32,
|
cell_top + m.ascent as i32,
|
||||||
fg,
|
fg,
|
||||||
|
|
@ -141,6 +143,54 @@ impl Renderer {
|
||||||
draw_decorations(&mut canvas, cell, origin_x, cell_top, m, fg);
|
draw_decorations(&mut canvas, cell, origin_x, cell_top, m, fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.draw_cursor(&mut canvas, grid, m, focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
||||||
|
/// outline when not.
|
||||||
|
fn draw_cursor(&mut self, canvas: &mut Canvas, grid: &Grid, m: CellMetrics, focused: bool) {
|
||||||
|
if !grid.cursor_visible() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (cx, cy) = grid.cursor();
|
||||||
|
let x0 = cx as i32 * m.width as i32;
|
||||||
|
let top = cy as i32 * m.height as i32;
|
||||||
|
let color = grid
|
||||||
|
.cursor_color()
|
||||||
|
.map_or(DEFAULT_FG, |(r, g, b)| Rgb(r, g, b));
|
||||||
|
|
||||||
|
if !focused {
|
||||||
|
let right = x0 + m.width as i32 - 1;
|
||||||
|
let bottom = top + m.height as i32 - 1;
|
||||||
|
canvas.hline(x0, top, m.width, color);
|
||||||
|
canvas.hline(x0, bottom, m.width, color);
|
||||||
|
canvas.fill_rect(x0, top, 1, m.height, color);
|
||||||
|
canvas.fill_rect(right, top, 1, m.height, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match grid.cursor_shape() {
|
||||||
|
CursorShape::Block => {
|
||||||
|
canvas.fill_rect(x0, top, m.width, m.height, color);
|
||||||
|
let cell = grid.cell(cx, cy);
|
||||||
|
if cell.c != ' ' && !cell.flags.contains(Flags::WIDE_CONT) {
|
||||||
|
let (_, bg) = cell_colors(cell);
|
||||||
|
self.draw_glyph(
|
||||||
|
canvas,
|
||||||
|
cell.c,
|
||||||
|
cell_style(cell),
|
||||||
|
x0,
|
||||||
|
top + m.ascent as i32,
|
||||||
|
bg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CursorShape::Underline => {
|
||||||
|
canvas.fill_rect(x0, top + m.height as i32 - 2, m.width, 2, color);
|
||||||
|
}
|
||||||
|
CursorShape::Beam => canvas.fill_rect(x0, top, 2, m.height, color),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_glyph(
|
fn draw_glyph(
|
||||||
|
|
@ -188,13 +238,20 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cell_style(cell: &Cell) -> Style {
|
||||||
|
Style {
|
||||||
|
bold: cell.flags.contains(Flags::BOLD),
|
||||||
|
italic: cell.flags.contains(Flags::ITALIC),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a cell's (foreground, background) RGB, applying reverse video,
|
/// Resolve a cell's (foreground, background) RGB, applying reverse video,
|
||||||
/// bold-as-bright, dim, and hidden.
|
/// bold-as-bright, dim, and hidden.
|
||||||
fn cell_colors(cell: &Cell, cursor: bool) -> (Rgb, Rgb) {
|
fn cell_colors(cell: &Cell) -> (Rgb, Rgb) {
|
||||||
let bold = cell.flags.contains(Flags::BOLD);
|
let bold = cell.flags.contains(Flags::BOLD);
|
||||||
let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
|
let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
|
||||||
let mut bg = resolve(cell.bg, DEFAULT_BG, false);
|
let mut bg = resolve(cell.bg, DEFAULT_BG, false);
|
||||||
if cell.flags.contains(Flags::REVERSE) ^ cursor {
|
if cell.flags.contains(Flags::REVERSE) {
|
||||||
std::mem::swap(&mut fg, &mut bg);
|
std::mem::swap(&mut fg, &mut bg);
|
||||||
}
|
}
|
||||||
if cell.flags.contains(Flags::DIM) {
|
if cell.flags.contains(Flags::DIM) {
|
||||||
|
|
|
||||||
92
src/vt.rs
92
src/vt.rs
|
|
@ -4,7 +4,7 @@ use std::io::Write as _;
|
||||||
|
|
||||||
use vte::{Params, Perform};
|
use vte::{Params, Perform};
|
||||||
|
|
||||||
use crate::grid::{Color, Flags, Grid, Underline};
|
use crate::grid::{Color, CursorShape, Flags, Grid, Underline};
|
||||||
|
|
||||||
/// G0/G1 character set designation.
|
/// G0/G1 character set designation.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|
@ -26,6 +26,31 @@ fn set_reset(on: bool) -> u8 {
|
||||||
if on { 1 } else { 2 }
|
if on { 1 } else { 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse an X11 colour spec from an OSC string: `rgb:rr/gg/bb` (1-4 hex
|
||||||
|
/// digits per channel) or `#rrggbb`.
|
||||||
|
fn parse_color(spec: &[u8]) -> Option<(u8, u8, u8)> {
|
||||||
|
let spec = std::str::from_utf8(spec).ok()?;
|
||||||
|
if let Some(rest) = spec.strip_prefix("rgb:") {
|
||||||
|
let mut it = rest.split('/');
|
||||||
|
let chan = |s: &str| -> Option<u8> {
|
||||||
|
// Normalize an n-hex-digit fraction to 8 bits (X11 rule).
|
||||||
|
let v = u32::from_str_radix(s, 16).ok()?;
|
||||||
|
let max = (1u32 << (4 * s.len() as u32)) - 1;
|
||||||
|
Some((v * 255 / max) as u8)
|
||||||
|
};
|
||||||
|
let r = chan(it.next()?)?;
|
||||||
|
let g = chan(it.next()?)?;
|
||||||
|
let b = chan(it.next()?)?;
|
||||||
|
return Some((r, g, b));
|
||||||
|
}
|
||||||
|
let hex = spec.strip_prefix('#')?;
|
||||||
|
if hex.len() == 6 {
|
||||||
|
let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok();
|
||||||
|
return Some((byte(0)?, byte(2)?, byte(4)?));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Map an SGR 4 param (`4` or `4:x`) to an underline style.
|
/// Map an SGR 4 param (`4` or `4:x`) to an underline style.
|
||||||
fn underline_from(param: &[u16]) -> Underline {
|
fn underline_from(param: &[u16]) -> Underline {
|
||||||
match param.get(1).copied().unwrap_or(1) {
|
match param.get(1).copied().unwrap_or(1) {
|
||||||
|
|
@ -108,6 +133,7 @@ impl Term {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(false, 4) => self.grid.set_insert(on),
|
(false, 4) => self.grid.set_insert(on),
|
||||||
|
(true, 25) => self.grid.set_cursor_visible(on),
|
||||||
// App-cursor/bracketed-paste/mouse/sync modes affect input and
|
// App-cursor/bracketed-paste/mouse/sync modes affect input and
|
||||||
// rendering, which arrive with the keyboard and renderer.
|
// rendering, which arrive with the keyboard and renderer.
|
||||||
_ => tracing::trace!("unhandled mode {code} private={private} on={on}"),
|
_ => tracing::trace!("unhandled mode {code} private={private} on={on}"),
|
||||||
|
|
@ -400,6 +426,13 @@ impl Perform for Term {
|
||||||
_ => DaLevel::Primary,
|
_ => DaLevel::Primary,
|
||||||
}),
|
}),
|
||||||
'q' if intermediates.first() == Some(&b'>') => self.report_version(),
|
'q' if intermediates.first() == Some(&b'>') => self.report_version(),
|
||||||
|
'q' if intermediates.first() == Some(&b' ') => {
|
||||||
|
self.grid.set_cursor_shape(match raw(params, 0) {
|
||||||
|
3 | 4 => CursorShape::Underline,
|
||||||
|
5 | 6 => CursorShape::Beam,
|
||||||
|
_ => CursorShape::Block,
|
||||||
|
});
|
||||||
|
}
|
||||||
'p' if intermediates.contains(&b'$') => self.report_mode(params, private),
|
'p' if intermediates.contains(&b'$') => self.report_mode(params, private),
|
||||||
'n' => self.device_status(params),
|
'n' => self.device_status(params),
|
||||||
's' => self.grid.save_cursor(),
|
's' => self.grid.save_cursor(),
|
||||||
|
|
@ -445,6 +478,12 @@ impl Perform for Term {
|
||||||
self.title = Some(String::from_utf8_lossy(text).into_owned());
|
self.title = Some(String::from_utf8_lossy(text).into_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// OSC 12: set cursor colour; OSC 112: reset to default.
|
||||||
|
Some(&n) if n == b"12" => {
|
||||||
|
self.grid
|
||||||
|
.set_cursor_color(params.get(1).and_then(|s| parse_color(s)));
|
||||||
|
}
|
||||||
|
Some(&n) if n == b"112" => self.grid.set_cursor_color(None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -529,6 +568,57 @@ mod tests {
|
||||||
assert_eq!(t.grid().cell(1, 0).underline, Underline::None);
|
assert_eq!(t.grid().cell(1, 0).underline, Underline::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decscusr_and_cursor_visibility() {
|
||||||
|
let mut t = Term::new(20, 1);
|
||||||
|
feed(&mut t, b"\x1b[4 q");
|
||||||
|
assert_eq!(t.grid().cursor_shape(), CursorShape::Underline);
|
||||||
|
feed(&mut t, b"\x1b[6 q");
|
||||||
|
assert_eq!(t.grid().cursor_shape(), CursorShape::Beam);
|
||||||
|
feed(&mut t, b"\x1b[0 q");
|
||||||
|
assert_eq!(t.grid().cursor_shape(), CursorShape::Block);
|
||||||
|
|
||||||
|
feed(&mut t, b"\x1b[?25l");
|
||||||
|
assert!(!t.grid().cursor_visible());
|
||||||
|
feed(&mut t, b"\x1b[?25h");
|
||||||
|
assert!(t.grid().cursor_visible());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc12_sets_and_resets_cursor_color() {
|
||||||
|
let mut t = Term::new(20, 1);
|
||||||
|
feed(&mut t, b"\x1b]12;#ff0000\x07");
|
||||||
|
assert_eq!(t.grid().cursor_color(), Some((255, 0, 0)));
|
||||||
|
feed(&mut t, b"\x1b]12;rgb:00/80/ff\x07");
|
||||||
|
assert_eq!(t.grid().cursor_color(), Some((0, 0x80, 0xff)));
|
||||||
|
feed(&mut t, b"\x1b]112\x07");
|
||||||
|
assert_eq!(t.grid().cursor_color(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decscusr_and_cursor_color() {
|
||||||
|
use crate::grid::CursorShape;
|
||||||
|
let mut t = Term::new(20, 1);
|
||||||
|
feed(&mut t, b"\x1b[5 q"); // blinking bar
|
||||||
|
assert_eq!(t.grid().cursor_shape(), CursorShape::Beam);
|
||||||
|
feed(&mut t, b"\x1b[4 q"); // steady underline
|
||||||
|
assert_eq!(t.grid().cursor_shape(), CursorShape::Underline);
|
||||||
|
feed(&mut t, b"\x1b]12;#ff3030\x07");
|
||||||
|
assert_eq!(t.grid().cursor_color(), Some((0xff, 0x30, 0x30)));
|
||||||
|
feed(&mut t, b"\x1b]112\x07");
|
||||||
|
assert_eq!(t.grid().cursor_color(), None);
|
||||||
|
feed(&mut t, b"\x1b[?25l"); // hide cursor
|
||||||
|
assert!(!t.grid().cursor_visible());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_color_forms() {
|
||||||
|
assert_eq!(parse_color(b"rgb:ff/00/80"), Some((255, 0, 128)));
|
||||||
|
assert_eq!(parse_color(b"rgb:ffff/0000/8080"), Some((255, 0, 128)));
|
||||||
|
assert_eq!(parse_color(b"#ff0080"), Some((255, 0, 128)));
|
||||||
|
assert_eq!(parse_color(b"nonsense"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn title_stack_push_pop() {
|
fn title_stack_push_pop() {
|
||||||
let mut t = Term::new(20, 4);
|
let mut t = Term::new(20, 4);
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
width: DEFAULT_W,
|
width: DEFAULT_W,
|
||||||
height: DEFAULT_H,
|
height: DEFAULT_H,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
|
focused: true,
|
||||||
exit: false,
|
exit: false,
|
||||||
exit_code: ExitCode::SUCCESS,
|
exit_code: ExitCode::SUCCESS,
|
||||||
};
|
};
|
||||||
|
|
@ -182,6 +183,8 @@ struct App {
|
||||||
height: u32,
|
height: u32,
|
||||||
/// The grid changed and the window needs repainting.
|
/// The grid changed and the window needs repainting.
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
|
/// Whether the toplevel currently has keyboard focus (drives the cursor).
|
||||||
|
focused: bool,
|
||||||
exit: bool,
|
exit: bool,
|
||||||
/// Exit code to return, taken from the shell when it exits.
|
/// Exit code to return, taken from the shell when it exits.
|
||||||
exit_code: ExitCode,
|
exit_code: ExitCode,
|
||||||
|
|
@ -252,8 +255,13 @@ impl App {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.renderer
|
self.renderer.render(
|
||||||
.render(self.term.grid(), canvas, w as usize, h as usize);
|
self.term.grid(),
|
||||||
|
canvas,
|
||||||
|
w as usize,
|
||||||
|
h as usize,
|
||||||
|
self.focused,
|
||||||
|
);
|
||||||
|
|
||||||
let surface = self.window.wl_surface();
|
let surface = self.window.wl_surface();
|
||||||
if let Err(err) = buffer.attach_to(surface) {
|
if let Err(err) = buffer.attach_to(surface) {
|
||||||
|
|
@ -322,6 +330,7 @@ impl WindowHandler for App {
|
||||||
self.width = w.get();
|
self.width = w.get();
|
||||||
self.height = h.get();
|
self.height = h.get();
|
||||||
}
|
}
|
||||||
|
self.focused = configure.is_activated();
|
||||||
self.resize_grid();
|
self.resize_grid();
|
||||||
self.draw();
|
self.draw();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue