Skip to content

Commit d5dd45b

Browse files
authored
Merge pull request #65 from rust-native-ui/fix-line-endings
Fix newline issues on Windows.
2 parents 701ca28 + 9ff9d25 commit d5dd45b

File tree

6 files changed

+112
-40
lines changed

6 files changed

+112
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
1313
- `RadioButtons` control for groups of radio buttons
1414
- `Combobox::selected()` method to retrieve the currently selected index of the combobox
1515
- Officially move communications to the Matrix room #rust-native-ui:matrix.nora.codes
16+
* `str_tools` module provides utilities for converting to and from system `CString` and
17+
`CStr` values, while enforcing correct newline values (CR vs CRLF).
1618

1719
### Changed
1820

@@ -29,12 +31,15 @@ No deprecations.
2931
### Removed
3032

3133
* `Transform` no longer implements `PartialEq` as the existing implementation was broken.
34+
* `Button` and `Label` no longer implement `text_ref` as we cannot ensure toolkit newline
35+
compliance.
3236

3337
### Fixed
3438

3539
* `VerticalBox` and `HorizontalBox` no longer link to the removed `BoxExt` trait.
3640
* `ui-sys` now builds on modern macOS.
3741
* `inputs` and `inputs-grid` examples no longer erroneously start at 0 for inputs starting at 1.
42+
* Text no longer uses incorrect newlines per platform.
3843

3944
### Security
4045

iui/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ bitflags = "1.0"
4242
libc = "0.2"
4343
failure = "0.1.1"
4444
ui-sys = { path = "../ui-sys", version = "0.2.1" }
45+
regex = "1"
46+
lazy_static = "1"
47+

iui/src/controls/basic.rs

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use super::Control;
22
use std::os::raw::c_void;
3-
use std::ffi::{CStr, CString};
43
use std::mem;
54
use ui::UI;
65
use ui_sys::{self, uiButton, uiControl, uiLabel};
6+
use str_tools::{from_toolkit_string, to_toolkit_string};
77

88
define_control!{
99
/// A non-interactable piece of text.
@@ -20,30 +20,23 @@ define_control!{
2020
impl Button {
2121
/// Create a new button with the given text as its label.
2222
pub fn new(_ctx: &UI, text: &str) -> Button {
23+
let c_string = to_toolkit_string(text);
2324
unsafe {
24-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
2525
Button::from_raw(ui_sys::uiNewButton(c_string.as_ptr()))
2626
}
2727
}
2828

2929
/// Get a copy of the existing text on the button.
3030
pub fn text(&self, _ctx: &UI) -> String {
3131
unsafe {
32-
CStr::from_ptr(ui_sys::uiButtonText(self.uiButton))
33-
.to_string_lossy()
34-
.into_owned()
32+
from_toolkit_string(ui_sys::uiButtonText(self.uiButton))
3533
}
3634
}
3735

38-
/// Get a reference to the existing text on the button.
39-
pub fn text_ref(&self, _ctx: &UI) -> &CStr {
40-
unsafe { CStr::from_ptr(ui_sys::uiButtonText(self.uiButton)) }
41-
}
42-
4336
/// Set the text on the button.
4437
pub fn set_text(&mut self, _ctx: &UI, text: &str) {
38+
let c_string = to_toolkit_string(text);
4539
unsafe {
46-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
4740
ui_sys::uiButtonSetText(self.uiButton, c_string.as_ptr())
4841
}
4942
}
@@ -74,30 +67,23 @@ impl Label {
7467
/// Note that labels do not auto-wrap their text; they will expand as far as needed
7568
/// to fit.
7669
pub fn new(_ctx: &UI, text: &str) -> Label {
70+
let c_string = to_toolkit_string(text);
7771
unsafe {
78-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
7972
Label::from_raw(ui_sys::uiNewLabel(c_string.as_ptr()))
8073
}
8174
}
8275

8376
/// Get a copy of the existing text on the label.
8477
pub fn text(&self, _ctx: &UI) -> String {
8578
unsafe {
86-
CStr::from_ptr(ui_sys::uiLabelText(self.uiLabel))
87-
.to_string_lossy()
88-
.into_owned()
79+
from_toolkit_string(ui_sys::uiLabelText(self.uiLabel))
8980
}
9081
}
9182

92-
/// Get a reference to the existing text on the label.
93-
pub fn text_ref(&self, _ctx: &UI) -> &CStr {
94-
unsafe { CStr::from_ptr(ui_sys::uiLabelText(self.uiLabel)) }
95-
}
96-
9783
/// Set the text on the label.
9884
pub fn set_text(&mut self, _ctx: &UI, text: &str) {
85+
let c_string = to_toolkit_string(text);
9986
unsafe {
100-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
10187
ui_sys::uiLabelSetText(self.uiLabel, c_string.as_ptr())
10288
}
10389
}

iui/src/controls/entry.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
//! User input mechanisms: numbers, colors, and text in various forms.
2+
//!
3+
//! All text buffers accept and return `\n` line endings; if on Windows, the appropriate
4+
//! `\r\n` for display are added and removed by the controls.
25
36
use super::Control;
47
use std::ffi::{CStr, CString};
58
use std::i32;
69
use std::mem;
710
use std::os::raw::c_void;
8-
use ui::UI;
911
use ui_sys::{
1012
self, uiCheckbox, uiCombobox, uiControl, uiEntry, uiMultilineEntry, uiRadioButtons, uiSlider,
1113
uiSpinbox,
1214
};
15+
use ui::UI;
16+
use str_tools::{from_toolkit_string, to_toolkit_string};
1317

1418
pub trait NumericEntry {
1519
fn value(&self, ctx: &UI) -> i32;
@@ -150,14 +154,11 @@ impl MultilineEntry {
150154

151155
impl TextEntry for Entry {
152156
fn value(&self, _ctx: &UI) -> String {
153-
unsafe {
154-
CStr::from_ptr(ui_sys::uiEntryText(self.uiEntry))
155-
.to_string_lossy()
156-
.into_owned()
157-
}
157+
unsafe { from_toolkit_string(ui_sys::uiEntryText(self.uiEntry)) }
158158
}
159+
159160
fn set_value(&mut self, _ctx: &UI, value: &str) {
160-
let cstring = CString::new(value.as_bytes().to_vec()).unwrap();
161+
let cstring = to_toolkit_string(value);
161162
unsafe { ui_sys::uiEntrySetText(self.uiEntry, cstring.as_ptr()) }
162163
}
163164

@@ -210,9 +211,7 @@ impl TextEntry for PasswordEntry {
210211

211212
extern "C" fn c_callback(entry: *mut uiEntry, data: *mut c_void) {
212213
unsafe {
213-
let string = CStr::from_ptr(ui_sys::uiEntryText(entry))
214-
.to_string_lossy()
215-
.into_owned();
214+
let string = from_toolkit_string(ui_sys::uiEntryText(entry));
216215
mem::transmute::<*mut c_void, &mut Box<dyn FnMut(String)>>(data)(string);
217216
mem::forget(entry);
218217
}
@@ -223,13 +222,12 @@ impl TextEntry for PasswordEntry {
223222
impl TextEntry for MultilineEntry {
224223
fn value(&self, _ctx: &UI) -> String {
225224
unsafe {
226-
CStr::from_ptr(ui_sys::uiMultilineEntryText(self.uiMultilineEntry))
227-
.to_string_lossy()
228-
.into_owned()
225+
from_toolkit_string(ui_sys::uiMultilineEntryText(self.uiMultilineEntry))
229226
}
230227
}
228+
231229
fn set_value(&mut self, _ctx: &UI, value: &str) {
232-
let cstring = CString::new(value.as_bytes().to_vec()).unwrap();
230+
let cstring = to_toolkit_string(value);
233231
unsafe { ui_sys::uiMultilineEntrySetText(self.uiMultilineEntry, cstring.as_ptr()) }
234232
}
235233

@@ -246,9 +244,7 @@ impl TextEntry for MultilineEntry {
246244

247245
extern "C" fn c_callback(entry: *mut uiMultilineEntry, data: *mut c_void) {
248246
unsafe {
249-
let string = CStr::from_ptr(ui_sys::uiMultilineEntryText(entry))
250-
.to_string_lossy()
251-
.into_owned();
247+
let string = from_toolkit_string(ui_sys::uiMultilineEntryText(entry));
252248
mem::transmute::<*mut c_void, &mut Box<dyn FnMut(String)>>(data)(string);
253249
mem::forget(entry);
254250
}
@@ -271,7 +267,7 @@ impl Combobox {
271267
/// Adds a new option to the combination box.
272268
pub fn append(&self, _ctx: &UI, name: &str) {
273269
unsafe {
274-
let c_string = CString::new(name.as_bytes().to_vec()).unwrap();
270+
let c_string = to_toolkit_string(name);
275271
ui_sys::uiComboboxAppend(self.uiCombobox, c_string.as_ptr())
276272
}
277273
}
@@ -314,7 +310,7 @@ define_control! {
314310
impl Checkbox {
315311
// Create a new Checkbox which can produce values from `min` to `max`.
316312
pub fn new(_ctx: &UI, text: &str) -> Self {
317-
let c_string = CString::new(text.as_bytes().to_vec()).unwrap();
313+
let c_string = to_toolkit_string(text);
318314
unsafe { Checkbox::from_raw(ui_sys::uiNewCheckbox(c_string.as_ptr())) }
319315
}
320316

iui/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@
2121
extern crate bitflags;
2222
#[macro_use]
2323
extern crate failure;
24+
#[macro_use]
25+
extern crate lazy_static;
2426
extern crate libc;
2527
extern crate ui_sys;
28+
extern crate regex;
2629

2730
pub mod controls;
2831
pub mod draw;
2932
mod error;
3033
mod ffi_tools;
3134
pub mod menus;
3235
mod ui;
36+
pub mod str_tools;
3337

3438
pub use error::UIError;
3539
pub use ui::{EventLoop, UI};

iui/src/str_tools.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Tools for making platform-independent string handling work properly
2+
3+
use regex::{Regex, RegexBuilder};
4+
use std::ffi::{CStr, CString};
5+
use std::os::raw::c_char;
6+
7+
/// Replaces every occurrance of `"\r\n"` with a single newline `\n`, without collapsing
8+
/// newlines.
9+
pub fn strip_dual_endings(s: &str) -> String {
10+
s.replace("\r\n", "\n")
11+
}
12+
13+
/// Replaces every occurrance of `"\n"` not followed by `"\r"` with `"\r\n"`.
14+
pub fn insert_dual_endings(s: &str) -> String {
15+
lazy_static! {
16+
//static ref RE: Regex = Regex::new("([^\r])\n").expect("Could not compile regex");
17+
static ref RE: Regex = RegexBuilder::new("\r\n|\n")
18+
.multi_line(true)
19+
.build()
20+
.expect("Could not compile regex");
21+
}
22+
RE.replace_all(s, "\r\n").to_string()
23+
}
24+
25+
/// Converts a &str to a CString, using either LF or CRLF as appropriate.
26+
///
27+
/// # Panics
28+
/// Panics if it isn't possible to create a CString from the given string.
29+
pub fn to_toolkit_string(s: &str) -> CString {
30+
let data = if cfg!(windows) {
31+
insert_dual_endings(s).as_bytes().to_vec()
32+
} else {
33+
s.as_bytes().to_vec()
34+
};
35+
CString::new(data).expect(&format!("Failed to create CString from {}", s))
36+
}
37+
38+
/// Converts a `*mut c_char` to a String guaranteed to use LF line endings.
39+
///
40+
/// # Unsafety
41+
/// Has the same unsafety as [CStr::from_ptr](https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr).
42+
pub unsafe fn from_toolkit_string(c: *mut c_char) -> String {
43+
CStr::from_ptr(c).to_string_lossy().into_owned()
44+
}
45+
46+
#[cfg(test)]
47+
mod test {
48+
use super::*;
49+
50+
#[test]
51+
fn strip_dual_endings_to_single() {
52+
assert_eq!(strip_dual_endings("Line 1\r\nLine 2\r\n"),
53+
"Line 1\nLine 2\n");
54+
}
55+
56+
#[test]
57+
fn insert_dual_endings_basic() {
58+
assert_eq!(insert_dual_endings("Line 1\nLine 2\n"),
59+
"Line 1\r\nLine 2\r\n");
60+
}
61+
62+
#[test]
63+
fn insert_dual_endings_nodupe() {
64+
assert_eq!(insert_dual_endings("Line 1\r\nLine 2\r\n"),
65+
"Line 1\r\nLine 2\r\n");
66+
}
67+
68+
#[test]
69+
fn test_toolkit_roundtripping() {
70+
let initial_string = "Here is some test data.\n\nMultiline!\n";
71+
let toolkit_string = to_toolkit_string(initial_string);
72+
let roundtripped_string = unsafe {
73+
from_toolkit_string(toolkit_string.into_raw())
74+
};
75+
assert_eq!(initial_string, &roundtripped_string);
76+
}
77+
}
78+

0 commit comments

Comments
 (0)