Skip to content

Commit 87bfb0f

Browse files
committed
Select by character or color and fill with the brush on home computer screens
The ATASCII, VT52 and PETSCII editors show the select tool's modes and actions like the ANSI editor. Filling a selection there paints the brush into every selected character (its character and colors, only colors, reverse or a shading step) instead of CP437's full block, so selecting a character and filling replaces it.
1 parent 8163588 commit 87bfb0f

5 files changed

Lines changed: 188 additions & 113 deletions

File tree

‎crates/icy_draw/src/bin/icy_draw_egui/app.rs‎

Lines changed: 113 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,118 @@ impl DrawApp {
12221222
}
12231223
}
12241224

1225+
/// The select tool's modes and what it does with the selection, in every editor's toolbar.
1226+
pub(super) fn selection_options(&mut self, ui: &mut egui::Ui, context: &egui::Context) {
1227+
use icy_draw::document::SelectionMode;
1228+
let mut mode = self.document.selection_mode;
1229+
if widgets::segmented(
1230+
ui,
1231+
&mut mode,
1232+
&[
1233+
(SelectionMode::Rectangle, fl!("tool-select-normal"), fl!("select-mode-normal-tooltip")),
1234+
(SelectionMode::Character, fl!("tool-select-character"), fl!("select-mode-character-tooltip")),
1235+
(SelectionMode::Attribute, fl!("tool-select-attribute"), fl!("select-mode-attribute-tooltip")),
1236+
(SelectionMode::Foreground, fl!("tool-select-foreground"), fl!("select-mode-foreground-tooltip")),
1237+
(SelectionMode::Background, fl!("tool-select-background"), fl!("select-mode-background-tooltip")),
1238+
],
1239+
) {
1240+
self.document.finish();
1241+
self.document.selection_mode = mode;
1242+
}
1243+
widgets::divider(ui);
1244+
let selected = self.document.with_state(|state| state.is_something_selected());
1245+
let paint = self.document.can_paint();
1246+
let shortcut = |label: String, shortcut: &egui::KeyboardShortcut| format!("{label} ({})", context.format_shortcut(shortcut));
1247+
let key = |label: String, key: &str| format!("{label} ({key})");
1248+
if self
1249+
.icons
1250+
.button(ui, "select", &shortcut(fl!("menu-select-all"), &menus::SELECT_ALL), false)
1251+
.clicked()
1252+
{
1253+
self.select_all();
1254+
}
1255+
ui.add_enabled_ui(selected, |ui| {
1256+
ui.spacing_mut().item_spacing.x = 4.0;
1257+
if self
1258+
.icons
1259+
.button(ui, "deselect", &shortcut(fl!("select-deselect"), &menus::DESELECT), false)
1260+
.clicked()
1261+
{
1262+
self.edit(|state| state.clear_selection());
1263+
}
1264+
widgets::divider(ui);
1265+
if self.icons.button(ui, "file_copy", &fl!("select-copy"), false).clicked() {
1266+
self.copy(context);
1267+
}
1268+
ui.add_enabled_ui(paint, |ui| {
1269+
ui.spacing_mut().item_spacing.x = 4.0;
1270+
if self.icons.button(ui, "library_add", &key(fl!("shortcut-block-copy"), "C"), false).clicked() {
1271+
let result = self.document.float_selection(false);
1272+
self.result(result);
1273+
}
1274+
if self.icons.button(ui, "move", &key(fl!("shortcut-block-move"), "M"), false).clicked() {
1275+
let result = self.document.float_selection(true);
1276+
self.result(result);
1277+
}
1278+
widgets::divider(ui);
1279+
if self.icons.button(ui, "fill", &key(fl!("shortcut-block-fill"), "F"), false).clicked() {
1280+
let result = self.document.fill_selection();
1281+
self.result(result);
1282+
}
1283+
let erase = egui::KeyboardShortcut::new(egui::Modifiers::NONE, egui::Key::Delete);
1284+
if self
1285+
.icons
1286+
.button(ui, "eraser", &shortcut(fl!("shortcut-erase-selection"), &erase), false)
1287+
.clicked()
1288+
{
1289+
self.document.finish();
1290+
self.edit(|state| state.erase_selection());
1291+
}
1292+
if self.icons.button(ui, "crop", &shortcut(fl!("menu-crop"), &menus::CROP), false).clicked() {
1293+
self.edit(|state| state.crop());
1294+
}
1295+
widgets::divider(ui);
1296+
if self.icons.button(ui, "flip_tool", &fl!("menu-flip-x"), false).clicked() {
1297+
self.edit(|state| state.flip_x());
1298+
}
1299+
if self.icons.button(ui, "swap", &fl!("menu-flip-y"), false).clicked() {
1300+
self.edit(|state| state.flip_y());
1301+
}
1302+
let justify = self.icons.button(ui, "format_align_center", &fl!("select-justify"), false);
1303+
egui::Popup::menu(&justify).id(egui::Id::new("select-justify")).show(|ui| {
1304+
if ui.button(fl!("menu-justifyleft")).clicked() {
1305+
self.edit(|state| state.justify_left());
1306+
ui.close();
1307+
}
1308+
if ui.button(fl!("menu-justifycenter")).clicked() {
1309+
self.edit(|state| state.center());
1310+
ui.close();
1311+
}
1312+
if ui.button(fl!("menu-justifyright")).clicked() {
1313+
self.edit(|state| state.justify_right());
1314+
ui.close();
1315+
}
1316+
});
1317+
});
1318+
});
1319+
widgets::divider(ui);
1320+
match selection_add_type(ui.input(|input| input.modifiers)) {
1321+
AddType::Add => {
1322+
ui.label(egui::RichText::new(fl!("select-mode-add")).color(appearance::PRIMARY).strong());
1323+
}
1324+
AddType::Subtract => {
1325+
ui.label(egui::RichText::new(fl!("select-mode-subtract")).color(appearance::PRIMARY).strong());
1326+
}
1327+
AddType::Default => {
1328+
if let Some(bounds) = self.document.with_state(|state| state.selection().map(|selection| selection.as_rectangle())) {
1329+
ui.weak(format!("{}, {} · {} × {}", bounds.left(), bounds.top(), bounds.width(), bounds.height()));
1330+
} else {
1331+
ui.weak(fl!("tool-select-description"));
1332+
}
1333+
}
1334+
}
1335+
}
1336+
12251337
/// Anchoring, a new layer, stamping, rotating, flipping and cancelling of a floating paste.
12261338
pub(super) fn paste_options(&mut self, ui: &mut egui::Ui) {
12271339
use icy_draw::document::PasteAction;
@@ -1484,116 +1596,7 @@ impl DrawApp {
14841596
Tool::Pipette => {
14851597
self.pipette_options(ui);
14861598
}
1487-
Tool::Select => {
1488-
use icy_draw::document::SelectionMode;
1489-
let mut mode = self.document.selection_mode;
1490-
if widgets::segmented(
1491-
ui,
1492-
&mut mode,
1493-
&[
1494-
(SelectionMode::Rectangle, fl!("tool-select-normal"), fl!("select-mode-normal-tooltip")),
1495-
(SelectionMode::Character, fl!("tool-select-character"), fl!("select-mode-character-tooltip")),
1496-
(SelectionMode::Attribute, fl!("tool-select-attribute"), fl!("select-mode-attribute-tooltip")),
1497-
(SelectionMode::Foreground, fl!("tool-select-foreground"), fl!("select-mode-foreground-tooltip")),
1498-
(SelectionMode::Background, fl!("tool-select-background"), fl!("select-mode-background-tooltip")),
1499-
],
1500-
) {
1501-
self.document.finish();
1502-
self.document.selection_mode = mode;
1503-
}
1504-
widgets::divider(ui);
1505-
let selected = self.document.with_state(|state| state.is_something_selected());
1506-
let paint = self.document.can_paint();
1507-
let shortcut = |label: String, shortcut: &egui::KeyboardShortcut| format!("{label} ({})", context.format_shortcut(shortcut));
1508-
let key = |label: String, key: &str| format!("{label} ({key})");
1509-
if self
1510-
.icons
1511-
.button(ui, "select", &shortcut(fl!("menu-select-all"), &menus::SELECT_ALL), false)
1512-
.clicked()
1513-
{
1514-
self.select_all();
1515-
}
1516-
ui.add_enabled_ui(selected, |ui| {
1517-
ui.spacing_mut().item_spacing.x = 4.0;
1518-
if self
1519-
.icons
1520-
.button(ui, "deselect", &shortcut(fl!("select-deselect"), &menus::DESELECT), false)
1521-
.clicked()
1522-
{
1523-
self.edit(|state| state.clear_selection());
1524-
}
1525-
widgets::divider(ui);
1526-
if self.icons.button(ui, "file_copy", &fl!("select-copy"), false).clicked() {
1527-
self.copy(context);
1528-
}
1529-
ui.add_enabled_ui(paint, |ui| {
1530-
ui.spacing_mut().item_spacing.x = 4.0;
1531-
if self.icons.button(ui, "library_add", &key(fl!("shortcut-block-copy"), "C"), false).clicked() {
1532-
let result = self.document.float_selection(false);
1533-
self.result(result);
1534-
}
1535-
if self.icons.button(ui, "move", &key(fl!("shortcut-block-move"), "M"), false).clicked() {
1536-
let result = self.document.float_selection(true);
1537-
self.result(result);
1538-
}
1539-
widgets::divider(ui);
1540-
if self.icons.button(ui, "fill", &key(fl!("shortcut-block-fill"), "F"), false).clicked() {
1541-
let result = self.document.fill_selection();
1542-
self.result(result);
1543-
}
1544-
let erase = egui::KeyboardShortcut::new(egui::Modifiers::NONE, egui::Key::Delete);
1545-
if self
1546-
.icons
1547-
.button(ui, "eraser", &shortcut(fl!("shortcut-erase-selection"), &erase), false)
1548-
.clicked()
1549-
{
1550-
self.document.finish();
1551-
self.edit(|state| state.erase_selection());
1552-
}
1553-
if self.icons.button(ui, "crop", &shortcut(fl!("menu-crop"), &menus::CROP), false).clicked() {
1554-
self.edit(|state| state.crop());
1555-
}
1556-
widgets::divider(ui);
1557-
if self.icons.button(ui, "flip_tool", &fl!("menu-flip-x"), false).clicked() {
1558-
self.edit(|state| state.flip_x());
1559-
}
1560-
if self.icons.button(ui, "swap", &fl!("menu-flip-y"), false).clicked() {
1561-
self.edit(|state| state.flip_y());
1562-
}
1563-
let justify = self.icons.button(ui, "format_align_center", &fl!("select-justify"), false);
1564-
egui::Popup::menu(&justify).id(egui::Id::new("select-justify")).show(|ui| {
1565-
if ui.button(fl!("menu-justifyleft")).clicked() {
1566-
self.edit(|state| state.justify_left());
1567-
ui.close();
1568-
}
1569-
if ui.button(fl!("menu-justifycenter")).clicked() {
1570-
self.edit(|state| state.center());
1571-
ui.close();
1572-
}
1573-
if ui.button(fl!("menu-justifyright")).clicked() {
1574-
self.edit(|state| state.justify_right());
1575-
ui.close();
1576-
}
1577-
});
1578-
});
1579-
});
1580-
widgets::divider(ui);
1581-
match selection_add_type(ui.input(|input| input.modifiers)) {
1582-
AddType::Add => {
1583-
ui.label(egui::RichText::new(fl!("select-mode-add")).color(appearance::PRIMARY).strong());
1584-
}
1585-
AddType::Subtract => {
1586-
ui.label(egui::RichText::new(fl!("select-mode-subtract")).color(appearance::PRIMARY).strong());
1587-
}
1588-
AddType::Default => {
1589-
if let Some(bounds) = self.document.with_state(|state| state.selection().map(|selection| selection.as_rectangle())) {
1590-
ui.weak(format!("{}, {} · {} × {}", bounds.left(), bounds.top(), bounds.width(), bounds.height()));
1591-
} else {
1592-
ui.weak(fl!("tool-select-description"));
1593-
}
1594-
}
1595-
}
1596-
}
1599+
Tool::Select => self.selection_options(ui, context),
15971600

15981601
Tool::Tag => {
15991602
if self.icons.button(ui, "tag", &fl!("tag-list-title"), false).clicked() {

‎crates/icy_draw/src/bin/icy_draw_egui/atascii.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,13 @@ impl DrawApp {
457457
}
458458
}
459459
widgets::divider(ui);
460-
(picked, step) = self.screen_fkey_bar(ui, &FKEY_SETS[set], (set, FKEY_SETS.len()), self.atascii_colors());
460+
if self.document.tool == Tool::Select {
461+
// Selecting by character or color and filling with the brush replaces characters.
462+
let context = ui.ctx().clone();
463+
self.selection_options(ui, &context);
464+
} else {
465+
(picked, step) = self.screen_fkey_bar(ui, &FKEY_SETS[set], (set, FKEY_SETS.len()), self.atascii_colors());
466+
}
461467
});
462468
if step != 0 {
463469
if let Some(editor) = &mut self.atascii {

‎crates/icy_draw/src/bin/icy_draw_egui/petscii.rs‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,13 @@ impl DrawApp {
688688
self.petscii_outline_options(ui);
689689
}
690690
widgets::divider(ui);
691-
(picked, step) = self.screen_fkey_bar(ui, &FKEY_SETS[set], (set, FKEY_SETS.len()), colors);
691+
if self.document.tool == Tool::Select {
692+
// Selecting by character or color and filling with the brush replaces characters.
693+
let context = ui.ctx().clone();
694+
self.selection_options(ui, &context);
695+
} else {
696+
(picked, step) = self.screen_fkey_bar(ui, &FKEY_SETS[set], (set, FKEY_SETS.len()), colors);
697+
}
692698
});
693699
if step != 0 {
694700
if let Some(editor) = &mut self.petscii {
@@ -1048,6 +1054,31 @@ mod tests {
10481054
assert_eq!(cell(&app, 2, 2).0, 0x5B);
10491055
}
10501056

1057+
#[test]
1058+
fn selecting_a_character_and_filling_with_the_brush_replaces_it() {
1059+
let (_, mut app) = petscii_app(PetsciiMachine::C64, PetsciiCase::Upper);
1060+
app.document.type_text("ABAB").unwrap();
1061+
// Select every A: the select tool's character mode.
1062+
app.document.tool = Tool::Select;
1063+
app.document.selection_mode = icy_draw::document::SelectionMode::Character;
1064+
app.document.begin(Position::new(0, 0), icy_engine::MouseButton::Left);
1065+
app.document.finish();
1066+
// Replace them with the brush: a reverse space in red.
1067+
app.document.brush.paint_char = char::from(0xA0);
1068+
app.document.with_state(|state| state.set_caret_foreground(2));
1069+
app.document.fill_selection().unwrap();
1070+
assert_eq!(
1071+
(0..4).map(|x| cell(&app, x, 0)).collect::<Vec<_>>(),
1072+
[(0xA0, 2, 6), (2, 14, 6), (0xA0, 2, 6), (2, 14, 6)]
1073+
);
1074+
app.undo(false);
1075+
assert_eq!(cell(&app, 0, 0), (1, 14, 6), "one undo step");
1076+
// Only the color.
1077+
app.apply_petscii_paint_mode(PaintMode::Color);
1078+
app.document.fill_selection().unwrap();
1079+
assert_eq!([cell(&app, 0, 0), cell(&app, 1, 0)], [(1, 2, 6), (2, 14, 6)]);
1080+
}
1081+
10511082
#[test]
10521083
fn the_reverse_pen_turns_characters_reverse_and_back() {
10531084
let (_, mut app) = petscii_app(PetsciiMachine::C64, PetsciiCase::Upper);

‎crates/icy_draw/src/bin/icy_draw_egui/vt52.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,13 @@ impl DrawApp {
306306
}
307307
}
308308
widgets::divider(ui);
309-
(picked, step) = self.screen_fkey_bar(ui, &fkey_codes(set), (set, FKEY_SETS.len()), colors);
309+
if self.document.tool == Tool::Select {
310+
// Selecting by character or color and filling with the brush replaces characters.
311+
let context = ui.ctx().clone();
312+
self.selection_options(ui, &context);
313+
} else {
314+
(picked, step) = self.screen_fkey_bar(ui, &fkey_codes(set), (set, FKEY_SETS.len()), colors);
315+
}
310316
});
311317
if step != 0 {
312318
if let Some(editor) = &mut self.vt52 {

‎crates/icy_draw/src/document.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,12 @@ impl Document {
390390
return Ok(());
391391
}
392392
self.finish();
393+
if !matches!(
394+
self.profile(),
395+
crate::screen_profile::ScreenProfile::Ansi | crate::screen_profile::ScreenProfile::Other(_)
396+
) {
397+
return self.fill_selection_with_brush();
398+
}
393399
self.with_state(|state| {
394400
let attribute = state.get_caret().attribute;
395401
if attribute.foreground() == 0 {
@@ -402,6 +408,29 @@ impl Document {
402408
.map_err(|error| error.to_string())
403409
}
404410

411+
/// Paints the brush once into every selected character of the current layer, in one undo
412+
/// step: the character with its colors, only the colors, reverse, or a shading step. Home
413+
/// computer screens fill this way, as CP437's full block is not theirs.
414+
fn fill_selection_with_brush(&mut self) -> DrawResult<()> {
415+
let mut brush = self.brush;
416+
brush.brush_size = 1;
417+
self.with_state(|state| {
418+
let Some(layer) = state.get_cur_layer() else {
419+
return;
420+
};
421+
let (offset, width, height) = (layer.offset(), layer.width(), layer.height());
422+
let selected: Vec<Position> = (0..height)
423+
.flat_map(|y| (0..width).map(move |x| Position::new(x, y) + offset))
424+
.filter(|&position| state.is_selected(position))
425+
.collect();
426+
let _undo = state.begin_atomic_undo("Fill selection");
427+
for position in selected {
428+
self.stamp_into(state, position, brush, MouseButton::Left);
429+
}
430+
});
431+
Ok(())
432+
}
433+
405434
fn start_floating_paste(&mut self, paste: impl FnOnce(&mut EditState) -> icy_engine::Result<()>) -> DrawResult<()> {
406435
self.start_floating(false, paste)
407436
}

0 commit comments

Comments
 (0)